//! Instance and community settings API endpoints. use axum::{ extract::{Path, State}, routing::{get, patch, post}, Extension, Json, Router, }; use serde::{Deserialize, Serialize}; use serde_json::Value; use sqlx::PgPool; use std::sync::Arc; use uuid::Uuid; use crate::auth::AuthUser; use crate::config::Config; use super::permissions::{require_permission, require_any_permission, perms}; use axum::http::StatusCode; // ============================================================================ // Types // ============================================================================ #[derive(Debug, Serialize)] pub struct SetupStatus { pub setup_required: bool, pub instance_name: Option, } #[derive(Debug, Serialize)] pub struct PublicInstanceSettings { pub setup_completed: bool, pub instance_name: String, pub platform_mode: String, pub registration_enabled: bool, pub registration_mode: String, pub single_community_slug: Option, pub single_community_name: Option, } #[derive(Debug, Serialize)] pub struct InstanceSettings { pub id: Uuid, pub setup_completed: bool, pub instance_name: String, pub platform_mode: String, pub registration_enabled: bool, pub registration_mode: String, pub default_community_visibility: String, pub allow_private_communities: bool, pub default_plugin_policy: String, pub default_moderation_mode: String, } #[derive(Debug, Deserialize)] pub struct SetupRequest { pub instance_name: String, pub platform_mode: String, #[serde(default)] pub single_community_name: Option, } #[derive(Debug, Deserialize)] pub struct UpdateInstanceRequest { #[serde(default)] pub instance_name: Option, #[serde(default)] pub platform_mode: Option, #[serde(default)] pub registration_enabled: Option, #[serde(default)] pub registration_mode: Option, } #[derive(Debug, Serialize)] pub struct CommunitySettings { pub community_id: Uuid, pub membership_mode: String, pub moderation_mode: String, pub governance_model: String, pub plugin_policy: String, pub features_enabled: Value, } #[derive(Debug, Deserialize)] pub struct UpdateCommunitySettingsRequest { #[serde(default)] pub membership_mode: Option, #[serde(default)] pub moderation_mode: Option, #[serde(default)] pub governance_model: Option, #[serde(default)] pub plugin_policy: Option, } // ============================================================================ // Handlers // ============================================================================ /// Check if setup is required (public endpoint) async fn get_setup_status(State(pool): State) -> Result, String> { let row = sqlx::query!( "SELECT setup_completed, instance_name FROM instance_settings LIMIT 1" ) .fetch_optional(&pool) .await .map_err(|e| e.to_string())?; match row { Some(r) => Ok(Json(SetupStatus { setup_required: !r.setup_completed, instance_name: Some(r.instance_name), })), None => Ok(Json(SetupStatus { setup_required: true, instance_name: None, })), } } /// Public instance settings (no auth) async fn get_public_settings(State(pool): State) -> Result, String> { let row = sqlx::query!( r#"SELECT setup_completed, instance_name, platform_mode, registration_enabled, registration_mode, single_community_id FROM instance_settings LIMIT 1"# ) .fetch_optional(&pool) .await .map_err(|e| e.to_string())?; let Some(r) = row else { return Ok(Json(PublicInstanceSettings { setup_completed: false, instance_name: "Likwid".to_string(), platform_mode: "open".to_string(), registration_enabled: true, registration_mode: "open".to_string(), single_community_slug: None, single_community_name: None, })); }; let mut single_community_slug: Option = None; let mut single_community_name: Option = None; if let Some(cid) = r.single_community_id { let comm = sqlx::query!("SELECT slug, name FROM communities WHERE id = $1", cid) .fetch_optional(&pool) .await .map_err(|e| e.to_string())?; if let Some(c) = comm { single_community_slug = Some(c.slug); single_community_name = Some(c.name); } } Ok(Json(PublicInstanceSettings { setup_completed: r.setup_completed, instance_name: r.instance_name, platform_mode: r.platform_mode, registration_enabled: r.registration_enabled, registration_mode: r.registration_mode, single_community_slug, single_community_name, })) } /// Complete initial setup async fn complete_setup( State(pool): State, auth: AuthUser, Json(req): Json, ) -> Result, (StatusCode, String)> { // Check platform admin permission require_permission(&pool, auth.user_id, perms::PLATFORM_ADMIN, None).await?; // Check if already set up let existing = sqlx::query!("SELECT setup_completed FROM instance_settings LIMIT 1") .fetch_optional(&pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; if existing.map(|e| e.setup_completed).unwrap_or(false) { return Err((StatusCode::BAD_REQUEST, "Setup already completed".to_string())); } // Handle single_community mode let single_community_id: Option = if req.platform_mode == "single_community" { let name = req.single_community_name.as_deref().unwrap_or("Main Community"); let community = sqlx::query!( r#"INSERT INTO communities (name, slug, description, is_active, created_by) VALUES ($1, $2, $3, true, $4) RETURNING id"#, name, slug::slugify(name), format!("The {} community", name), auth.user_id ) .fetch_one(&pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; Some(community.id) } else { None }; // Update settings let settings = sqlx::query!( r#"UPDATE instance_settings SET setup_completed = true, setup_completed_at = NOW(), setup_completed_by = $1, instance_name = $2, platform_mode = $3, single_community_id = $4 RETURNING id, setup_completed, instance_name, platform_mode, registration_enabled, registration_mode, default_community_visibility, allow_private_communities, default_plugin_policy, default_moderation_mode"#, auth.user_id, req.instance_name, req.platform_mode, single_community_id ) .fetch_one(&pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; Ok(Json(InstanceSettings { id: settings.id, setup_completed: settings.setup_completed, instance_name: settings.instance_name, platform_mode: settings.platform_mode, registration_enabled: settings.registration_enabled, registration_mode: settings.registration_mode, default_community_visibility: settings.default_community_visibility, allow_private_communities: settings.allow_private_communities, default_plugin_policy: settings.default_plugin_policy, default_moderation_mode: settings.default_moderation_mode, })) } /// Get instance settings (admin only) async fn get_instance_settings( State(pool): State, auth: AuthUser, ) -> Result, (StatusCode, String)> { // Check platform settings permission require_permission(&pool, auth.user_id, perms::PLATFORM_SETTINGS, None).await?; let s = sqlx::query!( r#"SELECT id, setup_completed, instance_name, platform_mode, registration_enabled, registration_mode, default_community_visibility, allow_private_communities, default_plugin_policy, default_moderation_mode FROM instance_settings LIMIT 1"# ) .fetch_one(&pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; Ok(Json(InstanceSettings { id: s.id, setup_completed: s.setup_completed, instance_name: s.instance_name, platform_mode: s.platform_mode, registration_enabled: s.registration_enabled, registration_mode: s.registration_mode, default_community_visibility: s.default_community_visibility, allow_private_communities: s.allow_private_communities, default_plugin_policy: s.default_plugin_policy, default_moderation_mode: s.default_moderation_mode, })) } /// Update instance settings (admin only) async fn update_instance_settings( State(pool): State, auth: AuthUser, Extension(config): Extension>, Json(req): Json, ) -> Result, (StatusCode, String)> { // Check platform settings permission require_permission(&pool, auth.user_id, perms::PLATFORM_SETTINGS, None).await?; if config.is_demo() { return Err(( StatusCode::FORBIDDEN, "Instance settings cannot be modified in demo mode".to_string(), )); } let s = sqlx::query!( r#"UPDATE instance_settings SET instance_name = COALESCE($1, instance_name), platform_mode = COALESCE($2, platform_mode), registration_enabled = COALESCE($3, registration_enabled), registration_mode = COALESCE($4, registration_mode) RETURNING id, setup_completed, instance_name, platform_mode, registration_enabled, registration_mode, default_community_visibility, allow_private_communities, default_plugin_policy, default_moderation_mode"#, req.instance_name, req.platform_mode, req.registration_enabled, req.registration_mode ) .fetch_one(&pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; Ok(Json(InstanceSettings { id: s.id, setup_completed: s.setup_completed, instance_name: s.instance_name, platform_mode: s.platform_mode, registration_enabled: s.registration_enabled, registration_mode: s.registration_mode, default_community_visibility: s.default_community_visibility, allow_private_communities: s.allow_private_communities, default_plugin_policy: s.default_plugin_policy, default_moderation_mode: s.default_moderation_mode, })) } /// Get community settings async fn get_community_settings( State(pool): State, Path(community_id): Path, ) -> Result, (StatusCode, String)> { // Ensure settings exist sqlx::query!( "INSERT INTO community_settings (community_id) VALUES ($1) ON CONFLICT DO NOTHING", community_id ) .execute(&pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; let s = sqlx::query!( r#"SELECT community_id, membership_mode, moderation_mode, governance_model, plugin_policy, features_enabled FROM community_settings WHERE community_id = $1"#, community_id ) .fetch_one(&pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; Ok(Json(CommunitySettings { community_id: s.community_id, membership_mode: s.membership_mode, moderation_mode: s.moderation_mode, governance_model: s.governance_model, plugin_policy: s.plugin_policy, features_enabled: s.features_enabled, })) } /// Update community settings async fn update_community_settings( State(pool): State, auth: AuthUser, Path(community_id): Path, Json(req): Json, ) -> Result, (StatusCode, String)> { // Check community settings permission (community admin or platform admin) require_any_permission( &pool, auth.user_id, &[perms::COMMUNITY_SETTINGS, perms::PLATFORM_ADMIN], Some(community_id), ).await?; // Ensure settings exist sqlx::query!( "INSERT INTO community_settings (community_id) VALUES ($1) ON CONFLICT DO NOTHING", community_id ) .execute(&pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; let s = sqlx::query!( r#"UPDATE community_settings SET membership_mode = COALESCE($2, membership_mode), moderation_mode = COALESCE($3, moderation_mode), governance_model = COALESCE($4, governance_model), plugin_policy = COALESCE($5, plugin_policy) WHERE community_id = $1 RETURNING community_id, membership_mode, moderation_mode, governance_model, plugin_policy, features_enabled"#, community_id, req.membership_mode, req.moderation_mode, req.governance_model, req.plugin_policy ) .fetch_one(&pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; Ok(Json(CommunitySettings { community_id: s.community_id, membership_mode: s.membership_mode, moderation_mode: s.moderation_mode, governance_model: s.governance_model, plugin_policy: s.plugin_policy, features_enabled: s.features_enabled, })) } // ============================================================================ // Router // ============================================================================ pub fn router(pool: PgPool) -> Router { Router::new() .route("/api/settings/public", get(get_public_settings)) .route("/api/settings/setup/status", get(get_setup_status)) .route("/api/settings/setup", post(complete_setup)) .route("/api/settings/instance", get(get_instance_settings)) .route("/api/settings/instance", patch(update_instance_settings)) .route("/api/settings/communities/{community_id}", get(get_community_settings)) .route("/api/settings/communities/{community_id}", patch(update_community_settings)) .with_state(pool) }