mirror of
https://codeberg.org/likwid/likwid.git
synced 2026-03-26 19:03:08 +00:00
feat(setup): improve bootstrap reliability
This commit is contained in:
parent
1d3780d7fd
commit
e94520f9f7
4 changed files with 160 additions and 7 deletions
|
|
@ -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"
|
||||||
|
}
|
||||||
|
|
@ -87,6 +87,8 @@ const KNOWN_COMMUNITY_VISIBILITIES: [&str; 3] = ["public", "unlisted", "private"
|
||||||
const KNOWN_PLUGIN_POLICIES: [&str; 3] = ["permissive", "curated", "strict"];
|
const KNOWN_PLUGIN_POLICIES: [&str; 3] = ["permissive", "curated", "strict"];
|
||||||
const KNOWN_MODERATION_MODES: [&str; 4] = ["minimal", "standard", "strict", "custom"];
|
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)> {
|
fn validate_theme_id(theme_id: &str) -> Result<(), (StatusCode, String)> {
|
||||||
if theme_id.trim().is_empty() {
|
if theme_id.trim().is_empty() {
|
||||||
return Err((StatusCode::BAD_REQUEST, "Theme cannot be empty".to_string()));
|
return Err((StatusCode::BAD_REQUEST, "Theme cannot be empty".to_string()));
|
||||||
|
|
@ -238,6 +240,34 @@ async fn complete_setup(
|
||||||
// Check platform admin permission
|
// Check platform admin permission
|
||||||
require_permission(&pool, auth.user_id, perms::PLATFORM_ADMIN, None).await?;
|
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
|
// Check if already set up
|
||||||
let existing = sqlx::query!("SELECT setup_completed FROM instance_settings LIMIT 1")
|
let existing = sqlx::query!("SELECT setup_completed FROM instance_settings LIMIT 1")
|
||||||
.fetch_optional(&pool)
|
.fetch_optional(&pool)
|
||||||
|
|
@ -300,6 +330,15 @@ async fn complete_setup(
|
||||||
.await
|
.await
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
.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!(
|
sqlx::query!(
|
||||||
r#"INSERT INTO community_settings (community_id, moderation_mode, plugin_policy)
|
r#"INSERT INTO community_settings (community_id, moderation_mode, plugin_policy)
|
||||||
VALUES ($1, $2, $3)
|
VALUES ($1, $2, $3)
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,22 @@ server {
|
||||||
|
|
||||||
## Next Steps
|
## 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
|
- [Configuration](configuration.md) - Detailed settings
|
||||||
- [Database](database.md) - Database management
|
- [Database](database.md) - Database management
|
||||||
- [Security](security.md) - Hardening your instance
|
- [Security](security.md) - Hardening your instance
|
||||||
|
|
|
||||||
|
|
@ -28,13 +28,15 @@ if (!setupRequired) {
|
||||||
|
|
||||||
<Layout title="Setup - Likwid">
|
<Layout title="Setup - Likwid">
|
||||||
<div class="setup-container">
|
<div class="setup-container">
|
||||||
<div class="setup-card ui-card">
|
<div class="setup-card ui-card" data-setup-instance-name={instanceName ?? ''}>
|
||||||
<div class="setup-header">
|
<div class="setup-header">
|
||||||
<h1>Welcome to Likwid</h1>
|
<h1>Welcome to Likwid</h1>
|
||||||
<p>Let's configure your governance platform</p>
|
<p>Let's configure your governance platform</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form id="setup-form" class="setup-form ui-form" style="--ui-form-group-mb: 1rem;">
|
<form id="setup-form" class="setup-form ui-form" style="--ui-form-group-mb: 1rem;">
|
||||||
|
<div id="setup-error" class="setup-banner error" style="display:none;"></div>
|
||||||
|
<div id="setup-success" class="setup-banner success" style="display:none;"></div>
|
||||||
<!-- Step 1: Instance Identity -->
|
<!-- Step 1: Instance Identity -->
|
||||||
<section class="setup-section" data-step="1">
|
<section class="setup-section" data-step="1">
|
||||||
<h2>Instance Identity</h2>
|
<h2>Instance Identity</h2>
|
||||||
|
|
@ -151,6 +153,26 @@ if (!setupRequired) {
|
||||||
padding: 2rem;
|
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 {
|
.setup-section {
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
padding-bottom: 2rem;
|
padding-bottom: 2rem;
|
||||||
|
|
@ -247,9 +269,43 @@ if (!setupRequired) {
|
||||||
const form = document.getElementById('setup-form');
|
const form = document.getElementById('setup-form');
|
||||||
const submitBtn = document.getElementById('submit-btn');
|
const submitBtn = document.getElementById('submit-btn');
|
||||||
const authStatus = document.getElementById('auth-status');
|
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 singleCommunityName = document.getElementById('single-community-name');
|
||||||
const platformModeInputs = document.querySelectorAll('input[name="platform_mode"]');
|
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
|
// Show/hide single community name field
|
||||||
platformModeInputs.forEach(input => {
|
platformModeInputs.forEach(input => {
|
||||||
input.addEventListener('change', (e) => {
|
input.addEventListener('change', (e) => {
|
||||||
|
|
@ -267,6 +323,9 @@ if (!setupRequired) {
|
||||||
<p>You need to log in first. <a href="/login">Go to login</a></p>
|
<p>You need to log in first. <a href="/login">Go to login</a></p>
|
||||||
<p>If you haven't registered yet, <a href="/register">register first</a> (first user becomes admin).</p>
|
<p>If you haven't registered yet, <a href="/register">register first</a> (first user becomes admin).</p>
|
||||||
`;
|
`;
|
||||||
|
showError(
|
||||||
|
'<p><strong>Not signed in.</strong> Create the first user account, then return to this page to complete setup.</p>'
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -289,11 +348,15 @@ if (!setupRequired) {
|
||||||
if (adminRes.status === 403) {
|
if (adminRes.status === 403) {
|
||||||
authStatus.className = 'auth-status error';
|
authStatus.className = 'auth-status error';
|
||||||
authStatus.innerHTML = `<p>You are logged in as ${user.username}, but you need admin privileges.</p>`;
|
authStatus.innerHTML = `<p>You are logged in as ${user.username}, but you need admin privileges.</p>`;
|
||||||
|
showError(
|
||||||
|
'<p><strong>Admin privileges required.</strong> The first registered user becomes platform admin. If you are not the first user, ask an operator to grant you platform admin.</p>'
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
authStatus.className = 'auth-status success';
|
authStatus.className = 'auth-status success';
|
||||||
authStatus.innerHTML = `<p>Logged in as <strong>${user.username}</strong> (admin)</p>`;
|
authStatus.innerHTML = `<p>Logged in as <strong>${user.username}</strong> (admin)</p>`;
|
||||||
|
clearError();
|
||||||
submitBtn.disabled = false;
|
submitBtn.disabled = false;
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -301,6 +364,7 @@ if (!setupRequired) {
|
||||||
authStatus.innerHTML = `
|
authStatus.innerHTML = `
|
||||||
<p>Session expired. <a href="/login">Please log in again</a></p>
|
<p>Session expired. <a href="/login">Please log in again</a></p>
|
||||||
`;
|
`;
|
||||||
|
showError('<p><strong>Session expired.</strong> Please log in again and retry setup.</p>');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -310,10 +374,11 @@ if (!setupRequired) {
|
||||||
// Form submission
|
// Form submission
|
||||||
form.addEventListener('submit', async (e) => {
|
form.addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
clearError();
|
||||||
|
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
if (!token) {
|
if (!token) {
|
||||||
alert('Please log in first');
|
showError('<p><strong>Please log in first.</strong> <a href="/login">Go to login</a></p>');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -338,14 +403,32 @@ if (!setupRequired) {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const error = await res.text();
|
const errorText = await res.text();
|
||||||
throw new Error(error);
|
if (res.status === 401) {
|
||||||
|
showError('<p><strong>Not authorized.</strong> Your session expired. <a href="/login">Log in again</a>.</p>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (res.status === 403) {
|
||||||
|
showError('<p><strong>Forbidden.</strong> You must be a platform admin to complete setup.</p>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showError(
|
||||||
|
`<p><strong>Setup failed.</strong></p><p>${(errorText || 'Unknown error')}</p><p>Fix the issue and try again.</p>`
|
||||||
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Success - redirect to home
|
showSuccess(
|
||||||
window.location.href = '/';
|
'<p><strong>Setup complete.</strong></p>' +
|
||||||
|
'<p>Next steps:</p>' +
|
||||||
|
'<p><a href="/admin/settings">Go to Admin Settings</a></p>' +
|
||||||
|
'<p><a href="/communities">Browse or create communities</a></p>'
|
||||||
|
);
|
||||||
|
window.likwidUi?.toast?.('success', 'Setup complete');
|
||||||
|
submitBtn.textContent = 'Setup Complete';
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert('Setup failed: ' + err.message);
|
showError(`<p><strong>Setup failed.</strong></p><p>${err && err.message ? err.message : 'Unknown error'}</p>`);
|
||||||
|
window.likwidUi?.toast?.('error', 'Setup failed');
|
||||||
submitBtn.disabled = false;
|
submitBtn.disabled = false;
|
||||||
submitBtn.textContent = 'Complete Setup';
|
submitBtn.textContent = 'Complete Setup';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue