mod v2; pub mod views; use std::sync::Arc; use cot::App; use cot::db::Database; use cot::db::migrations::SyncDynMigration; use cot::json::Json; 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 serde::Deserialize; use crate::auth; use crate::config::AppConfig; use crate::i18n::I18n; use crate::scheduler::{JobRegistry, SchedulerHandle}; use crate::user::User; use views::{ ArtistForm, CronForm, MetadataBackfillForm, OidcSettingsForm, ReleaseForm, ReviewsBulkForm, SetImageBody, SetupForm, UploadImageBody, UserForm, }; #[derive(Debug, Deserialize)] struct ReviewsQuery { status: Option, } /// 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, registry: Arc, scheduler_handle: Arc>>, } impl AdminApp { pub fn new( config: Arc, registry: Arc, scheduler_handle: Arc>>, ) -> Self { Self { config, registry, scheduler_handle, } } } #[derive(Debug, Deserialize)] struct SettingsQuery { saved: Option, } #[derive(Debug, Deserialize)] struct PathId { id: i64, } #[derive(Debug, Deserialize)] struct PathName { name: String, } #[derive(Debug, Deserialize)] struct PathNameRunId { name: String, run_id: i64, } #[derive(Debug, Deserialize)] struct ReleasesQuery { artist_id: Option, } impl App for AdminApp { fn name(&self) -> &'static str { "admin" } fn router(&self) -> Router { // Create a shared sqlx pool for admin routes that need it let pool_config = Arc::clone(&self.config); let pool: Arc> = Arc::new(tokio::sync::OnceCell::new()); 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", ), // -- Admin v2 ----------------------------------------------------- Route::with_handler_and_name( "/v2", |session: Session, db: Database, i18n: I18n| async move { 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), }; v2::page(admin, i18n).await?.into_response() }, "admin_v2", ), Route::with_handler_and_name( "/v2/api/dashboard", { let pool = Arc::clone(&pool); let pool_config = Arc::clone(&pool_config); let registry = Arc::clone(&self.registry); get(move |session: Session, db: Database| { let pool = Arc::clone(&pool); let pool_config = Arc::clone(&pool_config); let registry = Arc::clone(®istry); async move { let pg_pool = pool .get_or_init(|| async { sqlx::postgres::PgPoolOptions::new() .max_connections(5) .connect(&pool_config.database_url) .await .expect("admin pool") }) .await; v2::dashboard(session, db, pg_pool, ®istry).await } }) }, "admin_v2_dashboard", ), Route::with_handler_and_name( "/v2/api/reviews", { let pool = Arc::clone(&pool); let pool_config = Arc::clone(&pool_config); get(move |session: Session, db: Database, query: UrlQuery| { let pool = Arc::clone(&pool); let pool_config = Arc::clone(&pool_config); async move { let pg_pool = pool .get_or_init(|| async { sqlx::postgres::PgPoolOptions::new() .max_connections(5) .connect(&pool_config.database_url) .await .expect("admin pool") }) .await; v2::reviews(session, db, pg_pool, query.0).await } }) }, "admin_v2_reviews", ), Route::with_handler_and_name( "/v2/api/reviews/bulk", { let pool = Arc::clone(&pool); let pool_config = Arc::clone(&pool_config); cot::router::method::post( move |session: Session, db: Database, json: Json| { let pool = Arc::clone(&pool); let pool_config = Arc::clone(&pool_config); async move { let pg_pool = pool .get_or_init(|| async { sqlx::postgres::PgPoolOptions::new() .max_connections(5) .connect(&pool_config.database_url) .await .expect("admin pool") }) .await; v2::bulk_reviews(session, db, pg_pool, json).await } }, ) }, "admin_v2_reviews_bulk", ), Route::with_handler_and_name( "/v2/api/jobs", { let pool = Arc::clone(&pool); let pool_config = Arc::clone(&pool_config); let registry = Arc::clone(&self.registry); get(move |session: Session, db: Database| { let pool = Arc::clone(&pool); let pool_config = Arc::clone(&pool_config); let registry = Arc::clone(®istry); async move { let pg_pool = pool .get_or_init(|| async { sqlx::postgres::PgPoolOptions::new() .max_connections(5) .connect(&pool_config.database_url) .await .expect("admin pool") }) .await; v2::jobs(session, db, pg_pool, ®istry).await } }) }, "admin_v2_jobs", ), Route::with_handler_and_name( "/v2/api/jobs/{name}/run", cot::router::method::post({ let handle = Arc::clone(&self.scheduler_handle); move |session: Session, db: Database, path: Path| { let handle = Arc::clone(&handle); async move { v2::run_job(session, db, &handle, &path.0.name).await } } }), "admin_v2_job_run", ), Route::with_handler_and_name( "/v2/api/jobs/{name}/toggle", cot::router::method::post({ let handle = Arc::clone(&self.scheduler_handle); move |session: Session, db: Database, path: Path| { let handle = Arc::clone(&handle); async move { v2::toggle_job(session, db, &handle, &path.0.name).await } } }), "admin_v2_job_toggle", ), Route::with_handler_and_name( "/v2/api/jobs/{name}/runs", { let pool = Arc::clone(&pool); let pool_config = Arc::clone(&pool_config); get(move |session: Session, db: Database, path: Path| { let pool = Arc::clone(&pool); let pool_config = Arc::clone(&pool_config); async move { let pg_pool = pool .get_or_init(|| async { sqlx::postgres::PgPoolOptions::new() .max_connections(5) .connect(&pool_config.database_url) .await .expect("admin pool") }) .await; v2::job_runs(session, db, pg_pool, &path.0.name).await } }) }, "admin_v2_job_runs", ), Route::with_handler_and_name( "/v2/api/jobs/{name}/runs/{run_id}", { let pool = Arc::clone(&pool); let pool_config = Arc::clone(&pool_config); get( move |session: Session, db: Database, path: Path| { let pool = Arc::clone(&pool); let pool_config = Arc::clone(&pool_config); async move { let pg_pool = pool .get_or_init(|| async { sqlx::postgres::PgPoolOptions::new() .max_connections(5) .connect(&pool_config.database_url) .await .expect("admin pool") }) .await; v2::job_run_detail(session, db, pg_pool, path.0.run_id).await } }, ) }, "admin_v2_job_run_detail", ), Route::with_handler_and_name( "/v2/api/library", { let pool = Arc::clone(&pool); let pool_config = Arc::clone(&pool_config); get(move |session: Session, db: Database, query: UrlQuery| { let pool = Arc::clone(&pool); let pool_config = Arc::clone(&pool_config); async move { let pg_pool = pool .get_or_init(|| async { sqlx::postgres::PgPoolOptions::new() .max_connections(5) .connect(&pool_config.database_url) .await .expect("admin pool") }) .await; v2::library(session, db, pg_pool, query.0).await } }) }, "admin_v2_library", ), Route::with_handler_and_name( "/v2/api/library/item", { let pool = Arc::clone(&pool); let pool_config = Arc::clone(&pool_config); cot::router::method::post( move |session: Session, db: Database, json: Json| { let pool = Arc::clone(&pool); let pool_config = Arc::clone(&pool_config); async move { let pg_pool = pool .get_or_init(|| async { sqlx::postgres::PgPoolOptions::new() .max_connections(5) .connect(&pool_config.database_url) .await .expect("admin pool") }) .await; v2::update_library_item(session, db, pg_pool, json).await } }, ) }, "admin_v2_library_item", ), Route::with_handler_and_name( "/v2/api/library/bulk", { let pool = Arc::clone(&pool); let pool_config = Arc::clone(&pool_config); cot::router::method::post( move |session: Session, db: Database, json: Json| { let pool = Arc::clone(&pool); let pool_config = Arc::clone(&pool_config); async move { let pg_pool = pool .get_or_init(|| async { sqlx::postgres::PgPoolOptions::new() .max_connections(5) .connect(&pool_config.database_url) .await .expect("admin pool") }) .await; v2::bulk_library(session, db, pg_pool, json).await } }, ) }, "admin_v2_library_bulk", ), // -- Dashboard ---------------------------------------------------- Route::with_handler_and_name( "/", |session: Session, db: Database, i18n: I18n| async move { 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), }; let _ = i18n; Ok::(auth::redirect("/admin/v2")) }, "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", ), // -- Settings probe (HTMX fragment) ----------------------------------- Route::with_handler_and_name( "/settings/probe", { 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::settings_probe_handler(admin, i18n, &config, &db) .await? .into_response() } } }, "admin_settings_probe", ), // -- 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", ), // -- Artists ------------------------------------------------------ Route::with_handler_and_name( "/artists", |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::artists_list(admin, i18n, &db).await?.into_response() }, "admin_artists", ), Route::with_handler_and_name( "/artists/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::artists_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::artists_create(admin, &db, form).await }, ), "admin_artists_new", ), Route::with_handler_and_name( "/artists/{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::artists_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::artists_update(admin, &db, path.0.id, form).await }, ), "admin_artists_edit", ), Route::with_handler_and_name( "/artists/{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::artists_delete(admin, &db, path.0.id).await }, ), "admin_artists_delete", ), Route::with_handler_and_name( "/artists/{id}/available-covers", get( |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::artists_available_covers(admin, &db, path.0.id).await }, ), "admin_artists_available_covers", ), Route::with_handler_and_name( "/artists/{id}/set-image", cot::router::method::post( |session: Session, db: Database, path: Path, json: Json| async move { let admin = match auth::require_admin_or_redirect(&session, &db).await { Ok(u) => u, Err(resp) => return Ok(resp), }; views::artists_set_image(admin, &db, path.0.id, json.0).await }, ), "admin_artists_set_image", ), Route::with_handler_and_name( "/artists/{id}/upload-image", cot::router::method::post({ let pool = Arc::clone(&pool); let pool_config = Arc::clone(&pool_config); move |session: Session, db: Database, path: Path, json: Json| { let pool = Arc::clone(&pool); let pool_config = Arc::clone(&pool_config); async move { let admin = match auth::require_admin_or_redirect(&session, &db).await { Ok(u) => u, Err(resp) => return Ok(resp), }; let pg_pool = pool.get_or_init(|| async { sqlx::postgres::PgPoolOptions::new() .max_connections(3) .connect(&pool_config.database_url) .await .expect("admin pool") }).await; let (live_config, _) = AppConfig::load_with_db(&db).await; views::artists_upload_image(admin, &db, pg_pool, &live_config, path.0.id, json.0).await } } }), "admin_artists_upload_image", ), // -- Releases ----------------------------------------------------- Route::with_handler_and_name( "/releases", |session: Session, db: Database, i18n: I18n, query: UrlQuery| async move { let admin = match auth::require_admin_or_redirect(&session, &db).await { Ok(u) => u, Err(resp) => return Ok(resp), }; views::releases_list(admin, i18n, &db, query.0.artist_id) .await? .into_response() }, "admin_releases", ), Route::with_handler_and_name( "/releases/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::releases_new(admin, i18n, &db).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::releases_create(admin, &db, form).await }, ), "admin_releases_new", ), Route::with_handler_and_name( "/releases/{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::releases_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::releases_update(admin, &db, path.0.id, form).await }, ), "admin_releases_edit", ), Route::with_handler_and_name( "/releases/{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::releases_delete(admin, &db, path.0.id).await }, ), "admin_releases_delete", ), // -- Media Files -------------------------------------------------- Route::with_handler_and_name( "/media-files", |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::media_files_list(admin, i18n, &db).await?.into_response() }, "admin_media_files", ), Route::with_handler_and_name( "/media-files/{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::media_files_delete(admin, &db, path.0.id).await }, ), "admin_media_files_delete", ), // -- Jobs --------------------------------------------------------- Route::with_handler_and_name( "/jobs", { let registry = Arc::clone(&self.registry); move |session: Session, db: Database, i18n: I18n| { let registry = Arc::clone(®istry); async move { let admin = match auth::require_admin_or_redirect(&session, &db).await { Ok(u) => u, Err(resp) => return Ok(resp), }; views::jobs_list(admin, i18n, &db, ®istry).await?.into_response() } } }, "admin_jobs", ), Route::with_handler_and_name( "/jobs/metadata_backfill/run-options", cot::router::method::post({ let pool = Arc::clone(&pool); let pool_config = Arc::clone(&pool_config); move |session: Session, db: Database, form: RequestForm| { let pool = Arc::clone(&pool); let pool_config = Arc::clone(&pool_config); async move { let admin = match auth::require_admin_or_redirect(&session, &db).await { Ok(u) => u, Err(resp) => return Ok(resp), }; let pg_pool = pool.get_or_init(|| async { sqlx::postgres::PgPoolOptions::new() .max_connections(3) .connect(&pool_config.database_url) .await .expect("admin pool") }).await; views::metadata_backfill_run(admin, &db, pg_pool, form).await } } }), "admin_metadata_backfill_run", ), Route::with_handler_and_name( "/jobs/{name}/run", cot::router::method::post({ let handle = Arc::clone(&self.scheduler_handle); move |session: Session, db: Database, path: Path| { let handle = Arc::clone(&handle); async move { let admin = match auth::require_admin_or_redirect(&session, &db).await { Ok(u) => u, Err(resp) => return Ok(resp), }; views::job_run_now(admin, &handle, &path.0.name).await } } }), "admin_job_run", ), Route::with_handler_and_name( "/jobs/{name}/toggle", cot::router::method::post({ let handle = Arc::clone(&self.scheduler_handle); move |session: Session, db: Database, path: Path| { let handle = Arc::clone(&handle); async move { let admin = match auth::require_admin_or_redirect(&session, &db).await { Ok(u) => u, Err(resp) => return Ok(resp), }; views::job_toggle_enabled(admin, &db, &handle, &path.0.name).await } } }), "admin_job_toggle", ), Route::with_handler_and_name( "/jobs/{name}/cron", cot::router::method::post({ let handle = Arc::clone(&self.scheduler_handle); move |session: Session, db: Database, path: Path, form: RequestForm| { let handle = Arc::clone(&handle); async move { let admin = match auth::require_admin_or_redirect(&session, &db).await { Ok(u) => u, Err(resp) => return Ok(resp), }; views::job_update_cron(admin, &db, &handle, &path.0.name, form).await } } }), "admin_job_cron", ), Route::with_handler_and_name( "/jobs/{name}/runs/{run_id}", { move |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::job_run_detail(admin, i18n, &db, &path.0.name, path.0.run_id) .await? .into_response() } } }, "admin_job_run_detail", ), Route::with_handler_and_name( "/jobs/{name}", { let pool = Arc::clone(&pool); let pool_config = Arc::clone(&pool_config); move |session: Session, db: Database, i18n: I18n, path: Path| { let pool = Arc::clone(&pool); let pool_config = Arc::clone(&pool_config); async move { let admin = match auth::require_admin_or_redirect(&session, &db).await { Ok(u) => u, Err(resp) => return Ok(resp), }; let pg_pool = pool.get_or_init(|| async { sqlx::postgres::PgPoolOptions::new() .max_connections(3) .connect(&pool_config.database_url) .await .expect("admin pool") }).await; views::job_detail(admin, i18n, &db, pg_pool, &path.0.name) .await? .into_response() } } }, "admin_job_detail", ), // -- Reviews: clear ----------------------------------------------- Route::with_handler_and_name( "/reviews/clear", cot::router::method::post( |session: Session, db: Database, query: UrlQuery| async move { let admin = match auth::require_admin_or_redirect(&session, &db).await { Ok(u) => u, Err(resp) => return Ok(resp), }; views::reviews_clear(admin, &db, query.0.status.as_deref()).await }, ), "admin_reviews_clear", ), Route::with_handler_and_name( "/reviews/bulk", cot::router::method::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::reviews_bulk(admin, &db, form).await }, ), "admin_reviews_bulk", ), // -- Reviews ------------------------------------------------------ Route::with_handler_and_name( "/reviews", { let pool = Arc::clone(&pool); let pool_config = Arc::clone(&pool_config); move |session: Session, db: Database, i18n: I18n, query: UrlQuery| { let pool = Arc::clone(&pool); let pool_config = Arc::clone(&pool_config); async move { let admin = match auth::require_admin_or_redirect(&session, &db).await { Ok(u) => u, Err(resp) => return Ok(resp), }; let pg_pool = pool.get_or_init(|| async { sqlx::postgres::PgPoolOptions::new() .max_connections(3) .connect(&pool_config.database_url) .await .expect("admin pool") }).await; views::reviews_list(admin, i18n, &db, pg_pool, query.0.status.as_deref()) .await? .into_response() } } }, "admin_reviews", ), Route::with_handler_and_name( "/reviews/{id}", |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::review_detail(admin, i18n, &db, path.0.id) .await? .into_response() }, "admin_review_detail", ), Route::with_handler_and_name( "/reviews/{id}/approve", cot::router::method::post({ let config = Arc::clone(&self.config); let pool = Arc::clone(&pool); let pool_config = Arc::clone(&pool_config); move |session: Session, db: Database, path: Path| { let config = Arc::clone(&config); let pool = Arc::clone(&pool); let pool_config = Arc::clone(&pool_config); async move { let admin = match auth::require_admin_or_redirect(&session, &db).await { Ok(u) => u, Err(resp) => return Ok(resp), }; let pg_pool = pool.get_or_init(|| async { sqlx::postgres::PgPoolOptions::new() .max_connections(3) .connect(&pool_config.database_url) .await .expect("admin pool") }).await; views::review_approve(admin, &config, &db, pg_pool, path.0.id).await } } }), "admin_review_approve", ), Route::with_handler_and_name( "/reviews/{id}/reject", 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::review_reject(admin, &db, path.0.id).await }, ), "admin_review_reject", ), Route::with_handler_and_name( "/reviews/{id}/requeue", 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::review_requeue(admin, &db, path.0.id).await }, ), "admin_review_requeue", ), ]) } 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.extend(cot::db::migrations::wrap_migrations( crate::music::db_migrations::MIGRATIONS, )); all.extend(cot::db::migrations::wrap_migrations( crate::scheduler::db_migrations::MIGRATIONS, )); all } }