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.
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { invalidateAll } from '$app/navigation';
|
||||
import Button from '$ui/components/primitives/Button.svelte';
|
||||
import SectionHeader from '$ui/components/primitives/SectionHeader.svelte';
|
||||
let currentPassword = $state(''); let newPassword = $state(''); let confirmPassword = $state('');
|
||||
let busy = $state(false); let signingOut = $state(false); let error = $state(''); let notice = $state('');
|
||||
const user = $derived($page.data.user);
|
||||
async function changePassword(event: SubmitEvent) {
|
||||
event.preventDefault(); if (busy || signingOut) return; error = ''; notice = '';
|
||||
if (newPassword !== confirmPassword) { error = 'The new passwords do not match.'; return; }
|
||||
const required = user?.mustChangePassword;
|
||||
busy = true;
|
||||
try {
|
||||
const response = await fetch('/api/auth/password', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ currentPassword, newPassword }) });
|
||||
const body = await response.json(); if (!response.ok) throw new Error(body.error ?? 'Could not change your password.');
|
||||
currentPassword = ''; newPassword = ''; confirmPassword = '';
|
||||
if (required) { window.location.assign('/'); return; }
|
||||
await invalidateAll(); notice = 'Your password has been changed.';
|
||||
} catch (cause) { error = (cause as Error).message; } finally { busy = false; }
|
||||
}
|
||||
async function logout() {
|
||||
if (busy || signingOut) return; signingOut = true; error = '';
|
||||
try {
|
||||
const response = await fetch('/api/auth/logout', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' });
|
||||
if (!response.ok) { const body = await response.json(); throw new Error(body.error ?? 'Could not sign out.'); }
|
||||
try { sessionStorage.removeItem('electric-cherry:notifications'); } catch { /* Optional history. */ }
|
||||
window.location.assign('/login');
|
||||
} catch (cause) { error = (cause as Error).message; signingOut = false; }
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head><title>My account · Electric Cherry</title></svelte:head>
|
||||
<section class="page"><SectionHeader title="My account" subtitle="Manage your password and current session." />
|
||||
{#if user}
|
||||
<section class="panel identity"><div><h2>{user.displayName}</h2><p>@{user.username} · <span class="role">{user.role}</span></p></div><Button disabled={busy || signingOut} loading={signingOut} onclick={logout}>Sign out</Button></section>
|
||||
{#if user.mustChangePassword}<p class="required" role="status">Your account uses a temporary password. Choose a new password before continuing to Electric Cherry.</p>{/if}
|
||||
<section class="panel"><h2>{user.mustChangePassword ? 'Choose your password' : 'Change password'}</h2>
|
||||
{#if error}<p class="error" role="alert">{error}</p>{/if}{#if notice}<p class="notice" role="status">{notice}</p>{/if}
|
||||
<form onsubmit={changePassword} aria-busy={busy}><fieldset disabled={busy || signingOut}>
|
||||
<input type="text" class="password-username" value={user.username} autocomplete="username" aria-label="Username" readonly tabindex="-1" />
|
||||
<label for="current-password">{user.mustChangePassword ? 'Temporary password' : 'Current password'}<input id="current-password" name="currentPassword" type="password" autocomplete="current-password" required maxlength="128" bind:value={currentPassword} /></label>
|
||||
<label for="new-password">New password<input id="new-password" name="newPassword" type="password" autocomplete="new-password" required minlength="15" maxlength="128" bind:value={newPassword} aria-describedby="new-password-help" /></label>
|
||||
<p id="new-password-help" class="hint">Use 15–128 characters and a password you do not use elsewhere.</p>
|
||||
<label for="confirm-password">Confirm new password<input id="confirm-password" name="confirmPassword" type="password" autocomplete="new-password" required minlength="15" maxlength="128" bind:value={confirmPassword} /></label>
|
||||
<div><Button type="submit" variant="primary" loading={busy} disabled={busy || signingOut}>{user.mustChangePassword ? 'Save password and continue' : 'Change password'}</Button></div>
|
||||
</fieldset></form>
|
||||
</section>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.page { max-width:760px; display:grid; gap:24px; } .panel { padding:24px; background:var(--surface); border:1px solid var(--border-strong); border-radius:var(--radius-md); } h2 { font-size:var(--text-md); margin:0; } .identity { display:flex; align-items:center; justify-content:space-between; gap:20px; } .identity p { margin:8px 0 0; color:var(--text-secondary); font-size:var(--text-sm); } .role { text-transform:capitalize; }
|
||||
.required { padding:16px; margin:0; color:var(--text-primary); background:var(--surface-raised); border-left:3px solid var(--accent-primary); font-size:var(--text-sm); line-height:1.6; }
|
||||
form { margin-top:24px; max-width:460px; } fieldset { border:0; margin:0; padding:0; display:grid; gap:18px; min-width:0; } label { display:grid; gap:8px; font-size:var(--text-sm); } input { width:100%; padding:12px; color:var(--text-primary); background:var(--surface-sunken); border:1px solid var(--border-control); border-radius:var(--radius-md); font:inherit; } .password-username { position:absolute; width:1px; height:1px; clip-path:inset(50%); overflow:hidden; padding:0; border:0; } .hint { margin:-8px 0 0; color:var(--text-secondary); font-size:var(--text-xs); line-height:1.6; } .error { color:var(--state-danger); font-size:var(--text-sm); } .notice { color:var(--state-online); font-size:var(--text-sm); }
|
||||
@media(max-width:540px) { .identity { align-items:flex-start; flex-direction:column; } }
|
||||
</style>
|
||||
Reference in New Issue
Block a user