Files
ThothII/frontend/src/auth/authState.ts
T
Codex d8a29bfbdd Add full shell, replaceable Omics adapter and bilingual interaction
Implement approved specification #32 and tickets #33-#37. Keep host authentication server-verified and pin session interaction language. Compile scoped base selectors for browser compatibility and retain full gutters during CSS pruning.
2026-09-13 14:26:39 +02:00

88 lines
2.7 KiB
TypeScript

import { useSyncExternalStore } from "react";
import type { AuthenticatedUser } from "../api/types";
import { queryClient } from "../app/queryClient";
import { useSessionStore } from "../store/sessionStore";
import { rememberSession } from "../shell/host/rememberedSession";
let current: AuthenticatedUser | null = null;
let generation = 0;
const listeners = new Set<() => void>();
function notify() {
for (const listener of listeners) listener();
}
function scrubUserBoundState(): void {
queryClient.clear();
useSessionStore.getState().resetSession();
}
/** Authentication is intentionally process-local; no browser storage is involved. */
export function getAuthState(): AuthenticatedUser | null {
return current;
}
export function setAuthState(user: AuthenticatedUser): void {
if (current && (current.issuer !== user.issuer || current.subject !== user.subject)) rememberSession(current, null);
scrubUserBoundState();
current = user;
generation += 1;
notify();
}
/** Refresh expiry/CSRF metadata without discarding the current user's work. */
export function refreshAuthState(user: AuthenticatedUser): void {
if (!current || current.issuer !== user.issuer || current.subject !== user.subject
|| current.isAdmin !== user.isAdmin
|| JSON.stringify([...current.permissions].sort()) !== JSON.stringify([...user.permissions].sort())
|| JSON.stringify([...current.roles].sort()) !== JSON.stringify([...user.roles].sort())) {
setAuthState(user);
return;
}
current = user;
notify();
}
export function clearAuthState(): void {
if (current) rememberSession(current, null);
scrubUserBoundState();
current = null;
generation += 1;
notify();
}
export function isAuthGenerationCurrent(expectedGeneration: number): boolean {
return generation === expectedGeneration;
}
export function clearAuthStateIfCurrent(expectedGeneration: number): boolean {
if (!isAuthGenerationCurrent(expectedGeneration)) return false;
clearAuthState();
return true;
}
export function getAuthGeneration(): number {
return generation;
}
export function subscribeAuthState(listener: () => void): () => void {
listeners.add(listener);
return () => listeners.delete(listener);
}
export function useAuthState(): AuthenticatedUser | null {
return useSyncExternalStore(subscribeAuthState, getAuthState, getAuthState);
}
export function useAuthUser(): AuthenticatedUser | null {
return useAuthState();
}
export function useAuthGeneration(): number {
return useSyncExternalStore(subscribeAuthState, getAuthGeneration, getAuthGeneration);
}
export function hasPermission(user: Pick<AuthenticatedUser, "permissions"> | null | undefined, permission: string): boolean {
return user?.permissions.includes(permission) ?? false;
}