feat(auth): add remembered local login to the frontend

This commit is contained in:
2026-08-17 04:52:39 +02:00
parent 8c67cb75dc
commit 202822f3ba
36 changed files with 2649 additions and 163 deletions
+71
View File
@@ -0,0 +1,71 @@
import { useSyncExternalStore } from "react";
import type { AuthenticatedUser } from "../api/types";
import { queryClient } from "../app/queryClient";
import { useSessionStore } from "../store/sessionStore";
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 {
scrubUserBoundState();
current = user;
generation += 1;
notify();
}
export function clearAuthState(): void {
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;
}