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_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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -28,13 +28,15 @@ if (!setupRequired) {
|
|||
|
||||
<Layout title="Setup - Likwid">
|
||||
<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">
|
||||
<h1>Welcome to Likwid</h1>
|
||||
<p>Let's configure your governance platform</p>
|
||||
</div>
|
||||
|
||||
<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 -->
|
||||
<section class="setup-section" data-step="1">
|
||||
<h2>Instance Identity</h2>
|
||||
|
|
@ -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) {
|
|||
<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>
|
||||
`;
|
||||
showError(
|
||||
'<p><strong>Not signed in.</strong> Create the first user account, then return to this page to complete setup.</p>'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -289,11 +348,15 @@ if (!setupRequired) {
|
|||
if (adminRes.status === 403) {
|
||||
authStatus.className = 'auth-status error';
|
||||
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;
|
||||
}
|
||||
|
||||
authStatus.className = 'auth-status success';
|
||||
authStatus.innerHTML = `<p>Logged in as <strong>${user.username}</strong> (admin)</p>`;
|
||||
clearError();
|
||||
submitBtn.disabled = false;
|
||||
return true;
|
||||
} catch (e) {
|
||||
|
|
@ -301,6 +364,7 @@ if (!setupRequired) {
|
|||
authStatus.innerHTML = `
|
||||
<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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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('<p><strong>Please log in first.</strong> <a href="/login">Go to login</a></p>');
|
||||
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('<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
|
||||
window.location.href = '/';
|
||||
showSuccess(
|
||||
'<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) {
|
||||
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.textContent = 'Complete Setup';
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue