Files
famdone/frontend/src/hooks.server.ts
T
2026-09-10 13:23:55 +01:00

76 lines
2.5 KiB
TypeScript

import type { Handle } from '@sveltejs/kit';
import { createPbClient } from '$lib/server/pocketbase';
import {
SESSION_COOKIE,
setSessionCookie,
clearSessionCookie,
PLATFORM_SESSION_COOKIE,
clearPlatformSession
} from '$lib/server/session';
import type { SessionUser } from '$lib/server/types';
import { handleOf } from '@shared/slugify';
import { migrateOnBoot } from '$lib/server/migrate-boot';
// Run the PB schema migration once at server boot (idempotent).
void migrateOnBoot();
export const handle: Handle = async ({ event, resolve }) => {
event.locals.user = null;
event.locals.pbToken = null;
event.locals.platformAdmin = false;
// Platform-admin routes authenticate via the superuser JWT in
// platform_session, verified against PB (authRefresh) — the cookie value
// is a real signed token, so forging it gains nothing.
if (event.url.pathname.startsWith('/admin')) {
const suToken = event.cookies.get(PLATFORM_SESSION_COOKIE);
if (suToken) {
try {
await createPbClient(suToken).collection('_superusers').authRefresh();
event.locals.platformAdmin = true;
} catch {
// Expired/revoked/forged token — drop it and treat as logged out.
clearPlatformSession(event.cookies);
}
}
return resolve(event);
}
// Fam-user session: pb_token JWT → authRefresh → locals.user.
const token = event.cookies.get(SESSION_COOKIE);
if (token) {
const pb = createPbClient(token);
try {
// authRefresh() does two jobs in one call:
// 1. Verifies the token (PB JWTs can't be checked offline — the
// signing secret is per-record and never leaves PB), so this
// round trip IS the verification step.
// 2. Returns the current record — the only way to get
// name/role/famId, since PB doesn't embed custom fields in the
// token itself.
const { record, token: freshToken } = await pb.collection('users').authRefresh();
event.locals.user = {
id: record.id,
name: record.name || record.username || '',
username: handleOf(record.username || ''),
role: record.role || 'parent',
famId: record.famId,
color: record.color || '',
pattern: record.pattern || '',
themeSize: record.themeSize || '',
themeOpacity: record.themeOpacity || ''
} satisfies SessionUser;
event.locals.pbToken = freshToken;
if (freshToken !== token) {
setSessionCookie(event.cookies, freshToken);
}
} catch {
// Expired, malformed, or revoked — drop it and treat as logged out.
clearSessionCookie(event.cookies);
}
}
return resolve(event);
};