Files
famdone/frontend/src/lib/server/session.ts
T
2026-09-12 08:45:13 +01:00

98 lines
3.4 KiB
TypeScript

import type { Cookies } from '@sveltejs/kit';
// PocketBase JWTs live in cookies shared by:
// - the server hooks (authRefresh -> locals.user)
// - the client SDK (seeded from page.data.pbToken -> authenticated famStore reads/subscribe)
// httpOnly keeps tokens out of reach of browser JS/XSS; the client receives
// the token server-side via the layout load (pbToken) and seeds pb.authStore.
// Secure flag is set in prod so it's only sent over HTTPS.
export const SESSION_COOKIE = 'pb_token';
// Shared-device multi-session: children hold ONE cookie per account
// (`pb_token_<userId>`); `pb_active` names which one is the current session.
// Parents stay on the single `pb_token`.
export const CHILD_COOKIE_PREFIX = 'pb_token_';
export const ACTIVE_COOKIE = 'pb_active';
const MAX_AGE = 60 * 60 * 24 * 5; // 5 days — matches the PB users auth token duration
function cookieOpts() {
return {
httpOnly: true,
sameSite: 'lax' as const,
path: '/',
maxAge: MAX_AGE,
secure: import.meta.env.PROD
};
}
export function setSessionCookie(cookies: Cookies, token: string) {
cookies.set(SESSION_COOKIE, token, cookieOpts());
}
export function childSessionCookie(userId: string) {
return `${CHILD_COOKIE_PREFIX}${userId}`;
}
export function setChildSessionCookie(cookies: Cookies, userId: string, token: string) {
cookies.set(childSessionCookie(userId), token, cookieOpts());
}
export function clearChildSession(cookies: Cookies, userId: string) {
cookies.delete(childSessionCookie(userId), { path: '/' });
}
export function setActiveChild(cookies: Cookies, userId: string) {
cookies.set(ACTIVE_COOKIE, userId, cookieOpts());
}
export function clearActiveChild(cookies: Cookies) {
cookies.delete(ACTIVE_COOKIE, { path: '/' });
}
// Ids of every child that has a session cookie on this device.
export function scanChildSessions(cookies: Cookies): string[] {
return cookies
.getAll()
.filter((c) => c.name.startsWith(CHILD_COOKIE_PREFIX))
.map((c) => c.name.slice(CHILD_COOKIE_PREFIX.length))
.filter(Boolean);
}
// Remove every child session on this device (logout-all).
export function clearDeviceSessions(cookies: Cookies) {
for (const id of scanChildSessions(cookies)) clearChildSession(cookies, id);
clearActiveChild(cookies);
}
export function clearSessionCookie(cookies: Cookies) {
cookies.delete(SESSION_COOKIE, { path: '/' });
}
// Legacy pre-PB-auth child cookie. No longer issued anywhere; these deletes
// exist only to scrub it from browsers that still carry one.
const LEGACY_DEVICE_COOKIE = 'device_token';
export function clearLegacyCookies(cookies: Cookies) {
cookies.delete(LEGACY_DEVICE_COOKIE, { path: '/' });
}
// ── Platform-admin session (/admin) ──
// Holds a REAL PocketBase superuser JWT (minted by _superusers.authWithPassword
// at login) — verified per-request in hooks via authRefresh, so forging the
// cookie value gains nothing. Separate from the fam-user pb_token.
export const PLATFORM_SESSION_COOKIE = 'platform_session';
const PLATFORM_MAX_AGE = 60 * 60 * 24; // 24h; PB token expiry is the ceiling
export function setPlatformSession(cookies: Cookies, token: string) {
cookies.set(PLATFORM_SESSION_COOKIE, token, {
httpOnly: true,
sameSite: 'lax',
path: '/',
maxAge: PLATFORM_MAX_AGE,
secure: import.meta.env.PROD
});
}
export function clearPlatformSession(cookies: Cookies) {
cookies.delete(PLATFORM_SESSION_COOKIE, { path: '/' });
}