Files
PiCloud/dashboard/src/lib/auth.ts
MechaCat02 6891496589 feat(manager-core): admin auth gate (Phase 3a)
Closes the regression risk of the admin API and dashboard being open
to anyone reaching the bound port. Required foundation before v1.1
data-plane services land.

Per-user accounts (admin_users), Argon2id passwords, env-var bootstrap
of the first admin that becomes inert once any admin exists, opaque
32-byte session token doubling as bearer credential, 24h sliding TTL
configurable via PICLOUD_SESSION_TTL_HOURS. is_active column lets
admins be deactivated without losing audit history; last-active-admin
guard on DELETE and on PATCH that flips is_active to false (sessions
also wiped on deactivation).

require_admin middleware fronts every /api/v1/admin/* route. The data
plane (/api/v1/execute/{id}), /healthz, /version, and user routes
stay open. picloud admin reset-password <username> subcommand handles
recovery without going through HTTP.

Dashboard gains /admin/login and /admin/admins surfaces, a top-bar
user menu, and a token store with a localStorage echo so refreshes
don't sign you out. Cookie-based auth works in parallel for non-SPA
clients.

Forward compatibility: future RBAC tables (admin_roles,
admin_user_roles) join on admin_users.id; the auth middleware is the
seam where role checks slot in. Email, 2FA, passkeys, and personal
API tokens are all additive without touching admin_users.

Blueprint §11.4 updated to reflect what actually shipped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 19:30:25 +02:00

61 lines
1.7 KiB
TypeScript

// Session state for the dashboard. Backed by a pair of Svelte stores
// plus a tiny localStorage echo so a page reload doesn't sign you out.
//
// The bearer token doubles as the cookie value on the server side, so
// in browsers that honor the Set-Cookie response the cookie path "just
// works"; the token-in-localStorage path covers the rest (HTTP dev, API
// clients impersonating the dashboard) by being injected into the
// Authorization header in api.ts.
import { writable, get } from 'svelte/store';
import { browser } from '$app/environment';
export interface AdminUser {
id: string;
username: string;
}
const TOKEN_KEY = 'picloud.admin.token';
function readStoredToken(): string | null {
if (!browser) return null;
try {
return localStorage.getItem(TOKEN_KEY);
} catch {
return null;
}
}
function writeStoredToken(value: string | null) {
if (!browser) return;
try {
if (value === null) localStorage.removeItem(TOKEN_KEY);
else localStorage.setItem(TOKEN_KEY, value);
} catch {
// Non-fatal: localStorage can be disabled. The session will
// just not survive page reloads, but the in-memory store still
// works for the current SPA lifetime.
}
}
export const token = writable<string | null>(readStoredToken());
export const currentUser = writable<AdminUser | null>(null);
token.subscribe((value) => writeStoredToken(value));
/** Snapshot of the current token without subscribing — used by the
* fetch wrapper. Returns null when no admin is logged in. */
export function getToken(): string | null {
return get(token);
}
export function setSession(user: AdminUser, raw_token: string) {
currentUser.set(user);
token.set(raw_token);
}
export function clearSession() {
currentUser.set(null);
token.set(null);
}