//! Platform admin handlers. use axum::{ extract::{Path, Query}, http::StatusCode, response::{IntoResponse, Redirect, Response}, Form, }; use tower_sessions::Session; use crate::auth::PlatformAdmin; use crate::csrf; use crate::templates::*; use crate::AppState; use mt_core::types::ModAction; use super::{ get_community, log_mod_action, parse_uuid, template_user, AdminSearchQuery, CleanSlateForm, SuspendForm, }; #[tracing::instrument(skip_all)] pub(super) async fn admin_dashboard( axum::extract::State(state): axum::extract::State, session: Session, PlatformAdmin(admin): PlatformAdmin, Query(query): Query, ) -> Result { let csrf_token = Some(csrf::get_or_create_token(&session).await); let communities = mt_db::queries::list_all_communities(&state.db) .await .map_err(|e| { tracing::error!(error = ?e, "db error listing communities"); (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error").into_response() })? .into_iter() .map(|c| AdminCommunityViewRow { id: c.id.to_string(), name: c.name, slug: c.slug, is_suspended: c.suspended_at.is_some(), suspension_reason: c.suspension_reason, }) .collect(); let search_query = query.q.unwrap_or_default(); let users = if !search_query.is_empty() { mt_db::queries::search_users(&state.db, &search_query) .await .map_err(|e| { tracing::error!(error = ?e, "db error searching users"); (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error").into_response() })? .into_iter() .map(|u| AdminUserViewRow { id: u.id.to_string(), username: u.username, display_name: u.display_name, is_suspended: u.suspended_at.is_some(), suspension_reason: u.suspension_reason, }) .collect() } else { vec![] }; Ok(AdminDashboardTemplate { csrf_token, session_user: Some(TemplateSessionUser { is_platform_admin: true, username: admin.username, }), mnw_base_url: state.config.mnw_base_url.clone(), communities, users, search_query, }) } #[tracing::instrument(skip_all)] pub(super) async fn suspend_community_handler( axum::extract::State(state): axum::extract::State, PlatformAdmin(admin): PlatformAdmin, Path(id): Path, Form(form): Form, ) -> Result { let community_id = parse_uuid(&id)?; let reason = form.reason.as_deref().filter(|r| !r.trim().is_empty()); mt_db::mutations::suspend_community(&state.db, community_id, reason) .await .map_err(|e| { tracing::error!(error = ?e, "db error suspending community"); (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error").into_response() })?; log_mod_action( &state.db, None, admin.user_id, ModAction::SuspendCommunity, None, Some(community_id), reason, ).await; Ok(Redirect::to("/_admin?toast=Community+suspended")) } #[tracing::instrument(skip_all)] pub(super) async fn unsuspend_community_handler( axum::extract::State(state): axum::extract::State, PlatformAdmin(admin): PlatformAdmin, Path(id): Path, ) -> Result { let community_id = parse_uuid(&id)?; mt_db::mutations::unsuspend_community(&state.db, community_id) .await .map_err(|e| { tracing::error!(error = ?e, "db error unsuspending community"); (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error").into_response() })?; log_mod_action( &state.db, None, admin.user_id, ModAction::UnsuspendCommunity, None, Some(community_id), None, ).await; Ok(Redirect::to("/_admin?toast=Community+unsuspended")) } #[tracing::instrument(skip_all)] pub(super) async fn suspend_user_handler( axum::extract::State(state): axum::extract::State, PlatformAdmin(admin): PlatformAdmin, Path(id): Path, Form(form): Form, ) -> Result { let user_id = parse_uuid(&id)?; let reason = form.reason.as_deref().filter(|r| !r.trim().is_empty()); mt_db::mutations::suspend_user(&state.db, user_id, reason) .await .map_err(|e| { tracing::error!(error = ?e, "db error suspending user"); (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error").into_response() })?; log_mod_action( &state.db, None, admin.user_id, ModAction::SuspendUser, Some(user_id), None, reason, ).await; Ok(Redirect::to("/_admin?toast=User+suspended")) } #[tracing::instrument(skip_all)] pub(super) async fn unsuspend_user_handler( axum::extract::State(state): axum::extract::State, PlatformAdmin(admin): PlatformAdmin, Path(id): Path, ) -> Result { let user_id = parse_uuid(&id)?; mt_db::mutations::unsuspend_user(&state.db, user_id) .await .map_err(|e| { tracing::error!(error = ?e, "db error unsuspending user"); (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error").into_response() })?; log_mod_action( &state.db, None, admin.user_id, ModAction::UnsuspendUser, Some(user_id), None, None, ).await; Ok(Redirect::to("/_admin?toast=User+unsuspended")) } // ============================================================================ // Dedicated admin view per community: state machine + clean-slate. // ============================================================================ #[tracing::instrument(skip_all)] pub(super) async fn admin_community_detail( axum::extract::State(state): axum::extract::State, session: Session, PlatformAdmin(admin): PlatformAdmin, Path(slug): Path, ) -> Result { let csrf_token = Some(csrf::get_or_create_token(&session).await); let community = get_community(&state.db, &slug).await?; let thread_count: i64 = sqlx::query_scalar( "SELECT COUNT(*) FROM threads t JOIN categories c ON c.id = t.category_id WHERE c.community_id = $1", ) .bind(community.id) .fetch_one(&state.db) .await .map_err(|e| { tracing::error!(error = ?e, "db error counting threads"); StatusCode::INTERNAL_SERVER_ERROR.into_response() })?; let member_count = mt_db::queries::count_community_members(&state.db, community.id) .await .map_err(|e| { tracing::error!(error = ?e, "db error counting members"); StatusCode::INTERNAL_SERVER_ERROR.into_response() })?; let suspension_reason: Option = if community.suspended_at.is_some() { sqlx::query_scalar("SELECT suspension_reason FROM communities WHERE id = $1") .bind(community.id) .fetch_one(&state.db) .await .ok() .flatten() } else { None }; Ok(AdminCommunityTemplate { csrf_token, session_user: Some(template_user(&admin, state.config.platform_admin_id)), mnw_base_url: state.config.mnw_base_url.clone(), community_name: community.name, community_slug: slug, current_state: community.state.as_str(), thread_count, member_count, is_suspended: community.suspended_at.is_some(), suspension_reason, }) } #[tracing::instrument(skip_all)] pub(super) async fn admin_community_clean_slate_handler( axum::extract::State(state): axum::extract::State, PlatformAdmin(admin): PlatformAdmin, Path(slug): Path, Form(form): Form, ) -> Result { let community = get_community(&state.db, &slug).await?; // GitHub-style typed-phrase confirmation: must match the community slug // exactly. Trim only — case is significant. if form.confirm.trim() != slug { return Err(( StatusCode::UNPROCESSABLE_ENTITY, "Confirmation phrase did not match the community slug.", ) .into_response()); } let result = mt_db::mutations::clean_slate_community( &state.db, community.id, admin.user_id, &admin.username, ) .await .map_err(|e| { tracing::error!(error = ?e, "clean-slate failed"); (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error").into_response() })?; log_mod_action( &state.db, Some(community.id), admin.user_id, ModAction::CleanSlateCommunity, None, result.system_thread_id, Some(&format!("deleted {} threads", result.deleted_thread_count)), ) .await; Ok(Redirect::to(&format!( "/_admin/communities/{slug}?toast=Community+reset" ))) }