Files
electric-cherry/src/routes/setup/+page.svelte
T
josh e5c0fda1ed Add native host management and role-based operator authentication
Introduce an outbound Python host agent, Debian installer, durable remote
operations, immutable game-file libraries, host-aware deployments and ports,
and occupancy-safe campaigns and draining. Native hosts do not require Komodo.

Add administrator, operator and viewer accounts, protected management routes,
secure sessions, password recovery, audit history and separate telemetry tokens.
Document first-admin setup, reporter migration and live-host acceptance checks.

Validation: 1,343 TypeScript tests and 39 agent tests passed, along with type
checking, the production build, compiled SSR/API smoke tests and administrator
recovery. Real Debian/Docker T5/T6 acceptance remains unrun.
2026-10-03 11:42:36 -04:00

45 lines
4.4 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts">
import Button from '$ui/components/primitives/Button.svelte';
import SectionHeader from '$ui/components/primitives/SectionHeader.svelte';
let token = $state(''); let username = $state(''); let displayName = $state('');
let password = $state(''); let confirmPassword = $state(''); let busy = $state(false); let error = $state('');
async function setup(event: SubmitEvent) {
event.preventDefault(); if (busy) return; error = '';
if (password !== confirmPassword) { error = 'The passwords do not match.'; return; }
busy = true;
try {
const response = await fetch('/api/auth/setup', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token, username, displayName, password }) });
const body = await response.json(); if (!response.ok) throw new Error(body.error ?? 'Could not create the administrator.');
token = ''; password = ''; confirmPassword = '';
window.location.assign(body.user?.mustChangePassword ? '/account' : '/');
} catch (cause) { error = (cause as Error).message; busy = false; }
}
</script>
<svelte:head><title>Initial setup · Electric Cherry</title><meta name="robots" content="noindex" /></svelte:head>
<section class="auth-card">
<SectionHeader title="Create the first administrator" subtitle="Secure this controller and invite your team." />
<p class="help">Use the setup token configured by the controller owner. This step is available only until the first administrator is created.</p>
{#if error}<p class="error" role="alert">{error}</p>{/if}
<form onsubmit={setup} aria-busy={busy}><fieldset disabled={busy}>
<label for="setup-token">Setup token<input id="setup-token" name="token" type="password" required minlength="32" autocomplete="off" spellcheck="false" bind:value={token} aria-describedby="token-help" /></label>
<p id="token-help" class="hint">Provided through the controller’s <code>EC_SETUP_TOKEN</code> setting.</p>
<label for="setup-username">Username<input id="setup-username" name="username" required minlength="3" maxlength="64" pattern={'[a-zA-Z0-9][a-zA-Z0-9_.\\-]{2,63}'} autocomplete="username" autocapitalize="none" spellcheck="false" bind:value={username} aria-describedby="username-help" /></label>
<p id="username-help" class="hint">3–64 characters. Start with a letter or number; periods, underscores, and hyphens are allowed.</p>
<label for="setup-name">Display name<input id="setup-name" name="displayName" required maxlength="100" autocomplete="name" bind:value={displayName} /></label>
<label for="setup-password">Password<input id="setup-password" name="password" type="password" required minlength="15" maxlength="128" autocomplete="new-password" bind:value={password} aria-describedby="password-help" /></label>
<p id="password-help" class="hint">Use 15–128 characters. A long, unique passphrase works well.</p>
<label for="setup-confirm">Confirm password<input id="setup-confirm" name="confirmPassword" type="password" required minlength="15" maxlength="128" autocomplete="new-password" bind:value={confirmPassword} /></label>
<Button type="submit" variant="primary" disabled={busy} loading={busy}>Create administrator</Button>
</fieldset></form>
<a class="login-link" href="/login">Already have an account? Sign in</a>
</section>
<style>
.auth-card { padding:32px; border:1px solid var(--border-strong); border-radius:var(--radius-lg); background:var(--surface); box-shadow:0 18px 50px #0002; }
form { margin-top:24px; } fieldset { display:grid; gap:16px; border:0; margin:0; padding:0; min-width:0; } label { display:grid; gap:8px; font-size:var(--text-sm); } input { width:100%; padding:12px; background:var(--surface-sunken); color:var(--text-primary); border:1px solid var(--border-control); border-radius:var(--radius-md); font:inherit; }
.help,.hint,.login-link { font-size:var(--text-sm); color:var(--text-secondary); line-height:1.6; } .help { margin:20px 0 0; } .hint { margin:-8px 0 0; font-size:var(--text-xs); } .login-link { display:block; margin-top:24px; color:var(--accent-primary-hover); }
.error { color:var(--state-danger); font-size:var(--text-sm); padding:12px; border:1px solid var(--state-danger); border-radius:var(--radius-md); overflow-wrap:anywhere; }
@media(max-width:540px) { .auth-card { padding:24px; } }
</style>