From e94520f9f72bf5e56622a19fe8bc56456e70b0d3 Mon Sep 17 00:00:00 2001 From: Marco Allegretti Date: Sun, 15 Feb 2026 22:43:29 +0100 Subject: [PATCH] feat(setup): improve bootstrap reliability --- ...8ccbf26c373207b8fb76bf738fd5bfd98f861.json | 15 +++ backend/src/api/settings.rs | 39 ++++++++ docs/admin/installation.md | 16 +++ frontend/src/pages/setup.astro | 97 +++++++++++++++++-- 4 files changed, 160 insertions(+), 7 deletions(-) create mode 100644 backend/.sqlx/query-a1d971610cd2f33b2e2f47b5ee38ccbf26c373207b8fb76bf738fd5bfd98f861.json diff --git a/backend/.sqlx/query-a1d971610cd2f33b2e2f47b5ee38ccbf26c373207b8fb76bf738fd5bfd98f861.json b/backend/.sqlx/query-a1d971610cd2f33b2e2f47b5ee38ccbf26c373207b8fb76bf738fd5bfd98f861.json new file mode 100644 index 0000000..1f9c3f8 --- /dev/null +++ b/backend/.sqlx/query-a1d971610cd2f33b2e2f47b5ee38ccbf26c373207b8fb76bf738fd5bfd98f861.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO community_members (user_id, community_id, role) VALUES ($1, $2, 'admin') ON CONFLICT DO NOTHING", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "a1d971610cd2f33b2e2f47b5ee38ccbf26c373207b8fb76bf738fd5bfd98f861" +} diff --git a/backend/src/api/settings.rs b/backend/src/api/settings.rs index f76f6a9..32f38d0 100644 --- a/backend/src/api/settings.rs +++ b/backend/src/api/settings.rs @@ -87,6 +87,8 @@ 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"]; +const KNOWN_PLATFORM_MODES: [&str; 4] = ["open", "approval", "admin_only", "single_community"]; + 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())); @@ -238,6 +240,34 @@ async fn complete_setup( // Check platform admin permission require_permission(&pool, auth.user_id, perms::PLATFORM_ADMIN, None).await?; + if req.instance_name.trim().is_empty() { + return Err(( + StatusCode::BAD_REQUEST, + "Platform name cannot be empty".to_string(), + )); + } + if req.instance_name.len() > 100 { + return Err(( + StatusCode::BAD_REQUEST, + "Platform name is too long".to_string(), + )); + } + if !KNOWN_PLATFORM_MODES + .iter() + .any(|m| m == &req.platform_mode.as_str()) + { + return Err((StatusCode::BAD_REQUEST, "Invalid platform mode".to_string())); + } + if req.platform_mode == "single_community" { + let name = req.single_community_name.as_deref().unwrap_or(""); + if name.trim().is_empty() { + return Err(( + StatusCode::BAD_REQUEST, + "Community name is required in single community mode".to_string(), + )); + } + } + // Check if already set up let existing = sqlx::query!("SELECT setup_completed FROM instance_settings LIMIT 1") .fetch_optional(&pool) @@ -300,6 +330,15 @@ async fn complete_setup( .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; + sqlx::query!( + "INSERT INTO community_members (user_id, community_id, role) VALUES ($1, $2, 'admin') ON CONFLICT DO NOTHING", + auth.user_id, + community.id + ) + .execute(&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) diff --git a/docs/admin/installation.md b/docs/admin/installation.md index b6b0891..80aa7e9 100644 --- a/docs/admin/installation.md +++ b/docs/admin/installation.md @@ -126,6 +126,22 @@ server { ## Next Steps +## First Admin Bootstrap (no manual DB edits) + +After the containers are running, you must create the first platform admin and complete the web setup flow. + +1. Open the site in your browser. +2. Register the **first** user account at `/register`. + - The first registered user is automatically granted platform admin permissions. +3. Visit `/setup`. +4. Complete instance setup: + - Set the platform name. + - Choose a platform mode. + - If using **Single Community** mode, provide the community name. +5. After setup completes: + - Configure instance settings at `/admin/settings`. + - Create or browse communities at `/communities`. + - [Configuration](configuration.md) - Detailed settings - [Database](database.md) - Database management - [Security](security.md) - Hardening your instance diff --git a/frontend/src/pages/setup.astro b/frontend/src/pages/setup.astro index 20a27fb..f27893b 100644 --- a/frontend/src/pages/setup.astro +++ b/frontend/src/pages/setup.astro @@ -28,13 +28,15 @@ if (!setupRequired) {
-
+

Welcome to Likwid

Let's configure your governance platform

+ +

Instance Identity

@@ -151,6 +153,26 @@ if (!setupRequired) { padding: 2rem; } + .setup-banner { + padding: 1rem; + border-radius: 10px; + margin-bottom: 1.25rem; + border: 1px solid var(--color-border); + background: var(--color-bg-alt); + } + + .setup-banner.error { + border-color: color-mix(in srgb, var(--color-error) 40%, var(--color-border)); + background: var(--color-error-muted); + color: var(--color-error); + } + + .setup-banner.success { + border-color: color-mix(in srgb, var(--color-success) 40%, var(--color-border)); + background: var(--color-success-muted); + color: var(--color-success); + } + .setup-section { margin-bottom: 2rem; padding-bottom: 2rem; @@ -247,9 +269,43 @@ if (!setupRequired) { const form = document.getElementById('setup-form'); const submitBtn = document.getElementById('submit-btn'); const authStatus = document.getElementById('auth-status'); + const setupError = document.getElementById('setup-error'); + const setupSuccess = document.getElementById('setup-success'); const singleCommunityName = document.getElementById('single-community-name'); const platformModeInputs = document.querySelectorAll('input[name="platform_mode"]'); + const initialInstanceName = (document.getElementById('instance_name')); + if (initialInstanceName && !initialInstanceName.value) { + const setupCard = document.querySelector('.setup-card'); + const fromServer = setupCard ? setupCard.dataset.setupInstanceName : ''; + if (fromServer) { + try { + localStorage.setItem('setup-instance-name', fromServer); + } catch (e) {} + initialInstanceName.value = fromServer; + } + const fromStatus = localStorage.getItem('setup-instance-name'); + if (!initialInstanceName.value && fromStatus) initialInstanceName.value = fromStatus; + } + + function showError(html) { + if (!setupError) return; + setupError.innerHTML = html; + setupError.style.display = 'block'; + } + + function clearError() { + if (!setupError) return; + setupError.style.display = 'none'; + setupError.innerHTML = ''; + } + + function showSuccess(html) { + if (!setupSuccess) return; + setupSuccess.innerHTML = html; + setupSuccess.style.display = 'block'; + } + // Show/hide single community name field platformModeInputs.forEach(input => { input.addEventListener('change', (e) => { @@ -267,6 +323,9 @@ if (!setupRequired) {

You need to log in first. Go to login

If you haven't registered yet, register first (first user becomes admin).

`; + showError( + '

Not signed in. Create the first user account, then return to this page to complete setup.

' + ); return false; } @@ -289,11 +348,15 @@ if (!setupRequired) { if (adminRes.status === 403) { authStatus.className = 'auth-status error'; authStatus.innerHTML = `

You are logged in as ${user.username}, but you need admin privileges.

`; + showError( + '

Admin privileges required. The first registered user becomes platform admin. If you are not the first user, ask an operator to grant you platform admin.

' + ); return false; } authStatus.className = 'auth-status success'; authStatus.innerHTML = `

Logged in as ${user.username} (admin)

`; + clearError(); submitBtn.disabled = false; return true; } catch (e) { @@ -301,6 +364,7 @@ if (!setupRequired) { authStatus.innerHTML = `

Session expired. Please log in again

`; + showError('

Session expired. Please log in again and retry setup.

'); return false; } } @@ -310,10 +374,11 @@ if (!setupRequired) { // Form submission form.addEventListener('submit', async (e) => { e.preventDefault(); + clearError(); const token = localStorage.getItem('token'); if (!token) { - alert('Please log in first'); + showError('

Please log in first. Go to login

'); return; } @@ -338,14 +403,32 @@ if (!setupRequired) { }); if (!res.ok) { - const error = await res.text(); - throw new Error(error); + const errorText = await res.text(); + if (res.status === 401) { + showError('

Not authorized. Your session expired. Log in again.

'); + return; + } + if (res.status === 403) { + showError('

Forbidden. You must be a platform admin to complete setup.

'); + return; + } + showError( + `

Setup failed.

${(errorText || 'Unknown error')}

Fix the issue and try again.

` + ); + return; } - // Success - redirect to home - window.location.href = '/'; + showSuccess( + '

Setup complete.

' + + '

Next steps:

' + + '

Go to Admin Settings

' + + '

Browse or create communities

' + ); + window.likwidUi?.toast?.('success', 'Setup complete'); + submitBtn.textContent = 'Setup Complete'; } catch (err) { - alert('Setup failed: ' + err.message); + showError(`

Setup failed.

${err && err.message ? err.message : 'Unknown error'}

`); + window.likwidUi?.toast?.('error', 'Setup failed'); submitBtn.disabled = false; submitBtn.textContent = 'Complete Setup'; }