2026-01-27 16:21:58 +00:00
---
2026-01-28 23:47:14 +00:00
export const prerender = false;
2026-01-27 16:21:58 +00:00
import Layout from '../layouts/Layout.astro';
2026-01-28 23:47:14 +00:00
import { API_BASE as apiBase, SERVER_API_BASE as serverApiBase } from '../lib/api';
2026-01-27 16:21:58 +00:00
// Check if setup is needed
let setupRequired = true;
let instanceName = null;
2026-01-28 23:47:14 +00:00
const resolvedServerApiBase = serverApiBase || Astro.url.origin;
2026-01-27 16:21:58 +00:00
try {
2026-01-28 23:47:14 +00:00
const res = await fetch(`${resolvedServerApiBase}/api/settings/setup/status`);
2026-01-27 16:21:58 +00:00
if (res.ok) {
const data = await res.json();
setupRequired = data.setup_required;
instanceName = data.instance_name;
}
} catch (e) {
// Backend not available, assume setup needed
}
// Redirect if setup already complete
if (!setupRequired) {
return Astro.redirect('/');
}
---
<Layout title="Setup - Likwid">
<div class="setup-container">
2026-02-15 21:43:29 +00:00
<div class="setup-card ui-card" data-setup-instance-name={instanceName ?? ''}>
2026-01-27 16:21:58 +00:00
<div class="setup-header">
<h1>Welcome to Likwid</h1>
<p>Let's configure your governance platform</p>
</div>
2026-01-30 11:13:19 +00:00
<form id="setup-form" class="setup-form ui-form" style="--ui-form-group-mb: 1rem;">
2026-02-15 21:43:29 +00:00
<div id="setup-error" class="setup-banner error" style="display:none;"></div>
<div id="setup-success" class="setup-banner success" style="display:none;"></div>
2026-01-27 16:21:58 +00:00
<!-- Step 1: Instance Identity -->
<section class="setup-section" data-step="1">
<h2>Instance Identity</h2>
<div class="form-group">
<label for="instance_name">Platform Name</label>
<input type="text" id="instance_name" name="instance_name" required
placeholder="My Community Platform" />
2026-01-30 08:54:38 +00:00
<span class="help-text">This name will appear in the header and emails</span>
2026-01-27 16:21:58 +00:00
</div>
</section>
<!-- Step 2: Platform Mode -->
<section class="setup-section" data-step="2">
<h2>Platform Mode</h2>
<p class="section-desc">How should communities be created on this platform?</p>
<div class="mode-options">
<label class="mode-option">
<input type="radio" name="platform_mode" value="open" checked />
<div class="mode-card">
<strong>Open</strong>
<p>Any registered user can create communities</p>
</div>
</label>
<label class="mode-option">
<input type="radio" name="platform_mode" value="approval" />
<div class="mode-card">
<strong>Approval Required</strong>
<p>Users can request to create communities, admins approve</p>
</div>
</label>
<label class="mode-option">
<input type="radio" name="platform_mode" value="admin_only" />
<div class="mode-card">
<strong>Admin Only</strong>
<p>Only administrators can create communities</p>
</div>
</label>
<label class="mode-option">
<input type="radio" name="platform_mode" value="single_community" />
<div class="mode-card">
<strong>Single Community</strong>
<p>Platform dedicated to one community</p>
</div>
</label>
</div>
<div id="single-community-name" class="form-group" style="display: none;">
<label for="single_community_name">Community Name</label>
<input type="text" id="single_community_name" name="single_community_name"
placeholder="My Community" />
</div>
</section>
<!-- Admin Login Notice -->
<section class="setup-section" data-step="3">
<h2>Admin Account</h2>
<p class="section-desc">
You need to be logged in as an admin to complete setup.
The first user registered becomes the admin.
</p>
<div id="auth-status" class="auth-status">
<p>Checking authentication...</p>
</div>
</section>
<div class="form-actions">
2026-01-30 08:54:38 +00:00
<button type="submit" id="submit-btn" class="ui-btn ui-btn-primary" disabled>Complete Setup</button>
2026-01-27 16:21:58 +00:00
</div>
</form>
</div>
</div>
</Layout>
<style>
.setup-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
}
.setup-card {
max-width: 600px;
width: 100%;
overflow: hidden;
}
.setup-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 2rem;
text-align: center;
}
.setup-header h1 {
margin: 0 0 0.5rem 0;
font-size: 1.75rem;
}
.setup-header p {
margin: 0;
opacity: 0.9;
}
.setup-form {
padding: 2rem;
}
2026-02-15 21:43:29 +00:00
.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);
}
2026-01-27 16:21:58 +00:00
.setup-section {
margin-bottom: 2rem;
padding-bottom: 2rem;
2026-01-30 08:54:38 +00:00
border-bottom: 1px solid var(--color-border);
2026-01-27 16:21:58 +00:00
}
.setup-section:last-of-type {
border-bottom: none;
}
.setup-section h2 {
font-size: 1.25rem;
margin: 0 0 0.5rem 0;
2026-01-30 08:54:38 +00:00
color: var(--color-text);
2026-01-27 16:21:58 +00:00
}
.section-desc {
2026-01-30 08:54:38 +00:00
color: var(--color-text-muted);
2026-01-27 16:21:58 +00:00
margin: 0 0 1rem 0;
}
.mode-options {
display: grid;
gap: 1rem;
}
.mode-option {
cursor: pointer;
}
.mode-option input {
display: none;
}
.mode-card {
padding: 1rem;
2026-01-30 08:54:38 +00:00
border: 1px solid var(--color-border);
2026-01-27 16:21:58 +00:00
border-radius: 8px;
transition: all 0.2s;
2026-01-30 08:54:38 +00:00
background: var(--color-bg-alt);
2026-01-27 16:21:58 +00:00
}
.mode-option input:checked + .mode-card {
2026-01-30 08:54:38 +00:00
border-color: var(--color-primary);
background: var(--color-primary-muted);
2026-01-27 16:21:58 +00:00
}
.mode-card strong {
display: block;
2026-01-30 08:54:38 +00:00
color: var(--color-text);
2026-01-27 16:21:58 +00:00
}
.mode-card p {
margin: 0.25rem 0 0 0;
font-size: 0.9rem;
2026-01-30 08:54:38 +00:00
color: var(--color-text-muted);
2026-01-27 16:21:58 +00:00
}
.auth-status {
padding: 1rem;
border-radius: 8px;
2026-01-30 08:54:38 +00:00
background: var(--color-bg-alt);
2026-01-27 16:21:58 +00:00
}
.auth-status.success {
2026-01-30 08:54:38 +00:00
background: var(--color-success-muted);
color: var(--color-success);
2026-01-27 16:21:58 +00:00
}
.auth-status.error {
2026-01-30 08:54:38 +00:00
background: var(--color-error-muted);
color: var(--color-error);
2026-01-27 16:21:58 +00:00
}
.form-actions {
margin-top: 2rem;
}
2026-01-30 08:54:38 +00:00
.form-actions .ui-btn {
2026-01-27 16:21:58 +00:00
width: 100%;
2026-01-30 08:54:38 +00:00
justify-content: center;
2026-01-27 16:21:58 +00:00
padding: 1rem;
font-size: 1.1rem;
font-weight: 600;
}
2026-01-30 08:54:38 +00:00
.form-actions .ui-btn:disabled {
2026-01-27 16:21:58 +00:00
opacity: 0.5;
cursor: not-allowed;
}
</style>
2026-01-28 23:47:14 +00:00
<script define:vars={{ apiBase }}>
2026-01-27 16:21:58 +00:00
const form = document.getElementById('setup-form');
const submitBtn = document.getElementById('submit-btn');
const authStatus = document.getElementById('auth-status');
2026-02-15 21:43:29 +00:00
const setupError = document.getElementById('setup-error');
const setupSuccess = document.getElementById('setup-success');
2026-01-27 16:21:58 +00:00
const singleCommunityName = document.getElementById('single-community-name');
const platformModeInputs = document.querySelectorAll('input[name="platform_mode"]');
2026-02-15 21:43:29 +00:00
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';
}
2026-01-27 16:21:58 +00:00
// Show/hide single community name field
platformModeInputs.forEach(input => {
input.addEventListener('change', (e) => {
const target = e.target;
singleCommunityName.style.display = target.value === 'single_community' ? 'block' : 'none';
});
});
// Check auth status
async function checkAuth() {
const token = localStorage.getItem('token');
if (!token) {
authStatus.className = 'auth-status error';
authStatus.innerHTML = `
<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>
`;
2026-02-15 21:43:29 +00:00
showError(
'<p><strong>Not signed in.</strong> Create the first user account, then return to this page to complete setup.</p>'
);
2026-01-27 16:21:58 +00:00
return false;
}
try {
2026-01-28 23:47:14 +00:00
const res = await fetch(`${apiBase}/api/auth/me`, {
2026-01-27 16:21:58 +00:00
headers: { 'Authorization': `Bearer ${token}` }
});
if (!res.ok) {
throw new Error('Invalid token');
}
const user = await res.json();
// Check if user is admin
2026-01-28 23:47:14 +00:00
const adminRes = await fetch(`${apiBase}/api/settings/instance`, {
2026-01-27 16:21:58 +00:00
headers: { 'Authorization': `Bearer ${token}` }
});
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>`;
2026-02-15 21:43:29 +00:00
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>'
);
2026-01-27 16:21:58 +00:00
return false;
}
authStatus.className = 'auth-status success';
authStatus.innerHTML = `<p>Logged in as <strong>${user.username}</strong> (admin)</p>`;
2026-02-15 21:43:29 +00:00
clearError();
2026-01-27 16:21:58 +00:00
submitBtn.disabled = false;
return true;
} catch (e) {
authStatus.className = 'auth-status error';
authStatus.innerHTML = `
<p>Session expired. <a href="/login">Please log in again</a></p>
`;
2026-02-15 21:43:29 +00:00
showError('<p><strong>Session expired.</strong> Please log in again and retry setup.</p>');
2026-01-27 16:21:58 +00:00
return false;
}
}
checkAuth();
// Form submission
form.addEventListener('submit', async (e) => {
e.preventDefault();
2026-02-15 21:43:29 +00:00
clearError();
2026-01-27 16:21:58 +00:00
const token = localStorage.getItem('token');
if (!token) {
2026-02-15 21:43:29 +00:00
showError('<p><strong>Please log in first.</strong> <a href="/login">Go to login</a></p>');
2026-01-27 16:21:58 +00:00
return;
}
const formData = new FormData(form);
const data = {
instance_name: formData.get('instance_name'),
platform_mode: formData.get('platform_mode'),
single_community_name: formData.get('single_community_name') || undefined
};
submitBtn.disabled = true;
submitBtn.textContent = 'Setting up...';
try {
2026-01-28 23:47:14 +00:00
const res = await fetch(`${apiBase}/api/settings/setup`, {
2026-01-27 16:21:58 +00:00
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(data)
});
if (!res.ok) {
2026-02-15 21:43:29 +00:00
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;
2026-01-27 16:21:58 +00:00
}
2026-02-15 21:43:29 +00:00
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';
2026-01-27 16:21:58 +00:00
} catch (err) {
2026-02-15 21:43:29 +00:00
showError(`<p><strong>Setup failed.</strong></p><p>${err && err.message ? err.message : 'Unknown error'}</p>`);
window.likwidUi?.toast?.('error', 'Setup failed');
2026-01-27 16:21:58 +00:00
submitBtn.disabled = false;
submitBtn.textContent = 'Complete Setup';
}
});
</script>