likwid/backend/src/api/demo.rs

153 lines
4.7 KiB
Rust

//! 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<Config>,
}
/// Get demo mode status and available accounts
async fn get_demo_status(State(state): State<DemoState>) -> 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::<Vec<_>>()
} else {
vec![]
},
"restrictions": if state.config.is_demo() {
vec![
"Cannot delete communities",
"Cannot delete users",
"Cannot modify instance settings (except instance theme)",
"Data resets periodically"
]
} else {
vec![]
}
}))
}
/// Reset demo data to initial state (only in demo mode)
async fn reset_demo(State(state): State<DemoState>, 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<DemoState>) -> 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<Config>) -> 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)
}