Files
famdone/frontend/src/routes/[fam]/+layout.server.ts
T
2026-09-08 17:45:12 +01:00

110 lines
2.9 KiB
TypeScript

import { redirect } from '@sveltejs/kit';
import { createPbClient } from '$lib/server/pocketbase';
import { createServices, type ChatActor } from '$lib/server/services';
import { ensureFamAccess } from '$lib/server/access';
async function paydayCheck(famId: string, pbToken: string) {
try {
// Best-effort: never block render on the payday heartbeat.
const s = createServices(createPbClient(pbToken));
await s.fam.payday(famId);
} catch {}
}
function actorFrom(session: {
famId: string;
id: string;
role: string;
name?: string;
color?: string;
}): ChatActor {
return {
id: session.id,
type: session.role === 'parent' ? 'admin' : 'member',
name: session.name || '',
color: session.color || '#6366f1'
};
}
async function resolveChatIdentity(
session: {
famId: string;
id: string;
role: string;
name?: string;
color?: string;
},
pbToken: string
) {
try {
const s = createServices(createPbClient(pbToken));
const actor = actorFrom(session);
return await s.chat.me(session.famId, actor);
} catch {
return null;
}
}
export async function load(event) {
const session = event.locals.user;
const role = session?.role || 'child';
const isParent = role === 'parent';
const pbToken = event.cookies.get('pb_token') || '';
let famId = '';
let chat: { famId: string; actor: ChatActor } | null = null;
let fam: any = null;
let famAccess = {
disabled: false,
mode: 'none' as 'none' | 'code' | 'sub' | 'canceled',
reason: ''
};
if (session && pbToken) {
famId = session.famId;
await paydayCheck(famId, pbToken);
chat = await resolveChatIdentity(session, pbToken);
// fams is superadmin-only (non-realtime). Fetched server-side for both
// roles; also recomputes + persists the derived `active` flag.
const res = await ensureFamAccess(famId).catch(() => null);
if (res) {
fam = res.fam;
famAccess = res.access;
}
// Canonical URL: the [fam] segment must be the family SLUG, never the PB
// id. If someone lands on /{famId}/... (a stale shortcut, bookmark, or a
// login that fell back to the id), rewrite the first path segment to the
// slug so the id is replaced everywhere it'd otherwise persist.
const paramFam = event.params.fam;
const canonicalSlug = fam?.slug;
if (canonicalSlug && paramFam && paramFam !== canonicalSlug) {
const rest = event.url.pathname.replace(`/${paramFam}`, '') || '/';
const qs = event.url.search;
throw redirect(303, `/${encodeURIComponent(canonicalSlug)}${rest}${qs}`);
}
}
return {
// Canonical fam slug — from the URL param ([fam] routes). Client code
// reads page.data.famSlug; never copy it into local $state.
famSlug: event.params.fam || '',
session: session
? {
famId: session.famId,
userId: session.id,
memberName: session.name,
memberColor: session.color || '',
username: session.username || '',
role: session.role
}
: null,
isParent,
role,
famId,
chat,
pbToken,
fam,
famAccess
};
}