likwid/backend/src/api/settings.rs

359 lines
12 KiB
Rust
Raw Normal View History

//! Instance and community settings API endpoints.
use axum::{
extract::{Path, State},
routing::{get, patch, post},
Json, Router,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use sqlx::PgPool;
use uuid::Uuid;
use crate::auth::AuthUser;
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<String>,
}
#[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<String>,
}
#[derive(Debug, Deserialize)]
pub struct UpdateInstanceRequest {
#[serde(default)]
pub instance_name: Option<String>,
#[serde(default)]
pub platform_mode: Option<String>,
#[serde(default)]
pub registration_enabled: Option<bool>,
#[serde(default)]
pub registration_mode: Option<String>,
}
#[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<String>,
#[serde(default)]
pub moderation_mode: Option<String>,
#[serde(default)]
pub governance_model: Option<String>,
#[serde(default)]
pub plugin_policy: Option<String>,
}
// ============================================================================
// Handlers
// ============================================================================
/// Check if setup is required (public endpoint)
async fn get_setup_status(State(pool): State<PgPool>) -> Result<Json<SetupStatus>, 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,
})),
}
}
/// Complete initial setup
async fn complete_setup(
State(pool): State<PgPool>,
auth: AuthUser,
Json(req): Json<SetupRequest>,
) -> Result<Json<InstanceSettings>, (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<Uuid> = 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<PgPool>,
auth: AuthUser,
) -> Result<Json<InstanceSettings>, (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<PgPool>,
auth: AuthUser,
Json(req): Json<UpdateInstanceRequest>,
) -> Result<Json<InstanceSettings>, (StatusCode, String)> {
// Check platform settings permission
require_permission(&pool, auth.user_id, perms::PLATFORM_SETTINGS, None).await?;
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<PgPool>,
Path(community_id): Path<Uuid>,
) -> Result<Json<CommunitySettings>, (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<PgPool>,
auth: AuthUser,
Path(community_id): Path<Uuid>,
Json(req): Json<UpdateCommunitySettingsRequest>,
) -> Result<Json<CommunitySettings>, (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/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)
}