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:
2026-10-03 11:42:36 -04:00
parent a2e2311aa9
commit e5c0fda1ed
127 changed files with 8485 additions and 657 deletions
@@ -0,0 +1,12 @@
import { json } from '@sveltejs/kit';
import { rotateCredential } from '$lib/server/hosts/index.js';
import { authenticateAgent, hostApiError, objectBody } from '$lib/server/hosts/http.js';
export async function POST({ request }: { request: Request }) {
const host = authenticateAgent(request);
try {
const body = await objectBody(request);
if (typeof body.credential !== 'string') throw new Error('Persist a randomly generated replacement credential before requesting rotation.');
rotateCredential(host.id, body.credential);
return json({ ok: true }, { headers: { 'Cache-Control': 'no-store' } });
} catch (cause) { return hostApiError(cause); }
}
+8
View File
@@ -0,0 +1,8 @@
import { json } from '@sveltejs/kit';
import { enrollHost } from '$lib/server/hosts/index.js';
import { hostApiError, objectBody, requireAgentTransport } from '$lib/server/hosts/http.js';
export async function POST({ request }: { request: Request }) {
requireAgentTransport(request);
try { const body = await objectBody(request); return json(enrollHost(body.token, body.inventory), { headers: { 'Cache-Control': 'no-store' } }); }
catch (cause) { return hostApiError(cause); }
}
+16
View File
@@ -0,0 +1,16 @@
import { json } from '@sveltejs/kit';
import { heartbeat } from '$lib/server/hosts/index.js';
import { authenticateAgent, hostApiError, objectBody } from '$lib/server/hosts/http.js';
export async function POST({ request }: { request: Request }) {
const authenticated = authenticateAgent(request);
try {
const body = await objectBody(request);
const host = heartbeat(authenticated.id, body.inventory);
const inventory = body.inventory as Record<string, unknown>;
if (Array.isArray(inventory.libraryCaches)) {
const { updateHostLibraryCaches } = await import('$lib/server/game-files/index.js');
updateHostLibraryCaches(host.id, inventory.libraryCaches, { snapshot: true });
}
return json({ host, heartbeatSeconds: 15, serverTime: new Date().toISOString() });
} catch (cause) { return hostApiError(cause); }
}
@@ -0,0 +1,15 @@
import { json } from '@sveltejs/kit';
import { authenticateAgent } from '$lib/server/hosts/http.js';
import { requireImportOwner, setImportManifest } from '$lib/server/game-files/index.js';
import { gameFileError, readGameJson } from '$lib/server/game-files/http.js';
export function GET({ request, params }: { request: Request; params: { id: string } }) {
try { return json(requireImportOwner(authenticateAgent(request).id, params.id)); }
catch (cause) { return gameFileError(cause); }
}
export async function PUT({ request, params }: { request: Request; params: { id: string } }) {
try {
const host = authenticateAgent(request);
const body = await readGameJson(request, 64 * 1024 * 1024);
return json(setImportManifest(host.id, params.id, body.files));
} catch (cause) { return gameFileError(cause); }
}
@@ -0,0 +1,15 @@
import { json } from '@sveltejs/kit';
import { authenticateAgent } from '$lib/server/hosts/http.js';
import { importPublicationStatus, requestImportPublication } from '$lib/server/game-files/index.js';
import { gameFileError } from '$lib/server/game-files/http.js';
export function POST({ request, params }: { request: Request; params: { id: string } }) {
try {
const result = requestImportPublication(authenticateAgent(request).id, params.id);
return json(result, { status: result.status === 'verifying' || result.status === 'pending' ? 202 : 200 });
}
catch (cause) { return gameFileError(cause); }
}
export function GET({ request, params }: { request: Request; params: { id: string } }) {
try { return json(importPublicationStatus(authenticateAgent(request).id, params.id)); }
catch (cause) { return gameFileError(cause); }
}
@@ -0,0 +1,11 @@
import { json } from '@sveltejs/kit';
import { authenticateAgent } from '$lib/server/hosts/http.js';
import { uploadImportFile } from '$lib/server/game-files/index.js';
import { gameFileError } from '$lib/server/game-files/http.js';
export async function PUT({ request, params }: { request: Request; params: { id: string; path: string } }) {
try {
const host = authenticateAgent(request);
await uploadImportFile(host.id, params.id, params.path, request.body);
return json({ uploaded: true });
} catch (cause) { return gameFileError(cause); }
}
@@ -0,0 +1,2 @@
import { agentReleaseAsset } from '$lib/server/hosts/releases.js';
export async function GET() { return new Response(await agentReleaseAsset('install.sh'), { headers: { 'Content-Type': 'text/x-shellscript', 'Cache-Control': 'no-cache' } }); }
@@ -0,0 +1,7 @@
import { authenticateAgent } from '$lib/server/hosts/http.js';
import { gameFileError, serveGameFile } from '$lib/server/game-files/http.js';
export async function GET({ request, params }: { request: Request; params: { id: string; path: string } }) {
try { authenticateAgent(request); return await serveGameFile(request, params.id, params.path); }
catch (cause) { return gameFileError(cause); }
}
export const HEAD = GET;
@@ -0,0 +1,12 @@
import { json } from '@sveltejs/kit';
import { authenticateAgent } from '$lib/server/hosts/http.js';
import { getLibraryVersion } from '$lib/server/game-files/index.js';
import { gameFileError } from '$lib/server/game-files/http.js';
export function GET({ request, params }: { request: Request; params: { id: string } }) {
try {
authenticateAgent(request);
const version = getLibraryVersion(params.id);
if (!version || version.state !== 'ready') return json({ error: 'Published library not found.' }, { status: 404 });
return json({ id: version.id, game: version.game, files: version.files, sizeBytes: version.sizeBytes, manifestSha256: version.manifestSha256 });
} catch (cause) { return gameFileError(cause); }
}
@@ -0,0 +1,11 @@
import { json } from '@sveltejs/kit';
import { authenticateAgent } from '$lib/server/hosts/http.js';
import { updateHostLibraryCaches } from '$lib/server/game-files/index.js';
import { gameFileError, readGameJson } from '$lib/server/game-files/http.js';
export async function POST({ request, params }: { request: Request; params: { id: string } }) {
try {
const host = authenticateAgent(request); const body = await readGameJson(request);
updateHostLibraryCaches(host.id, [{ ...body, libraryVersionId: params.id }]);
return json({ ok: true });
} catch (cause) { return gameFileError(cause); }
}
@@ -0,0 +1,7 @@
import { json } from '@sveltejs/kit';
import { pendingHostOperations } from '$lib/server/hosts/index.js';
import { authenticateAgent } from '$lib/server/hosts/http.js';
export function GET({ request }: { request: Request }) {
const host = authenticateAgent(request);
return json({ operations: pendingHostOperations(host.id), serverTime: new Date().toISOString() }, { headers: { 'Cache-Control': 'no-store' } });
}
@@ -0,0 +1,8 @@
import { json } from '@sveltejs/kit';
import { reportHostOperation } from '$lib/server/hosts/index.js';
import { authenticateAgent, hostApiError, objectBody } from '$lib/server/hosts/http.js';
export async function POST({ request, params }: { request: Request; params: { id: string } }) {
const host = authenticateAgent(request);
try { const body = await objectBody(request, 128 * 1024 * 1024); const operation = reportHostOperation(host.id, params.id, { state: body.state, result: body.result, error: body.error }); return json({ operation: { id: operation.id, state: operation.state } }); }
catch (cause) { return hostApiError(cause); }
}
@@ -0,0 +1,6 @@
import { error } from '@sveltejs/kit';
import { agentReleaseAsset } from '$lib/server/hosts/releases.js';
export async function GET({ params }: { params: { file: string } }) {
if (!['ec-agent.py','manifest.json'].includes(params.file)) error(404, 'Release asset not found.');
return new Response(await agentReleaseAsset(params.file as 'ec-agent.py' | 'manifest.json'), { headers: { 'Content-Type': params.file.endsWith('.json') ? 'application/json' : 'text/plain', 'Cache-Control': 'no-cache' } });
}
+10
View File
@@ -0,0 +1,10 @@
import type { RequestHandler } from './$types';
import { authenticatePassword, revokeSession } from '$lib/server/auth/index.js';
import { authBody, authError, sessionCookie, sessionResponse } from '$lib/server/auth/http.js';
export const POST: RequestHandler = async ({ request, cookies, url, getClientAddress }) => {
try {
const result = await authenticatePassword(await authBody(request),getClientAddress());
revokeSession(cookies.get(sessionCookie(url)));
return sessionResponse(result,cookies,url);
} catch (cause) { return authError(cause); }
};
+9
View File
@@ -0,0 +1,9 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import { recordAudit, revokeSession } from '$lib/server/auth/index.js';
import { sessionCookie } from '$lib/server/auth/http.js';
export const POST: RequestHandler = ({ cookies, url, locals }) => {
revokeSession(cookies.get(sessionCookie(url)));
cookies.delete(sessionCookie(url),{path:'/'});
recordAudit(locals.user?.id??null,'logout',locals.user?.id??null);
return json({ ok:true });
};
+2
View File
@@ -0,0 +1,2 @@
import { json, type RequestHandler } from '@sveltejs/kit';
export const GET: RequestHandler = ({ locals }) => json({ user:locals.user },{headers:{'Cache-Control':'no-store'}});
+11
View File
@@ -0,0 +1,11 @@
import type { RequestHandler } from './$types';
import { changePassword, consumeAuthAttempt } from '$lib/server/auth/index.js';
import { authBody, authError, requireUser, sessionResponse } from '$lib/server/auth/http.js';
export const POST: RequestHandler = async ({ request, cookies, locals, url, getClientAddress }) => {
try {
const user = requireUser(locals.user);
consumeAuthAttempt(getClientAddress(),user.id,'password');
const body = await authBody(request);
return sessionResponse(await changePassword(user.id,body.currentPassword,body.newPassword),cookies,url);
} catch (cause) { return authError(cause); }
};
+7
View File
@@ -0,0 +1,7 @@
import type { RequestHandler } from './$types';
import { bootstrapAdmin } from '$lib/server/auth/index.js';
import { authBody, authError, sessionResponse } from '$lib/server/auth/http.js';
export const POST: RequestHandler = async ({ request, cookies, url, getClientAddress }) => {
try { return sessionResponse(await bootstrapAdmin(await authBody(request),getClientAddress()),cookies,url); }
catch (cause) { return authError(cause); }
};
@@ -4,6 +4,6 @@ import { previewCollection } from '$lib/server/fleet/collections.js';
export async function POST({ request }: { request: Request }) {
try {
const body = await request.json();
return json(previewCollection(body?.blueprintId, body?.mode));
return json(previewCollection(body?.blueprintId, body?.mode, body));
} catch (err) { return json({ error: (err as Error).message }, { status: 400 }); }
}
+10
View File
@@ -0,0 +1,10 @@
import { json } from '@sveltejs/kit';
import { createLibraryImport, libraryOverview } from '$lib/server/game-files/index.js';
import { gameFileError, readGameJson } from '$lib/server/game-files/http.js';
import { requireManagementOrigin } from '$lib/server/hosts/http.js';
export function GET() { return json(libraryOverview()); }
export async function POST({ request }: { request: Request }) {
try { requireManagementOrigin(request); return json({ version: createLibraryImport(await readGameJson(request)) }, { status: 202 }); }
catch (cause) { return gameFileError(cause); }
}
@@ -0,0 +1,6 @@
import { json } from '@sveltejs/kit';
import { getLibraryVersion } from '$lib/server/game-files/index.js';
export function GET({ params }: { params: { id: string } }) {
const version = getLibraryVersion(params.id);
return version ? json({ version }) : json({ error: 'Library version not found.' }, { status: 404 });
}
@@ -0,0 +1,12 @@
import { json } from '@sveltejs/kit';
import { queueLibraryDownload } from '$lib/server/game-files/index.js';
import { gameFileError, readGameJson } from '$lib/server/game-files/http.js';
import { requireManagementOrigin } from '$lib/server/hosts/http.js';
export async function POST({ request, params }: { request: Request; params: { id: string } }) {
try {
requireManagementOrigin(request);
const body = await readGameJson(request);
if (typeof body.hostId !== 'string') throw new Error('Choose a target host.');
return json({ operation: queueLibraryDownload(body.hostId, params.id) }, { status: 202 });
} catch (cause) { return gameFileError(cause); }
}
+13
View File
@@ -0,0 +1,13 @@
import { json } from '@sveltejs/kit';
import { createEnrollment, createHost, listHosts } from '$lib/server/hosts/index.js';
import { hostApiError, installerCommand, objectBody, requireManagementOrigin } from '$lib/server/hosts/http.js';
export function GET({ request }: { request: Request }) { requireManagementOrigin(request); return json({ hosts: listHosts() }); }
export async function POST({ request }: { request: Request }) {
requireManagementOrigin(request);
try {
const host = createHost(await objectBody(request));
const enrollment = createEnrollment(host.id);
return json({ host, enrollment: { ...enrollment, command: installerCommand(request.url, enrollment.token) } }, { status: 201, headers: { 'Cache-Control': 'no-store' } });
} catch (cause) { return hostApiError(cause); }
}
+14
View File
@@ -0,0 +1,14 @@
import { error, json } from '@sveltejs/kit';
import { getHost, listHostOperations, updateHost } from '$lib/server/hosts/index.js';
import { hostApiError, objectBody, requireManagementOrigin } from '$lib/server/hosts/http.js';
type Event = { request: Request; params: { id: string } };
export function GET({ request, params }: Event) {
requireManagementOrigin(request); const host = getHost(params.id); if (!host) error(404, 'Host not found.');
// Payloads and results can contain RCON keys or staged credentials. History
// exposes metadata only; agent APIs remain the only command delivery path.
return json({ host, operations: listHostOperations(host.id).map(({ payload: _payload, result: _result, ...operation }) => operation) });
}
export async function PATCH({ request, params }: Event) {
requireManagementOrigin(request);
try { return json({ host: updateHost(params.id, await objectBody(request)) }); } catch (cause) { return hostApiError(cause); }
}
@@ -0,0 +1,12 @@
import { json } from '@sveltejs/kit';
import { queueHostOperation, revokeCredentials } from '$lib/server/hosts/index.js';
import { hostApiError, requireManagementOrigin } from '$lib/server/hosts/http.js';
type Event = { request: Request; params: { id: string } };
export function DELETE({ request, params }: Event) {
requireManagementOrigin(request);
try { revokeCredentials(params.id); return json({ ok: true }); } catch (cause) { return hostApiError(cause); }
}
export function POST({ request, params }: Event) {
requireManagementOrigin(request);
try { const operation = queueHostOperation(params.id, 'credentials.rotate', {}); return json({ operationId: operation.id }, { status: 202 }); } catch (cause) { return hostApiError(cause); }
}
@@ -0,0 +1,8 @@
import { json } from '@sveltejs/kit';
import { createEnrollment } from '$lib/server/hosts/index.js';
import { hostApiError, installerCommand, requireManagementOrigin } from '$lib/server/hosts/http.js';
export function POST({ request, params }: { request: Request; params: { id: string } }) {
requireManagementOrigin(request);
try { const enrollment = createEnrollment(params.id); return json({ enrollment: { ...enrollment, command: installerCommand(request.url, enrollment.token) } }, { headers: { 'Cache-Control': 'no-store' } }); }
catch (cause) { return hostApiError(cause); }
}
@@ -0,0 +1,12 @@
import { json } from '@sveltejs/kit';
import { queueHostOperation } from '$lib/server/hosts/index.js';
import { hostApiError, objectBody, requireManagementOrigin } from '$lib/server/hosts/http.js';
export async function POST({ request, params }: { request: Request; params: { id: string } }) {
requireManagementOrigin(request);
try {
const body = await objectBody(request);
if (body.kind !== 'runtime.pull' || typeof body.image !== 'string' || !/^[a-zA-Z0-9][a-zA-Z0-9./_:@-]{0,511}$/.test(body.image)) throw new Error('Provide a runtime.pull request and a valid container image.');
const operation = queueHostOperation(params.id, 'runtime.pull', { image: body.image });
return json({ operationId: operation.id }, { status: 202 });
} catch (cause) { return hostApiError(cause); }
}
+13 -2
View File
@@ -7,17 +7,24 @@ import { queueCreateServer } from '$lib/server/provisioning/index.js';
import { config, komodoConfigured, rconConfigured } from '$lib/server/config.js';
import type { GameMode, ZombieGame } from '$lib/server/fleet/map-aliases.js';
import { runningVersions } from '$lib/server/integrations/plutonium.js';
import { listHosts } from '$lib/server/hosts/index.js';
import { nativeInstance } from '$lib/server/backends/native.js';
export async function GET() {
const rconHost = config.rcon.host;
const hosts = listHosts();
const hostsById = new Map(hosts.map(host => [host.id, host]));
const versions = new Map(runningVersions().map(version => [version.server, version]));
const servers = listServers().map((s) => {
const instance = s.native_instance_id ? nativeInstance(s.name) : null;
return {
...serverObservation(s),
host: s.host_id ? hostsById.get(s.host_id) ?? null : null,
network: instance ? { bindAddress: instance.bind_address, localPort: instance.port, publicAddress: instance.public_address, publicPort: instance.public_port } : null,
plutonium: versions.get(s.name),
// Derived endpoint — same logic as endpointFor() but without asking
// Komodo. If we don't have a port yet, endpoint is null.
endpoint: s.game_port ? { host: s.rcon_host ?? rconHost, port: s.game_port } : null,
endpoint: instance ? { host: `agent:${instance.host_id}`, port: instance.port } : s.game_port ? { host: s.rcon_host ?? rconHost, port: s.game_port } : null,
held: [...heldByBeta(s.name).entries()].map(([target, betaId]) => ({ target, betaId }))
};
});
@@ -26,6 +33,9 @@ export async function GET() {
serverNow: Date.now(),
reservedNames: (db().prepare("SELECT j.server FROM server_creation_jobs j JOIN server_deployments d ON d.id = j.deployment_id WHERE d.state IN ('queued', 'running', 'awaiting-health')").all() as { server: string }[]).map(row => row.server),
integrations: {
native: hosts.length > 0 || !komodoConfigured(),
hostsReady: hosts.filter(host => host.readiness === 'ready').length,
hostsTotal: hosts.length,
rcon: rconConfigured(),
komodo: komodoConfigured(),
rconTimeoutMs: config.rcon.timeoutMs,
@@ -36,6 +46,7 @@ export async function GET() {
export async function POST({ request }: { request: Request }) {
const body = (await request.json().catch(() => ({}))) as {
hostId?: string; libraryId?: string; image?: string; bindAddress?: string; publicAddress?: string; publicPort?: number;
game?: string;
gameMode?: string;
mapSlug?: string;
@@ -56,7 +67,7 @@ export async function POST({ request }: { request: Request }) {
plutoKey: body.plutoKey,
expectedName: body.expectedName,
previewId: body.previewId,
createdBy: 'ui'
createdBy: 'ui', hostId: body.hostId, libraryId: body.libraryId, image: body.image, bindAddress: body.bindAddress, publicAddress: body.publicAddress, publicPort: body.publicPort
});
if (!result.ok) {
const status = result.kind === 'bad-request' ? 400 : result.kind === 'unconfigured' ? 503
+19
View File
@@ -6,12 +6,31 @@ import { serverObservation } from '$lib/server/fleet/status.js';
import { freshStatusTimestamp } from '$lib/server-freshness.js';
import { tcpProbe } from '$lib/server/integrations/netProbe.js';
import type { SetupCheck, SystemStatus } from '$lib/system-status.js';
import { listHosts } from '$lib/server/hosts/index.js';
import { listLibraries } from '$lib/server/game-files/index.js';
export async function GET({ url }: { url: URL }) {
const servers = listServers();
const readyServers = servers.filter(server => serverObservation(server).health === 'online').length;
const rconServers = servers.filter(server => !server.last_error && freshStatusTimestamp(server.last_seen_at, Date.now()) && server.container_state === 'running').length;
const savedKeys = listServerKeys().length;
const hosts = listHosts();
if (hosts.length || !komodoConfigured()) {
const readyHosts = hosts.filter(host => host.readiness === 'ready');
const connected = hosts.filter(host => host.connected);
const versions = listLibraries().flatMap(library => library.versions).filter(version => version.state === 'ready');
const cached = versions.filter(version => version.caches.some(cache => cache.status === 'ready' && readyHosts.some(host => host.id === cache.hostId)));
const runtimes = connected.filter(host => host.inventory.runtimeImages?.length);
const checks: SetupCheck[] = [
{ id: 'hosts', title: 'Host agents', state: readyHosts.length ? 'ready' : hosts.length ? 'attention' : 'unconfigured', detail: `${connected.length} connected, ${readyHosts.length} deployment-ready, ${hosts.length} enrolled or awaiting enrollment.`, href: '/hosts', action: hosts.length ? 'Review hosts' : 'Add host' },
{ id: 'runtime', title: 'Plutainer runtime', state: runtimes.length ? 'ready' : 'attention', detail: runtimes.length ? `${runtimes.length} connected hosts report cached runtime images.` : 'Prepare a Plutainer image on the host before creating a server.', href: '/hosts', action: 'Prepare runtime' },
{ id: 'game-files', title: 'Verified game-file libraries', state: cached.length ? 'ready' : 'attention', detail: `${versions.length} published versions; ${cached.length} have a verified cache on a ready host.`, href: '/game-files', action: 'Manage game files' },
{ id: 'rcon', title: 'Agent-local game connection', state: rconServers ? 'ready' : 'unverified', detail: rconServers ? `${rconServers} servers have fresh RCON responses.` : 'The host agent checks RCON locally after deployment. Controller access to game ports is not required.', href: servers.length ? '/' : '/hosts', action: servers.length ? 'Review fleet' : 'Review hosts' },
{ id: 'keys', title: 'Saved server keys', state: savedKeys ? 'ready' : 'attention', detail: `${savedKeys} saved ${savedKeys === 1 ? 'key' : 'keys'}. Validity is checked with Plutonium at startup.`, href: '/server-keys', action: 'Manage keys' },
{ id: 'first-server', title: 'First server ready', state: readyServers ? 'ready' : 'attention', detail: readyServers ? `${readyServers} of ${servers.length} servers are responding.` : servers.length ? 'Registered servers are awaiting fresh healthy reports. Inspect their host and diagnostics.' : 'Choose a ready host and a game library when creating your first server.', href: servers.length ? '/' : '/provisioning', action: servers.length ? 'Review fleet' : 'Create a server' }
];
return json({ checkedAt: new Date().toISOString(), checks, readyServers, totalServers: servers.length, savedKeys } satisfies SystemStatus, { headers: { 'cache-control': 'no-store' } });
}
const missing = [!config.komodo.address && 'KOMODO_ADDRESS', !config.komodo.apiKey && 'KOMODO_API_KEY', !config.komodo.apiSecret && 'KOMODO_API_SECRET'].filter(Boolean);
const connection: SetupCheck = { id: 'komodo', title: 'Komodo connection', state: komodoConfigured() ? 'unverified' : 'unconfigured', detail: komodoConfigured() ? 'Credentials configured. Run connection check to verify host reachability.' : `Set ${missing.join(', ')} in the app environment.`, href: '#setup-guide', action: 'Setup instructions' };
if (komodoConfigured() && url.searchParams.get('probe') === '1') {
+10
View File
@@ -6,8 +6,18 @@ import { recordTelemetryRequest } from '$lib/server/fleet/telemetry-requests.js'
import { recordGameEvent } from '$lib/server/fleet/beta-tests.js';
import { db } from '$lib/server/db/index.js';
import { emit } from '$lib/server/pipeline/bus.js';
import { authenticateTelemetry } from '$lib/server/auth/telemetry.js';
export async function POST({ request }: { request: Request }) {
const authentication = authenticateTelemetry(request);
if (!authentication.ok) {
// Unauthenticated bodies are never parsed, attributed to a server, or
// retained in telemetry diagnostics; they cannot affect game evidence.
return json({ ok: false, error: authentication.error }, {
status: authentication.status,
headers: { 'Cache-Control': 'no-store' }
});
}
const receivedAt = new Date().toISOString();
const contentType = request.headers.get('content-type');
let value: unknown;
+8
View File
@@ -0,0 +1,8 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import { createUser, listUsers } from '$lib/server/auth/index.js';
import { authBody, authError, requireUser } from '$lib/server/auth/http.js';
export const GET: RequestHandler = () => json({ users:listUsers() });
export const POST: RequestHandler = async ({ request, locals }) => {
try { return json({ user:await createUser(requireUser(locals.user).id,await authBody(request)) },{status:201}); }
catch (cause) { return authError(cause); }
};
+8
View File
@@ -0,0 +1,8 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { updateUser } from '$lib/server/auth/index.js';
import { authBody, authError, requireUser } from '$lib/server/auth/http.js';
export const PATCH: RequestHandler = async ({ request, params, locals }) => {
try { return json({ user:await updateUser(requireUser(locals.user).id,params.id,await authBody(request)) }); }
catch (cause) { return authError(cause); }
};
@@ -0,0 +1,8 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { revokeUserSessions } from '$lib/server/auth/index.js';
import { authError, requireUser } from '$lib/server/auth/http.js';
export const POST: RequestHandler = ({ params, locals }) => {
try { revokeUserSessions(requireUser(locals.user).id,params.id); return json({ok:true}); }
catch (cause) { return authError(cause); }
};
+3
View File
@@ -0,0 +1,3 @@
import { json } from '@sveltejs/kit';
import { listAuthAudit } from '$lib/server/auth/index.js';
export function GET() { return json({ events:listAuthAudit() }); }