Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 | 5x 5x 5x 5x 84x 84x 84x 24x 24x 30x 30x 30x 30x 84x 84x 30x 84x 30x 30x 30x 30x 84x 30x 24x 24x 24x 24x 30x 30x 30x 30x 84x 51x 33x | "use client";
/**
* Tab-scoped session isolation for live Keycloak mode.
*
* Problem: Keycloak session cookies are origin-scoped — shared across all
* browser tabs. When Tab 2 switches to patient1, Tab 1 (edcadmin) silently
* picks up patient1's cookie on the next NextAuth polling cycle.
*
* Solution: On first authenticated load, snapshot {username, email, roles}
* into sessionStorage (tab-scoped). Subsequent renders use the snapshot
* for display, ignoring cookie changes from other tabs.
*
* The snapshot is refreshed when:
* - The tab navigates through the explicit switchPersona flow, which sets
* "session-switch-pending" in sessionStorage before redirecting.
* - The page loads fresh with no existing snapshot.
*/
import { useSession } from "next-auth/react";
import { useState, useEffect } from "react";
const SNAPSHOT_KEY = "tab-session-snapshot";
const SWITCH_PENDING_KEY = "session-switch-pending";
export interface TabSession {
username: string;
/** Keycloak login name (preferred_username) — used for role derivation. */
preferredUsername: string;
email: string;
roles: string[];
}
/** Mark that this tab is intentionally switching users. */
export function markSessionSwitch(): void {
Eif (typeof sessionStorage !== "undefined") {
sessionStorage.setItem(SWITCH_PENDING_KEY, "true");
}
}
/** Read the snapshot from sessionStorage (sync). */
function readSnapshot(): TabSession | null {
Iif (typeof sessionStorage === "undefined") return null;
const raw = sessionStorage.getItem(SNAPSHOT_KEY);
Eif (!raw) return null;
try {
return JSON.parse(raw) as TabSession;
} catch {
return null;
}
}
/** Write snapshot to sessionStorage. */
function writeSnapshot(snap: TabSession): void {
Eif (typeof sessionStorage !== "undefined") {
sessionStorage.setItem(SNAPSHOT_KEY, JSON.stringify(snap));
}
}
/** Check and clear the switch-pending flag. */
function consumeSwitchPending(): boolean {
Iif (typeof sessionStorage === "undefined") return false;
const pending = sessionStorage.getItem(SWITCH_PENDING_KEY);
Iif (pending) {
sessionStorage.removeItem(SWITCH_PENDING_KEY);
return true;
}
return false;
}
/**
* React hook — returns a tab-scoped session that is immune to cross-tab
* cookie changes.
*
* In live mode (IS_STATIC=false), this should replace direct useSession()
* calls in Navigation.tsx and UserMenu.tsx.
*
* Returns { session, status } similar to useSession() but the session
* data comes from a tab-scoped snapshot once established.
*/
export function useTabSession(): {
session: TabSession | null;
status: "loading" | "authenticated" | "unauthenticated";
liveSession: ReturnType<typeof useSession>["data"];
} {
const { data: liveSession, status: liveStatus } = useSession();
const [tabSession, setTabSession] = useState<TabSession | null>(() =>
readSnapshot(),
);
useEffect(() => {
// On mount, check if a switch was pending (user explicitly switched in THIS tab)
const switchPending = consumeSwitchPending();
Iif (switchPending) {
// Clear old snapshot — will be replaced by the new session below
sessionStorage.removeItem(SNAPSHOT_KEY);
setTabSession(null);
return;
}
// SSR hydration fix: useState initializer ran on the server where
// sessionStorage is unavailable, so it returned null. On the client
// the snapshot may already exist — load it now.
const snap = readSnapshot();
Iif (snap) setTabSession(snap);
}, []);
useEffect(() => {
if (liveStatus !== "authenticated" || !liveSession) return;
const existing = readSnapshot();
Iif (existing) {
// Snapshot exists — ensure state is in sync (covers SSR hydration gap)
setTabSession((prev) => prev ?? existing);
return;
}
// No snapshot yet (first load or after switch) — take the live session
const sessionData = liveSession as {
roles?: string[];
user?: { name?: string; email?: string; preferredUsername?: string };
};
const roles = sessionData.roles ?? [];
const preferredUsername =
sessionData.user?.preferredUsername ?? liveSession.user?.name ?? "";
const snap: TabSession = {
username: liveSession.user?.name ?? liveSession.user?.email ?? "",
preferredUsername,
email: liveSession.user?.email ?? "",
roles: [...roles],
};
writeSnapshot(snap);
setTabSession(snap);
}, [liveSession, liveStatus]);
// If we have a snapshot, use it; otherwise fall through to live status
if (tabSession) {
return {
session: tabSession,
status: "authenticated",
liveSession,
};
}
return {
session: null,
status: liveStatus,
liveSession,
};
}
|