import { derived, writable } from 'svelte/store'; import { getRole, onClearAuth, onSetAuth } from './auth'; export type Role = 'guest' | 'host' | 'admin'; /** * The viewer's LIVE role. * * The JWT is never reissued — the backend slides the session row forward instead and * deliberately ignores the token's own role claim (`auth/middleware.rs`: "the live user row * is authoritative"). So `getRole()`, which decodes the claim, is frozen for the lifetime of * the token: up to 30 days for a guest. A guest promoted to host saw no Host-Dashboard until * they signed out and back in, even though `/me/context` had already told the client their * real role on the very next page load — it was fetched and the `role` field dropped on the * floor in 4 of its 6 call sites. * * This store is seeded from the claim (so there is no flash of the wrong nav on boot) and * corrected by every `/me/context` response via `setRole`. Read this instead of calling * `getRole()` ad hoc. */ export const role = writable(getRole()); /** True for host and admin — the "can moderate" predicate used across the UI. */ export const isStaff = derived(role, ($role) => $role === 'host' || $role === 'admin'); /** Apply the authoritative role from a `/me/context` response. */ export function setRole(next: Role | null): void { role.set(next); } /** Re-seed from the token, e.g. straight after a login/join that minted a new one. */ export function syncRoleFromToken(): void { role.set(getRole()); } // Bind the store to the identity lifecycle. The store is a module-level singleton seeded // ONCE at import, and `goto()` navigations re-import nothing — so without these hooks the // previous user's role survives a logout→login in the same tab. A host who left and a guest // who then joined kept `isStaff === true` and were offered "Beitrag entfernen" on other // people's photos (the backend 403s it, so it was a false affordance rather than an // escalation — but the feed never fetches /me/context, so it never self-corrected either). // The mirror case is just as wrong: a guest recovering into a host account got no host // affordances at all. onClearAuth(() => role.set(null)); onSetAuth(syncRoleFromToken);