From 11afe56d87a2fa587754f2f0190449b890e02112 Mon Sep 17 00:00:00 2001 From: Marco Allegretti Date: Sun, 15 Feb 2026 22:09:54 +0100 Subject: [PATCH] feat(settings): instance defaults applied to new communities --- ...a348d439578815db96969d524ccbb3f83816.json} | 8 +- ...76edee15892248acb6b281ffdbab11a4bff0f.json | 20 --- ...df0e80fcb3769fc4e32c396ac246b038eb6d1.json | 16 ++ ...b9e85aab46b843ce5b58720743463051227fe.json | 16 ++ ...0088e54e843b17af24b6f7381831b179bef40.json | 38 +++++ ...8b83ac7c10efcff1ff52da930c72c71cac614.json | 32 ++++ ...2b4880f40187cb022d94126df9ced0f3f162.json} | 7 +- ...8c611122ae61057c805c82413fb69ed015c58.json | 14 -- ...8d6505fea09a24414d34141691f032e1f8f2.json} | 5 +- ...70cbb980e205b8d7ac4e27409f807680c7034.json | 14 ++ ...60215211500_approve_community_defaults.sql | 57 +++++++ backend/src/api/communities.rs | 155 +++++++++++------- backend/src/api/settings.rs | 119 +++++++++++++- frontend/src/pages/admin/settings.astro | 25 ++- .../pages/communities/[slug]/settings.astro | 8 +- 15 files changed, 418 insertions(+), 116 deletions(-) rename backend/.sqlx/{query-92461256ad7b62764b2bd75674ccbfc11df6648d6d856e3e68fc80801457c555.json => query-277e2cb84c2809b3077e36083e61a348d439578815db96969d524ccbb3f83816.json} (84%) delete mode 100644 backend/.sqlx/query-3d9153f242fa24637d71a4b4f0a76edee15892248acb6b281ffdbab11a4bff0f.json create mode 100644 backend/.sqlx/query-683446d56fc5fbb31800fda950adf0e80fcb3769fc4e32c396ac246b038eb6d1.json create mode 100644 backend/.sqlx/query-6a09254b2c257b9d281177cc012b9e85aab46b843ce5b58720743463051227fe.json create mode 100644 backend/.sqlx/query-6f4895e8ee8fd91b4266d36e0e60088e54e843b17af24b6f7381831b179bef40.json create mode 100644 backend/.sqlx/query-7704c0adfa1399b3f95db1fff2f8b83ac7c10efcff1ff52da930c72c71cac614.json rename backend/.sqlx/{query-a903d88370faa52169ffd4ec6a54a789ee4a6173fe84aca0ef8dedaa46b1f93c.json => query-786d22de6313d554e95069d020362b4880f40187cb022d94126df9ced0f3f162.json} (72%) delete mode 100644 backend/.sqlx/query-957b131c5ae23e306fe4634db068c611122ae61057c805c82413fb69ed015c58.json rename backend/.sqlx/{query-cc97b910b8afcfd348d5fe69f7e75862ddd7e31680e46a61170a467b64cdf547.json => query-ca0137d7aa900603770ccc69ef628d6505fea09a24414d34141691f032e1f8f2.json} (54%) create mode 100644 backend/.sqlx/query-fbd1e5ae4d4bf826df698eed56170cbb980e205b8d7ac4e27409f807680c7034.json create mode 100644 backend/migrations/20260215211500_approve_community_defaults.sql diff --git a/backend/.sqlx/query-92461256ad7b62764b2bd75674ccbfc11df6648d6d856e3e68fc80801457c555.json b/backend/.sqlx/query-277e2cb84c2809b3077e36083e61a348d439578815db96969d524ccbb3f83816.json similarity index 84% rename from backend/.sqlx/query-92461256ad7b62764b2bd75674ccbfc11df6648d6d856e3e68fc80801457c555.json rename to backend/.sqlx/query-277e2cb84c2809b3077e36083e61a348d439578815db96969d524ccbb3f83816.json index 70fb272..c744701 100644 --- a/backend/.sqlx/query-92461256ad7b62764b2bd75674ccbfc11df6648d6d856e3e68fc80801457c555.json +++ b/backend/.sqlx/query-277e2cb84c2809b3077e36083e61a348d439578815db96969d524ccbb3f83816.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n INSERT INTO communities (name, slug, description)\n VALUES ($1, $2, $3)\n RETURNING *\n ", + "query": "\n INSERT INTO communities (name, slug, description, settings, created_by)\n VALUES ($1, $2, $3, $4, $5)\n RETURNING *\n ", "describe": { "columns": [ { @@ -53,7 +53,9 @@ "Left": [ "Varchar", "Varchar", - "Text" + "Text", + "Jsonb", + "Uuid" ] }, "nullable": [ @@ -68,5 +70,5 @@ true ] }, - "hash": "92461256ad7b62764b2bd75674ccbfc11df6648d6d856e3e68fc80801457c555" + "hash": "277e2cb84c2809b3077e36083e61a348d439578815db96969d524ccbb3f83816" } diff --git a/backend/.sqlx/query-3d9153f242fa24637d71a4b4f0a76edee15892248acb6b281ffdbab11a4bff0f.json b/backend/.sqlx/query-3d9153f242fa24637d71a4b4f0a76edee15892248acb6b281ffdbab11a4bff0f.json deleted file mode 100644 index 2a71331..0000000 --- a/backend/.sqlx/query-3d9153f242fa24637d71a4b4f0a76edee15892248acb6b281ffdbab11a4bff0f.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT platform_mode FROM instance_settings LIMIT 1", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "platform_mode", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false - ] - }, - "hash": "3d9153f242fa24637d71a4b4f0a76edee15892248acb6b281ffdbab11a4bff0f" -} diff --git a/backend/.sqlx/query-683446d56fc5fbb31800fda950adf0e80fcb3769fc4e32c396ac246b038eb6d1.json b/backend/.sqlx/query-683446d56fc5fbb31800fda950adf0e80fcb3769fc4e32c396ac246b038eb6d1.json new file mode 100644 index 0000000..31f0aeb --- /dev/null +++ b/backend/.sqlx/query-683446d56fc5fbb31800fda950adf0e80fcb3769fc4e32c396ac246b038eb6d1.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO community_settings (community_id, moderation_mode, plugin_policy)\n VALUES ($1, $2, $3)\n ON CONFLICT DO NOTHING", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Varchar", + "Varchar" + ] + }, + "nullable": [] + }, + "hash": "683446d56fc5fbb31800fda950adf0e80fcb3769fc4e32c396ac246b038eb6d1" +} diff --git a/backend/.sqlx/query-6a09254b2c257b9d281177cc012b9e85aab46b843ce5b58720743463051227fe.json b/backend/.sqlx/query-6a09254b2c257b9d281177cc012b9e85aab46b843ce5b58720743463051227fe.json new file mode 100644 index 0000000..8b79799 --- /dev/null +++ b/backend/.sqlx/query-6a09254b2c257b9d281177cc012b9e85aab46b843ce5b58720743463051227fe.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO community_settings (community_id, moderation_mode, plugin_policy)\n VALUES ($1, $2, $3)\n ON CONFLICT DO NOTHING", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Varchar", + "Varchar" + ] + }, + "nullable": [] + }, + "hash": "6a09254b2c257b9d281177cc012b9e85aab46b843ce5b58720743463051227fe" +} diff --git a/backend/.sqlx/query-6f4895e8ee8fd91b4266d36e0e60088e54e843b17af24b6f7381831b179bef40.json b/backend/.sqlx/query-6f4895e8ee8fd91b4266d36e0e60088e54e843b17af24b6f7381831b179bef40.json new file mode 100644 index 0000000..2917026 --- /dev/null +++ b/backend/.sqlx/query-6f4895e8ee8fd91b4266d36e0e60088e54e843b17af24b6f7381831b179bef40.json @@ -0,0 +1,38 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT platform_mode,\n default_community_visibility,\n default_plugin_policy,\n default_moderation_mode\n FROM instance_settings\n LIMIT 1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "platform_mode", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "default_community_visibility", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "default_plugin_policy", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "default_moderation_mode", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "6f4895e8ee8fd91b4266d36e0e60088e54e843b17af24b6f7381831b179bef40" +} diff --git a/backend/.sqlx/query-7704c0adfa1399b3f95db1fff2f8b83ac7c10efcff1ff52da930c72c71cac614.json b/backend/.sqlx/query-7704c0adfa1399b3f95db1fff2f8b83ac7c10efcff1ff52da930c72c71cac614.json new file mode 100644 index 0000000..1dd1d45 --- /dev/null +++ b/backend/.sqlx/query-7704c0adfa1399b3f95db1fff2f8b83ac7c10efcff1ff52da930c72c71cac614.json @@ -0,0 +1,32 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT default_community_visibility,\n default_plugin_policy,\n default_moderation_mode\n FROM instance_settings\n LIMIT 1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "default_community_visibility", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "default_plugin_policy", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "default_moderation_mode", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "7704c0adfa1399b3f95db1fff2f8b83ac7c10efcff1ff52da930c72c71cac614" +} diff --git a/backend/.sqlx/query-a903d88370faa52169ffd4ec6a54a789ee4a6173fe84aca0ef8dedaa46b1f93c.json b/backend/.sqlx/query-786d22de6313d554e95069d020362b4880f40187cb022d94126df9ced0f3f162.json similarity index 72% rename from backend/.sqlx/query-a903d88370faa52169ffd4ec6a54a789ee4a6173fe84aca0ef8dedaa46b1f93c.json rename to backend/.sqlx/query-786d22de6313d554e95069d020362b4880f40187cb022d94126df9ced0f3f162.json index 2d2df88..0f2237e 100644 --- a/backend/.sqlx/query-a903d88370faa52169ffd4ec6a54a789ee4a6173fe84aca0ef8dedaa46b1f93c.json +++ b/backend/.sqlx/query-786d22de6313d554e95069d020362b4880f40187cb022d94126df9ced0f3f162.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE instance_settings SET\n instance_name = COALESCE($1, instance_name),\n theme_id = COALESCE($2, theme_id),\n platform_mode = COALESCE($3, platform_mode),\n registration_enabled = COALESCE($4, registration_enabled),\n registration_mode = COALESCE($5, registration_mode)\n RETURNING id, setup_completed, instance_name, platform_mode,\n theme_id, registration_enabled, registration_mode,\n default_community_visibility, allow_private_communities,\n default_plugin_policy, default_moderation_mode", + "query": "UPDATE instance_settings SET\n instance_name = COALESCE($1, instance_name),\n theme_id = COALESCE($2, theme_id),\n platform_mode = COALESCE($3, platform_mode),\n registration_enabled = COALESCE($4, registration_enabled),\n registration_mode = COALESCE($5, registration_mode),\n default_community_visibility = COALESCE($6, default_community_visibility),\n default_plugin_policy = COALESCE($7, default_plugin_policy),\n default_moderation_mode = COALESCE($8, default_moderation_mode)\n RETURNING id, setup_completed, instance_name, platform_mode,\n theme_id, registration_enabled, registration_mode,\n default_community_visibility, allow_private_communities,\n default_plugin_policy, default_moderation_mode", "describe": { "columns": [ { @@ -65,6 +65,9 @@ "Varchar", "Varchar", "Bool", + "Varchar", + "Varchar", + "Varchar", "Varchar" ] }, @@ -82,5 +85,5 @@ false ] }, - "hash": "a903d88370faa52169ffd4ec6a54a789ee4a6173fe84aca0ef8dedaa46b1f93c" + "hash": "786d22de6313d554e95069d020362b4880f40187cb022d94126df9ced0f3f162" } diff --git a/backend/.sqlx/query-957b131c5ae23e306fe4634db068c611122ae61057c805c82413fb69ed015c58.json b/backend/.sqlx/query-957b131c5ae23e306fe4634db068c611122ae61057c805c82413fb69ed015c58.json deleted file mode 100644 index 73896ad..0000000 --- a/backend/.sqlx/query-957b131c5ae23e306fe4634db068c611122ae61057c805c82413fb69ed015c58.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO community_settings (community_id) VALUES ($1) ON CONFLICT DO NOTHING", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Uuid" - ] - }, - "nullable": [] - }, - "hash": "957b131c5ae23e306fe4634db068c611122ae61057c805c82413fb69ed015c58" -} diff --git a/backend/.sqlx/query-cc97b910b8afcfd348d5fe69f7e75862ddd7e31680e46a61170a467b64cdf547.json b/backend/.sqlx/query-ca0137d7aa900603770ccc69ef628d6505fea09a24414d34141691f032e1f8f2.json similarity index 54% rename from backend/.sqlx/query-cc97b910b8afcfd348d5fe69f7e75862ddd7e31680e46a61170a467b64cdf547.json rename to backend/.sqlx/query-ca0137d7aa900603770ccc69ef628d6505fea09a24414d34141691f032e1f8f2.json index ccbe8bf..7c47b13 100644 --- a/backend/.sqlx/query-cc97b910b8afcfd348d5fe69f7e75862ddd7e31680e46a61170a467b64cdf547.json +++ b/backend/.sqlx/query-ca0137d7aa900603770ccc69ef628d6505fea09a24414d34141691f032e1f8f2.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO communities (name, slug, description, is_active, created_by)\n VALUES ($1, $2, $3, true, $4)\n RETURNING id", + "query": "INSERT INTO communities (name, slug, description, settings, is_active, created_by)\n VALUES ($1, $2, $3, $4, true, $5)\n RETURNING id", "describe": { "columns": [ { @@ -14,6 +14,7 @@ "Varchar", "Varchar", "Text", + "Jsonb", "Uuid" ] }, @@ -21,5 +22,5 @@ false ] }, - "hash": "cc97b910b8afcfd348d5fe69f7e75862ddd7e31680e46a61170a467b64cdf547" + "hash": "ca0137d7aa900603770ccc69ef628d6505fea09a24414d34141691f032e1f8f2" } diff --git a/backend/.sqlx/query-fbd1e5ae4d4bf826df698eed56170cbb980e205b8d7ac4e27409f807680c7034.json b/backend/.sqlx/query-fbd1e5ae4d4bf826df698eed56170cbb980e205b8d7ac4e27409f807680c7034.json new file mode 100644 index 0000000..ac40c7d --- /dev/null +++ b/backend/.sqlx/query-fbd1e5ae4d4bf826df698eed56170cbb980e205b8d7ac4e27409f807680c7034.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO community_settings (community_id, moderation_mode, plugin_policy)\n SELECT $1, default_moderation_mode, default_plugin_policy\n FROM instance_settings\n LIMIT 1\n ON CONFLICT DO NOTHING", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "fbd1e5ae4d4bf826df698eed56170cbb980e205b8d7ac4e27409f807680c7034" +} diff --git a/backend/migrations/20260215211500_approve_community_defaults.sql b/backend/migrations/20260215211500_approve_community_defaults.sql new file mode 100644 index 0000000..16d911c --- /dev/null +++ b/backend/migrations/20260215211500_approve_community_defaults.sql @@ -0,0 +1,57 @@ +-- Ensure approved communities inherit instance defaults +-- Applies instance default visibility and initializes community_settings with instance defaults + +CREATE OR REPLACE FUNCTION approve_community( + p_pending_id UUID, + p_reviewer_id UUID +) RETURNS UUID AS $$ +DECLARE + v_pending pending_communities%ROWTYPE; + v_community_id UUID; + v_default_visibility VARCHAR(20); + v_default_plugin_policy VARCHAR(20); + v_default_moderation_mode VARCHAR(20); +BEGIN + -- Get pending community + SELECT * INTO v_pending FROM pending_communities WHERE id = p_pending_id AND status = 'pending'; + IF NOT FOUND THEN + RAISE EXCEPTION 'Pending community not found or already processed'; + END IF; + + SELECT default_community_visibility, + default_plugin_policy, + default_moderation_mode + INTO v_default_visibility, + v_default_plugin_policy, + v_default_moderation_mode + FROM instance_settings + LIMIT 1; + + -- Create the community + INSERT INTO communities (name, slug, description, settings, created_by, is_active) + VALUES ( + v_pending.name, + v_pending.slug, + v_pending.description, + jsonb_build_object('visibility', v_default_visibility), + v_pending.requested_by, + true + ) + RETURNING id INTO v_community_id; + + -- Add requester as admin + INSERT INTO community_members (community_id, user_id, role) + VALUES (v_community_id, v_pending.requested_by, 'admin'); + + INSERT INTO community_settings (community_id, moderation_mode, plugin_policy) + VALUES (v_community_id, v_default_moderation_mode, v_default_plugin_policy) + ON CONFLICT DO NOTHING; + + -- Mark as approved + UPDATE pending_communities + SET status = 'approved', reviewed_by = p_reviewer_id, reviewed_at = NOW() + WHERE id = p_pending_id; + + RETURN v_community_id; +END; +$$ LANGUAGE plpgsql; diff --git a/backend/src/api/communities.rs b/backend/src/api/communities.rs index b95b49d..c1a1a9b 100644 --- a/backend/src/api/communities.rs +++ b/backend/src/api/communities.rs @@ -6,6 +6,7 @@ use axum::{ }; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; +use serde_json::json; use sqlx::PgPool; use std::sync::Arc; use uuid::Uuid; @@ -75,78 +76,106 @@ async fn create_community( Json(req): Json, ) -> Result, (StatusCode, String)> { // Check platform mode for community creation permissions - let settings = sqlx::query!("SELECT platform_mode FROM instance_settings LIMIT 1") - .fetch_optional(&pool) - .await - .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; + let settings = sqlx::query!( + r#"SELECT platform_mode, + default_community_visibility, + default_plugin_policy, + default_moderation_mode + FROM instance_settings + LIMIT 1"# + ) + .fetch_optional(&pool) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; - if let Some(s) = settings { - match s.platform_mode.as_str() { - "single_community" => { + let (platform_mode, default_visibility, default_plugin_policy, default_moderation_mode) = + if let Some(s) = settings { + ( + s.platform_mode, + s.default_community_visibility, + s.default_plugin_policy, + s.default_moderation_mode, + ) + } else { + ( + "open".to_string(), + "public".to_string(), + "curated".to_string(), + "standard".to_string(), + ) + }; + + match platform_mode.as_str() { + "single_community" => { + return Err(( + StatusCode::FORBIDDEN, + "This platform is dedicated to a single community".to_string(), + )); + } + "admin_only" => { + // Check platform admin or community create permission + let can_create = + user_has_permission(&pool, auth.user_id, perms::COMMUNITY_CREATE, None).await?; + if !can_create { return Err(( StatusCode::FORBIDDEN, - "This platform is dedicated to a single community".to_string(), + "Only administrators can create communities".to_string(), )); } - "admin_only" => { - // Check platform admin or community create permission - let can_create = - user_has_permission(&pool, auth.user_id, perms::COMMUNITY_CREATE, None).await?; - if !can_create { - return Err(( - StatusCode::FORBIDDEN, - "Only administrators can create communities".to_string(), - )); - } - } - "approval" => { - // Check if user has direct create permission (admins bypass approval) - let can_create = - user_has_permission(&pool, auth.user_id, perms::COMMUNITY_CREATE, None).await?; - if !can_create { - // Create pending community request instead - sqlx::query!( - r#"INSERT INTO pending_communities (name, slug, description, requested_by) - VALUES ($1, $2, $3, $4)"#, - req.name, - req.slug, - req.description, - auth.user_id - ) - .execute(&pool) - .await - .map_err(|e| { - if e.to_string().contains("duplicate key") { - ( - StatusCode::CONFLICT, - "A community with this slug already exists or is pending approval" - .to_string(), - ) - } else { - (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()) - } - })?; - - return Err(( - StatusCode::ACCEPTED, - "Community request submitted for approval".to_string(), - )); - } - } - _ => {} // "open" mode - anyone can create } + "approval" => { + // Check if user has direct create permission (admins bypass approval) + let can_create = + user_has_permission(&pool, auth.user_id, perms::COMMUNITY_CREATE, None).await?; + if !can_create { + // Create pending community request instead + sqlx::query!( + r#"INSERT INTO pending_communities (name, slug, description, requested_by) + VALUES ($1, $2, $3, $4)"#, + req.name, + req.slug, + req.description, + auth.user_id + ) + .execute(&pool) + .await + .map_err(|e| { + if e.to_string().contains("duplicate key") { + ( + StatusCode::CONFLICT, + "A community with this slug already exists or is pending approval" + .to_string(), + ) + } else { + (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()) + } + })?; + + return Err(( + StatusCode::ACCEPTED, + "Community request submitted for approval".to_string(), + )); + } + } + _ => {} // "open" mode - anyone can create } + let community_settings = json!({ + "visibility": default_visibility + }); + let community = sqlx::query_as!( crate::models::Community, r#" - INSERT INTO communities (name, slug, description) - VALUES ($1, $2, $3) + INSERT INTO communities (name, slug, description, settings, created_by) + VALUES ($1, $2, $3, $4, $5) RETURNING * "#, req.name, req.slug, - req.description + req.description, + community_settings, + auth.user_id ) .fetch_one(&pool) .await @@ -171,6 +200,18 @@ async fn create_community( .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; + sqlx::query!( + r#"INSERT INTO community_settings (community_id, moderation_mode, plugin_policy) + VALUES ($1, $2, $3) + ON CONFLICT DO NOTHING"#, + community.id, + default_moderation_mode, + default_plugin_policy + ) + .execute(&pool) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; + plugins .ensure_default_community_plugins(community.id, Some(auth.user_id)) .await diff --git a/backend/src/api/settings.rs b/backend/src/api/settings.rs index ce8bbd9..f76f6a9 100644 --- a/backend/src/api/settings.rs +++ b/backend/src/api/settings.rs @@ -73,10 +73,20 @@ pub struct UpdateInstanceRequest { pub registration_enabled: Option, #[serde(default)] pub registration_mode: Option, + #[serde(default)] + pub default_community_visibility: Option, + #[serde(default)] + pub default_plugin_policy: Option, + #[serde(default)] + pub default_moderation_mode: Option, } const KNOWN_THEME_IDS: [&str; 4] = ["neutral", "breeze-light", "breeze-dark", "opensuse"]; +const KNOWN_COMMUNITY_VISIBILITIES: [&str; 3] = ["public", "unlisted", "private"]; +const KNOWN_PLUGIN_POLICIES: [&str; 3] = ["permissive", "curated", "strict"]; +const KNOWN_MODERATION_MODES: [&str; 4] = ["minimal", "standard", "strict", "custom"]; + fn validate_theme_id(theme_id: &str) -> Result<(), (StatusCode, String)> { if theme_id.trim().is_empty() { return Err((StatusCode::BAD_REQUEST, "Theme cannot be empty".to_string())); @@ -101,6 +111,24 @@ fn validate_theme_id(theme_id: &str) -> Result<(), (StatusCode, String)> { Ok(()) } +fn validate_known_value( + value: &str, + allowed: &[&str], + field_name: &'static str, +) -> Result<(), (StatusCode, String)> { + if value.trim().is_empty() { + return Err(( + StatusCode::BAD_REQUEST, + format!("{} cannot be empty", field_name), + )); + } + if !allowed.iter().any(|v| v == &value) { + return Err((StatusCode::BAD_REQUEST, format!("Invalid {}", field_name))); + } + + Ok(()) +} + #[derive(Debug, Serialize)] pub struct CommunitySettings { pub community_id: Uuid, @@ -223,24 +251,66 @@ async fn complete_setup( )); } + let defaults = sqlx::query!( + r#"SELECT default_community_visibility, + default_plugin_policy, + default_moderation_mode + FROM instance_settings + LIMIT 1"# + ) + .fetch_optional(&pool) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; + + let (default_visibility, default_plugin_policy, default_moderation_mode) = + if let Some(d) = defaults { + ( + d.default_community_visibility, + d.default_plugin_policy, + d.default_moderation_mode, + ) + } else { + ( + "public".to_string(), + "curated".to_string(), + "standard".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_settings = serde_json::json!({ + "visibility": default_visibility + }); let community = sqlx::query!( - r#"INSERT INTO communities (name, slug, description, is_active, created_by) - VALUES ($1, $2, $3, true, $4) + r#"INSERT INTO communities (name, slug, description, settings, is_active, created_by) + VALUES ($1, $2, $3, $4, true, $5) RETURNING id"#, name, slug::slugify(name), format!("The {} community", name), + community_settings, auth.user_id ) .fetch_one(&pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; + + sqlx::query!( + r#"INSERT INTO community_settings (community_id, moderation_mode, plugin_policy) + VALUES ($1, $2, $3) + ON CONFLICT DO NOTHING"#, + community.id, + default_moderation_mode, + default_plugin_policy + ) + .execute(&pool) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; Some(community.id) } else { None @@ -332,8 +402,27 @@ async fn update_instance_settings( validate_theme_id(theme_id)?; } + if let Some(v) = req.default_community_visibility.as_deref() { + validate_known_value( + v, + &KNOWN_COMMUNITY_VISIBILITIES, + "default community visibility", + )?; + } + + if let Some(v) = req.default_plugin_policy.as_deref() { + validate_known_value(v, &KNOWN_PLUGIN_POLICIES, "default plugin policy")?; + } + + if let Some(v) = req.default_moderation_mode.as_deref() { + validate_known_value(v, &KNOWN_MODERATION_MODES, "default moderation mode")?; + } + if config.is_demo() { - let allowed = req.theme_id.is_some() + let allowed = (req.theme_id.is_some() + || req.default_community_visibility.is_some() + || req.default_plugin_policy.is_some() + || req.default_moderation_mode.is_some()) && req.instance_name.is_none() && req.platform_mode.is_none() && req.registration_enabled.is_none() @@ -341,7 +430,7 @@ async fn update_instance_settings( if !allowed { return Err(( StatusCode::FORBIDDEN, - "Only theme updates are allowed in demo mode".to_string(), + "Only theme/defaults updates are allowed in demo mode".to_string(), )); } } @@ -352,7 +441,10 @@ async fn update_instance_settings( theme_id = COALESCE($2, theme_id), platform_mode = COALESCE($3, platform_mode), registration_enabled = COALESCE($4, registration_enabled), - registration_mode = COALESCE($5, registration_mode) + registration_mode = COALESCE($5, registration_mode), + default_community_visibility = COALESCE($6, default_community_visibility), + default_plugin_policy = COALESCE($7, default_plugin_policy), + default_moderation_mode = COALESCE($8, default_moderation_mode) RETURNING id, setup_completed, instance_name, platform_mode, theme_id, registration_enabled, registration_mode, default_community_visibility, allow_private_communities, @@ -361,7 +453,10 @@ async fn update_instance_settings( req.theme_id, req.platform_mode, req.registration_enabled, - req.registration_mode + req.registration_mode, + req.default_community_visibility, + req.default_plugin_policy, + req.default_moderation_mode ) .fetch_one(&pool) .await @@ -389,7 +484,11 @@ async fn get_community_settings( ) -> Result, (StatusCode, String)> { // Ensure settings exist sqlx::query!( - "INSERT INTO community_settings (community_id) VALUES ($1) ON CONFLICT DO NOTHING", + r#"INSERT INTO community_settings (community_id, moderation_mode, plugin_policy) + SELECT $1, default_moderation_mode, default_plugin_policy + FROM instance_settings + LIMIT 1 + ON CONFLICT DO NOTHING"#, community_id ) .execute(&pool) @@ -434,7 +533,11 @@ async fn update_community_settings( // Ensure settings exist sqlx::query!( - "INSERT INTO community_settings (community_id) VALUES ($1) ON CONFLICT DO NOTHING", + r#"INSERT INTO community_settings (community_id, moderation_mode, plugin_policy) + SELECT $1, default_moderation_mode, default_plugin_policy + FROM instance_settings + LIMIT 1 + ON CONFLICT DO NOTHING"#, community_id ) .execute(&pool) diff --git a/frontend/src/pages/admin/settings.astro b/frontend/src/pages/admin/settings.astro index 00ddf31..0f1af95 100644 --- a/frontend/src/pages/admin/settings.astro +++ b/frontend/src/pages/admin/settings.astro @@ -83,7 +83,7 @@ import { API_BASE as apiBase } from '../../lib/api';
- @@ -92,7 +92,7 @@ import { API_BASE as apiBase } from '../../lib/api';
- @@ -101,10 +101,11 @@ import { API_BASE as apiBase } from '../../lib/api';
- + + + +
@@ -262,6 +263,9 @@ import { API_BASE as apiBase } from '../../lib/api'; platform_mode: (document.getElementById('platform_mode')).value, registration_enabled: (document.getElementById('registration_enabled')).checked, registration_mode: (document.getElementById('registration_mode')).value, + default_community_visibility: (document.getElementById('default_community_visibility')).value, + default_plugin_policy: (document.getElementById('default_plugin_policy')).value, + default_moderation_mode: (document.getElementById('default_moderation_mode')).value, }; const data = {}; @@ -280,6 +284,15 @@ import { API_BASE as apiBase } from '../../lib/api'; if (!initialSettings || current.registration_mode !== initialSettings.registration_mode) { data.registration_mode = current.registration_mode; } + if (!initialSettings || current.default_community_visibility !== initialSettings.default_community_visibility) { + data.default_community_visibility = current.default_community_visibility; + } + if (!initialSettings || current.default_plugin_policy !== initialSettings.default_plugin_policy) { + data.default_plugin_policy = current.default_plugin_policy; + } + if (!initialSettings || current.default_moderation_mode !== initialSettings.default_moderation_mode) { + data.default_moderation_mode = current.default_moderation_mode; + } if (Object.keys(data).length === 0) { saveStatus.textContent = 'No changes to save.'; diff --git a/frontend/src/pages/communities/[slug]/settings.astro b/frontend/src/pages/communities/[slug]/settings.astro index feae364..6f93420 100644 --- a/frontend/src/pages/communities/[slug]/settings.astro +++ b/frontend/src/pages/communities/[slug]/settings.astro @@ -43,10 +43,10 @@ const { slug } = Astro.params;