fix(auth): bind the role store to the identity, not to the tab
The role store I added in the moderation work is a module-level singleton seeded ONCE at import. `goto()` is a client-side navigation, so leaving and re-joining in the same tab re-imports no module and re-runs no onMount — the previous user's role simply stayed resident. Nothing reset it: not join, recover, admin login, "Event verlassen", `clearAuth`, nor the api.ts 401 auto-clear. So a host who left, followed by a guest joining on the same phone, left that guest with `isStaff === true` and a "🚫 Beitrag entfernen" action on other people's photos. The backend 403s the delete, so this was a false affordance rather than a privilege escalation — but `/feed` never fetched `/me/context`, so unlike every other route it never self-corrected either. It survived until a hard reload. The mirror case was equally broken and easier to overlook: a guest who recovered into a host account got NO host affordances. `clearAuth` already had a hook registry for exactly this shape of problem, with a comment explaining it exists to avoid circular imports. Add the missing mirror, `onSetAuth`, fired by both `setAuth` and `setAdminAuth` after the new token is resident, and have the role store register on both sides: clear to null on logout, re-seed from the new token on login. That also gives `syncRoleFromToken` — dead code with zero callers since I introduced it — its intended purpose. Seeding from the claim fixes the reported bug, but the claim is frozen for the token's 30-day life, so a promotion or demotion still wouldn't reach the feed. `/feed` now calls the existing `refreshEventState()` on mount, which fetches `/me/context` and applies both the authoritative role and the lock/release state in one request. The feed is the one route gating a destructive action on the role, so it should not be the only route running on a stale claim. Tests: 04-host/role-identity-reset drives the real flows. The first asserts the host DOES see the action before asserting the newcomer does not — a negative assertion alone would pass against a build that shipped no moderation at all. The second covers the mirror, promoting a guest server-side while their resident token still claims `role: guest`, so a fix that only cleared the role would fail it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -86,6 +86,7 @@ export function setAuth(
|
||||
localStorage.setItem(USER_ID_KEY, userId);
|
||||
if (displayName) localStorage.setItem(DISPLAY_NAME_KEY, displayName);
|
||||
isAuthenticated.set(true);
|
||||
fireSetAuthHooks();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -106,6 +107,7 @@ export function setAdminAuth(jwt: string, userId: string, displayName?: string):
|
||||
sessionStorage.setItem(USER_ID_KEY, userId);
|
||||
if (displayName) sessionStorage.setItem(DISPLAY_NAME_KEY, displayName);
|
||||
isAuthenticated.set(true);
|
||||
fireSetAuthHooks();
|
||||
}
|
||||
|
||||
// Hook registry: cross-cutting stores (export-status, etc.) register a callback
|
||||
@@ -118,6 +120,26 @@ export function onClearAuth(fn: () => void): void {
|
||||
clearAuthHooks.push(fn);
|
||||
}
|
||||
|
||||
// The mirror of `onClearAuth`, for stores that must be RE-SEEDED when a new identity
|
||||
// arrives rather than merely cleared. Without it, anything derived from the token
|
||||
// survives a logout→login in the same tab: `goto()` is a client-side navigation, so no
|
||||
// module is re-imported and no `onMount` re-runs, and the previous user's value simply
|
||||
// stays. Fires after the new token is resident, so hooks can read it.
|
||||
const setAuthHooks: Array<() => void> = [];
|
||||
export function onSetAuth(fn: () => void): void {
|
||||
setAuthHooks.push(fn);
|
||||
}
|
||||
|
||||
function fireSetAuthHooks(): void {
|
||||
for (const fn of setAuthHooks) {
|
||||
try {
|
||||
fn();
|
||||
} catch {
|
||||
/* hook failure is non-fatal */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function clearAuth(): void {
|
||||
if (!browser) return;
|
||||
// Clear from BOTH stores — a guest token lives in localStorage, an admin token in
|
||||
|
||||
Reference in New Issue
Block a user