mirror of
https://codeberg.org/likwid/likwid.git
synced 2026-06-25 07:27:42 +00:00
Verified changes: - add backend/.sqlx/query-6c6f48705ccafb301ec03c8a0b719c0f702cb6530e056fd1504227f3aa1fd930.json - rename backend/.sqlx/query-0620f314de8df0c7990ef63fda55f2ff646d5159c59d8288e4ffdfdb07dc159f.json -> backend/.sqlx/query-709e697a9c2e689a114c9bf3f1877bd5ad8b39f1803a0f86a421d467368015bd.json - add backend/.sqlx/query-71eef9a9c46cd41a8272aeaa9d86b032d0607f699a477487a6e4924bd75cef41.json - rename backend/.sqlx/query-593dc329afc129680dd505221df649aa8cb544841fa78a5fe740adfbb4439502.json -> backend/.sqlx/query-85b30d0c2440ee40790c496173ebd1f93f6514df19c9b83969f0d58900f461ec.json - add backend/.sqlx/query-8d5731d3f05af6b7069068752bd7ad6184392718f1da98b793a8ba2dfaf84c2b.json - rename backend/.sqlx/query-786d22de6313d554e95069d020362b4880f40187cb022d94126df9ced0f3f162.json -> backend/.sqlx/query-8f7e57bc074a8a9c80e20ae51d497582ed79f8dc5fd135ea7117bfc3d1d33c11.json - rename backend/.sqlx/query-8dd178663df95d64d72c776e3b8bda63851d2ad0e13a6d80b327610078ecbaeb.json -> backend/.sqlx/query-a1e179a81e94c91a7ad6201cea880a8e133766adfa97bad4bb22936317ddcd6c.json - add backend/.sqlx/query-cf573b897b379059a8a14132fdedb844e5210ef3ad0205ae2515ab7e4589240c.json - add backend/migrations/20260221160000_instance_type_and_community_creator_role.sql - add backend/migrations/20260221163000_user_roles_unique_platform.sql - modify backend/src/api/roles.rs - modify backend/src/api/settings.rs - modify backend/src/api/users.rs - modify frontend/src/components/AdminNav.astro - modify frontend/src/layouts/Layout.astro - add frontend/src/pages/admin/community-creators.astro - add frontend/src/pages/admin/index.astro - modify frontend/src/pages/communities/new.astro - modify frontend/src/pages/setup.astro Diffstat: - 19 files changed, 828 insertions(+), 45 deletions(-)
502 lines
15 KiB
Text
502 lines
15 KiB
Text
---
|
|
export const prerender = false;
|
|
import Layout from '../layouts/Layout.astro';
|
|
import { API_BASE as apiBase, SERVER_API_BASE as serverApiBase } from '../lib/api';
|
|
|
|
// Check if setup is needed
|
|
let setupRequired = true;
|
|
let instanceName = null;
|
|
|
|
const resolvedServerApiBase = serverApiBase || Astro.url.origin;
|
|
|
|
try {
|
|
const res = await fetch(`${resolvedServerApiBase}/api/settings/setup/status`);
|
|
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">
|
|
<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>
|
|
|
|
<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" />
|
|
<span class="help-text">This name will appear in the header and emails</span>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="setup-section" data-step="2">
|
|
<h2>Instance Type</h2>
|
|
<p class="section-desc">Choose what kind of Likwid instance you want to run.</p>
|
|
|
|
<div class="mode-options">
|
|
<label class="mode-option">
|
|
<input type="radio" name="instance_type" value="multi_community" checked />
|
|
<div class="mode-card">
|
|
<strong>Multi-community</strong>
|
|
<p>Host multiple communities with shared platform policies</p>
|
|
</div>
|
|
</label>
|
|
|
|
<label class="mode-option">
|
|
<input type="radio" name="instance_type" value="single_community" />
|
|
<div class="mode-card">
|
|
<strong>Single community</strong>
|
|
<p>Dedicated instance with one main community</p>
|
|
</div>
|
|
</label>
|
|
|
|
<label class="mode-option">
|
|
<input type="radio" name="instance_type" value="federation" />
|
|
<div class="mode-card">
|
|
<strong>Federation</strong>
|
|
<p>Prepare for federation features and cross-instance collaboration</p>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Step 2: Platform Mode -->
|
|
<section class="setup-section" data-step="3">
|
|
<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" />
|
|
<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" checked />
|
|
<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="4">
|
|
<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">
|
|
<button type="submit" id="submit-btn" class="ui-btn ui-btn-primary" disabled>Complete Setup</button>
|
|
</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;
|
|
}
|
|
|
|
.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;
|
|
border-bottom: 1px solid var(--color-border);
|
|
}
|
|
|
|
.setup-section:last-of-type {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.setup-section h2 {
|
|
font-size: 1.25rem;
|
|
margin: 0 0 0.5rem 0;
|
|
color: var(--color-text);
|
|
}
|
|
|
|
.section-desc {
|
|
color: var(--color-text-muted);
|
|
margin: 0 0 1rem 0;
|
|
}
|
|
|
|
.mode-options {
|
|
display: grid;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.mode-option {
|
|
cursor: pointer;
|
|
}
|
|
|
|
.mode-option input {
|
|
display: none;
|
|
}
|
|
|
|
.mode-card {
|
|
padding: 1rem;
|
|
border: 1px solid var(--color-border);
|
|
border-radius: 8px;
|
|
transition: all 0.2s;
|
|
background: var(--color-bg-alt);
|
|
}
|
|
|
|
.mode-option input:checked + .mode-card {
|
|
border-color: var(--color-primary);
|
|
background: var(--color-primary-muted);
|
|
}
|
|
|
|
.mode-card strong {
|
|
display: block;
|
|
color: var(--color-text);
|
|
}
|
|
|
|
.mode-card p {
|
|
margin: 0.25rem 0 0 0;
|
|
font-size: 0.9rem;
|
|
color: var(--color-text-muted);
|
|
}
|
|
|
|
.auth-status {
|
|
padding: 1rem;
|
|
border-radius: 8px;
|
|
background: var(--color-bg-alt);
|
|
}
|
|
|
|
.auth-status.success {
|
|
background: var(--color-success-muted);
|
|
color: var(--color-success);
|
|
}
|
|
|
|
.auth-status.error {
|
|
background: var(--color-error-muted);
|
|
color: var(--color-error);
|
|
}
|
|
|
|
.form-actions {
|
|
margin-top: 2rem;
|
|
}
|
|
|
|
.form-actions .ui-btn {
|
|
width: 100%;
|
|
justify-content: center;
|
|
padding: 1rem;
|
|
font-size: 1.1rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.form-actions .ui-btn:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
</style>
|
|
|
|
<script define:vars={{ apiBase }}>
|
|
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 instanceTypeInputs = document.querySelectorAll('input[name="instance_type"]');
|
|
|
|
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) => {
|
|
const target = e.target;
|
|
singleCommunityName.style.display = target.value === 'single_community' ? 'block' : 'none';
|
|
|
|
if (target.value === 'single_community') {
|
|
const singleType = document.querySelector('input[name="instance_type"][value="single_community"]');
|
|
if (singleType && singleType instanceof HTMLInputElement) {
|
|
singleType.checked = true;
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
instanceTypeInputs.forEach(input => {
|
|
input.addEventListener('change', (e) => {
|
|
const target = e.target;
|
|
if (!(target instanceof HTMLInputElement)) return;
|
|
|
|
if (target.value === 'single_community') {
|
|
const singleMode = document.querySelector('input[name="platform_mode"][value="single_community"]');
|
|
if (singleMode && singleMode instanceof HTMLInputElement) {
|
|
singleMode.checked = true;
|
|
singleCommunityName.style.display = 'block';
|
|
}
|
|
} else {
|
|
const selectedMode = document.querySelector('input[name="platform_mode"]:checked');
|
|
if (selectedMode && selectedMode instanceof HTMLInputElement) {
|
|
if (selectedMode.value === 'single_community') {
|
|
const approvalMode = document.querySelector('input[name="platform_mode"][value="approval"]');
|
|
if (approvalMode && approvalMode instanceof HTMLInputElement) {
|
|
approvalMode.checked = true;
|
|
}
|
|
}
|
|
}
|
|
singleCommunityName.style.display = '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>
|
|
`;
|
|
showError(
|
|
'<p><strong>Not signed in.</strong> Create the first user account, then return to this page to complete setup.</p>'
|
|
);
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
const res = await fetch(`${apiBase}/api/auth/me`, {
|
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
});
|
|
|
|
if (!res.ok) {
|
|
throw new Error('Invalid token');
|
|
}
|
|
|
|
const user = await res.json();
|
|
|
|
// Check if user is admin
|
|
const adminRes = await fetch(`${apiBase}/api/settings/instance`, {
|
|
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>`;
|
|
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) {
|
|
authStatus.className = 'auth-status error';
|
|
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;
|
|
}
|
|
}
|
|
|
|
checkAuth();
|
|
|
|
// Form submission
|
|
form.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
clearError();
|
|
|
|
const token = localStorage.getItem('token');
|
|
if (!token) {
|
|
showError('<p><strong>Please log in first.</strong> <a href="/login">Go to login</a></p>');
|
|
return;
|
|
}
|
|
|
|
const formData = new FormData(form);
|
|
const data = {
|
|
instance_name: formData.get('instance_name'),
|
|
instance_type: formData.get('instance_type') || 'multi_community',
|
|
platform_mode: formData.get('platform_mode'),
|
|
single_community_name: formData.get('single_community_name') || undefined
|
|
};
|
|
|
|
submitBtn.disabled = true;
|
|
submitBtn.textContent = 'Setting up...';
|
|
|
|
try {
|
|
const res = await fetch(`${apiBase}/api/settings/setup`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${token}`
|
|
},
|
|
body: JSON.stringify(data)
|
|
});
|
|
|
|
if (!res.ok) {
|
|
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;
|
|
}
|
|
|
|
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) {
|
|
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';
|
|
}
|
|
});
|
|
</script>
|