110 lines
2.9 KiB
TypeScript
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
|
|
};
|
|
}
|