pub mod views; use std::sync::Arc; use cot::db::Database; use cot::db::migrations::SyncDynMigration; use cot::request::extractors::{Path, RequestForm, UrlQuery}; use cot::response::IntoResponse; use cot::router::method::get; use cot::router::{Route, Router}; use cot::session::Session; use cot::App; use serde::Deserialize; use crate::auth; use crate::config::AppConfig; use crate::i18n::I18n; use crate::user::User; use views::{OidcSettingsForm, SetupForm, UserForm}; /// Build-time metadata baked in by `build.rs` and Cargo env vars. #[derive(Debug)] pub struct BuildInfo { pub pkg_name: &'static str, pub pkg_version: &'static str, pub profile: &'static str, pub target: &'static str, pub rustc_version: &'static str, } pub static BUILD_INFO: BuildInfo = BuildInfo { pkg_name: env!("CARGO_PKG_NAME"), pkg_version: env!("CARGO_PKG_VERSION"), profile: if cfg!(debug_assertions) { "debug" } else { "release" }, target: env!("FURU_TARGET"), rustc_version: env!("FURU_RUSTC_VERSION"), }; pub struct AdminApp { config: Arc, } impl AdminApp { pub fn new(config: Arc) -> Self { Self { config } } } #[derive(Debug, Deserialize)] struct SettingsQuery { saved: Option, } #[derive(Debug, Deserialize)] struct PathId { id: i64, } impl App for AdminApp { fn name(&self) -> &'static str { "admin" } fn router(&self) -> Router { Router::with_urls([ // -- Setup (first-run, no auth required) -------------------------- Route::with_handler_and_name( "/setup", get(|i18n: I18n, db: Database| async move { let count = User::count_all(&db).await.unwrap_or(1); if count > 0 { return Ok(auth::redirect("/admin/")); } views::setup_page(i18n, String::new()) .await? .into_response() }) .post( |i18n: I18n, db: Database, session: Session, form: RequestForm| async move { let count = User::count_all(&db).await.unwrap_or(1); if count > 0 { return Ok(auth::redirect("/admin/")); } views::setup_submit(i18n, &db, &session, form).await }, ), "admin_setup", ), // -- Dashboard ---------------------------------------------------- Route::with_handler_and_name( "/", |session: Session, db: Database, i18n: I18n| async move { // First-run redirect let count = User::count_all(&db).await.unwrap_or(0); if count == 0 { return Ok(auth::redirect("/admin/setup")); } let admin = match auth::require_admin_or_redirect(&session, &db).await { Ok(u) => u, Err(resp) => return Ok(resp), }; views::admin_index(admin, i18n).await?.into_response() }, "admin_index", ), // -- Debug -------------------------------------------------------- Route::with_handler_and_name( "/debug", { let config = Arc::clone(&self.config); move |session: Session, db: Database, i18n: I18n| { let config = Arc::clone(&config); async move { let admin = match auth::require_admin_or_redirect(&session, &db).await { Ok(u) => u, Err(resp) => return Ok(resp), }; views::debug_handler(admin, i18n, &config, &db) .await? .into_response() } } }, "admin_debug", ), // -- Settings ----------------------------------------------------- Route::with_handler_and_name( "/settings", get({ let config = Arc::clone(&self.config); move |session: Session, db: Database, i18n: I18n, query: UrlQuery| { let config = Arc::clone(&config); async move { let admin = match auth::require_admin_or_redirect(&session, &db).await { Ok(u) => u, Err(resp) => return Ok(resp), }; let saved = query.0.saved.as_deref() == Some("1"); views::settings_handler(admin, i18n, &config, &db, saved) .await? .into_response() } } }) .post({ let config = Arc::clone(&self.config); move |session: Session, db: Database, i18n: I18n, form: RequestForm| { let config = Arc::clone(&config); async move { let admin = match auth::require_admin_or_redirect(&session, &db).await { Ok(u) => u, Err(resp) => return Ok(resp), }; views::settings_submit(admin, i18n, &config, &db, form).await } } }), "admin_settings", ), // -- Users -------------------------------------------------------- Route::with_handler_and_name( "/users", |session: Session, db: Database, i18n: I18n| async move { let admin = match auth::require_admin_or_redirect(&session, &db).await { Ok(u) => u, Err(resp) => return Ok(resp), }; views::users_list(admin, i18n, &db).await?.into_response() }, "admin_users", ), Route::with_handler_and_name( "/users/new", get(|session: Session, db: Database, i18n: I18n| async move { let admin = match auth::require_admin_or_redirect(&session, &db).await { Ok(u) => u, Err(resp) => return Ok(resp), }; views::users_new(admin, i18n).await?.into_response() }) .post( |session: Session, db: Database, form: RequestForm| async move { let admin = match auth::require_admin_or_redirect(&session, &db).await { Ok(u) => u, Err(resp) => return Ok(resp), }; views::users_create(admin, &db, form).await }, ), "admin_users_new", ), Route::with_handler_and_name( "/users/{id}/edit", get( |session: Session, db: Database, i18n: I18n, path: Path| async move { let admin = match auth::require_admin_or_redirect(&session, &db).await { Ok(u) => u, Err(resp) => return Ok(resp), }; views::users_edit(admin, i18n, &db, path.0.id) .await? .into_response() }, ) .post( |session: Session, db: Database, path: Path, form: RequestForm| async move { let admin = match auth::require_admin_or_redirect(&session, &db).await { Ok(u) => u, Err(resp) => return Ok(resp), }; views::users_update(admin, &db, path.0.id, form).await }, ), "admin_users_edit", ), Route::with_handler_and_name( "/users/{id}/delete", cot::router::method::post( |session: Session, db: Database, path: Path| async move { let admin = match auth::require_admin_or_redirect(&session, &db).await { Ok(u) => u, Err(resp) => return Ok(resp), }; views::users_delete(admin, &db, path.0.id).await }, ), "admin_users_delete", ), ]) } fn migrations(&self) -> Vec> { let mut all = cot::db::migrations::wrap_migrations(crate::config::db_migrations::MIGRATIONS); all.extend(cot::db::migrations::wrap_migrations( crate::user::db_migrations::MIGRATIONS, )); all } }