//! Demo API endpoints use axum::{ extract::State, http::StatusCode, response::IntoResponse, routing::{get, post}, Json, Router, }; use serde_json::json; use sqlx::PgPool; use std::sync::Arc; use super::permissions::{perms, require_permission}; use crate::auth::AuthUser; use crate::config::Config; use crate::demo::{self, DemoResetError, DEMO_ACCOUNTS}; /// Combined state for demo endpoints #[derive(Clone)] pub struct DemoState { pub pool: PgPool, pub config: Arc, } /// Get demo mode status and available accounts async fn get_demo_status(State(state): State) -> impl IntoResponse { Json(json!({ "demo_mode": state.config.is_demo(), "accounts": if state.config.is_demo() { DEMO_ACCOUNTS.iter().map(|(u, _, d)| json!({ "username": u, "display_name": d, "password": "demo123" })).collect::>() } else { vec![] }, "restrictions": if state.config.is_demo() { vec![ "Cannot delete communities", "Cannot delete users", "Cannot modify instance settings", "Data resets periodically" ] } else { vec![] } })) } /// Reset demo data to initial state (only in demo mode) async fn reset_demo(State(state): State, auth: AuthUser) -> impl IntoResponse { if !state.config.is_demo() { return ( StatusCode::FORBIDDEN, Json(json!({"error": "Demo mode not enabled"})), ) .into_response(); } if let Err((status, msg)) = require_permission(&state.pool, auth.user_id, perms::PLATFORM_ADMIN, None).await { return (status, Json(json!({"error": msg}))).into_response(); } match demo::reset_demo_data(&state.pool).await { Ok(_) => ( StatusCode::OK, Json(json!({"success": true, "message": "Demo data has been reset to initial state"})), ) .into_response(), Err(DemoResetError::Busy) => ( StatusCode::CONFLICT, Json(json!({"error": "Demo reset already in progress"})), ) .into_response(), Err(DemoResetError::LockTimeout) => ( StatusCode::GATEWAY_TIMEOUT, Json(json!({"error": "Timed out while acquiring demo reset lock"})), ) .into_response(), Err(DemoResetError::Sql(e)) => { tracing::error!("Failed to reset demo data: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "Failed to reset demo data"})), ) .into_response() } } } /// Get demo communities summary async fn get_demo_communities(State(state): State) -> impl IntoResponse { if !state.config.is_demo() { return (StatusCode::OK, Json(json!({"communities": []}))).into_response(); } let communities = sqlx::query_as::<_, (String, String, String, i64, i64)>( r#" SELECT c.name, c.slug, COALESCE(c.description, '') as description, (SELECT COUNT(*) FROM community_members WHERE community_id = c.id) as member_count, (SELECT COUNT(*) FROM proposals WHERE community_id = c.id) as proposal_count FROM communities c WHERE c.slug IN ('aurora', 'civic-commons', 'makers') ORDER BY c.name "#, ) .fetch_all(&state.pool) .await; match communities { Ok(rows) => { let communities: Vec<_> = rows .iter() .map(|(name, slug, desc, members, proposals)| { json!({ "name": name, "slug": slug, "description": desc, "member_count": members, "proposal_count": proposals }) }) .collect(); (StatusCode::OK, Json(json!({"communities": communities}))).into_response() } Err(e) => { tracing::error!("Failed to fetch demo communities: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "Failed to fetch communities"})), ) .into_response() } } } pub fn router(pool: PgPool, config: Arc) -> Router { let state = DemoState { pool, config }; Router::new() .route("/api/demo/status", get(get_demo_status)) .route("/api/demo/reset", post(reset_demo)) .route("/api/demo/communities", get(get_demo_communities)) .with_state(state) }