2026-01-27 16:21:58 +00:00
|
|
|
//! Instance and community settings API endpoints.
|
|
|
|
|
|
|
|
|
|
use axum::{
|
|
|
|
|
extract::{Path, State},
|
|
|
|
|
routing::{get, patch, post},
|
2026-01-28 23:46:43 +00:00
|
|
|
Extension,
|
2026-01-27 16:21:58 +00:00
|
|
|
Json, Router,
|
|
|
|
|
};
|
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
use serde_json::Value;
|
|
|
|
|
use sqlx::PgPool;
|
2026-01-28 23:46:43 +00:00
|
|
|
use std::sync::Arc;
|
2026-01-27 16:21:58 +00:00
|
|
|
use uuid::Uuid;
|
|
|
|
|
|
|
|
|
|
use crate::auth::AuthUser;
|
2026-01-28 23:46:43 +00:00
|
|
|
use crate::config::Config;
|
2026-01-27 16:21:58 +00:00
|
|
|
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>,
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-28 23:46:43 +00:00
|
|
|
#[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<String>,
|
|
|
|
|
pub single_community_name: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-27 16:21:58 +00:00
|
|
|
#[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,
|
|
|
|
|
})),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-28 23:46:43 +00:00
|
|
|
/// Public instance settings (no auth)
|
|
|
|
|
async fn get_public_settings(State(pool): State<PgPool>) -> Result<Json<PublicInstanceSettings>, 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<String> = None;
|
|
|
|
|
let mut single_community_name: Option<String> = 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,
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-27 16:21:58 +00:00
|
|
|
/// 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,
|
2026-01-28 23:46:43 +00:00
|
|
|
Extension(config): Extension<Arc<Config>>,
|
2026-01-27 16:21:58 +00:00
|
|
|
Json(req): Json<UpdateInstanceRequest>,
|
|
|
|
|
) -> Result<Json<InstanceSettings>, (StatusCode, String)> {
|
|
|
|
|
// Check platform settings permission
|
|
|
|
|
require_permission(&pool, auth.user_id, perms::PLATFORM_SETTINGS, None).await?;
|
|
|
|
|
|
2026-01-28 23:46:43 +00:00
|
|
|
if config.is_demo() {
|
|
|
|
|
return Err((
|
|
|
|
|
StatusCode::FORBIDDEN,
|
|
|
|
|
"Instance settings cannot be modified in demo mode".to_string(),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-27 16:21:58 +00:00
|
|
|
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()
|
2026-01-28 23:46:43 +00:00
|
|
|
.route("/api/settings/public", get(get_public_settings))
|
2026-01-27 16:21:58 +00:00
|
|
|
.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)
|
|
|
|
|
}
|