Build and Push Docker Images / Build and Push Docker Images (push) Failing after 9s
80 lines
2.9 KiB
TypeScript
80 lines
2.9 KiB
TypeScript
import { atomWithStorage } from "jotai/utils";
|
|
import { store } from "./atoms";
|
|
import type { AuthUser } from "./types/user";
|
|
|
|
/**
|
|
* Client-side auth store (replaces the old next-auth session).
|
|
*
|
|
* The Knox token + user returned by `POST /callback/auth/` are persisted to
|
|
* localStorage via `atomWithStorage`, so they survive reloads and stay reactive
|
|
* for components (`userAtom` drives the header wallet balance, welcome banner,
|
|
* and sidebar admin/permission gating).
|
|
*
|
|
* Both the reactive atoms and the non-React helpers below operate on the SAME
|
|
* shared jotai `store` (from ./atoms, which the app <Provider> is bound to), so
|
|
* a write from the axios interceptor (e.g. `clearAuth()` on 401) is immediately
|
|
* visible to React components — no split-brain between localStorage and state.
|
|
*/
|
|
|
|
export const TOKEN_KEY = "sarlink_token";
|
|
export const USER_KEY = "sarlink_user";
|
|
|
|
// `getOnInit: true` is REQUIRED here. Without it, jotai defers reading
|
|
// localStorage until the atom is mounted by a React component, so the
|
|
// synchronous `store.get()` calls in `getToken()` / `isAuthenticated()`
|
|
// (used by RouteGuard, RootRedirect, and the axios interceptor during the
|
|
// first render on a page refresh) would see the initial `null` instead of the
|
|
// persisted token — bouncing an already-authenticated user to signin on reload.
|
|
export const tokenAtom = atomWithStorage<string | null>(TOKEN_KEY, null, undefined, {
|
|
getOnInit: true,
|
|
});
|
|
export const userAtom = atomWithStorage<AuthUser | null>(USER_KEY, null, undefined, {
|
|
getOnInit: true,
|
|
});
|
|
|
|
/** Read the raw token (used by the axios request interceptor). */
|
|
export function getToken(): string | null {
|
|
return store.get(tokenAtom);
|
|
}
|
|
|
|
/** Read the stored user without a React hook. */
|
|
export function getStoredUser(): AuthUser | null {
|
|
return store.get(userAtom);
|
|
}
|
|
|
|
/**
|
|
* Single source of truth for "is this user an administrator?" on the client.
|
|
* Mirrors the backend `user_is_admin` helper: any of the app-specific
|
|
* `is_admin` flag, Django `is_staff`, or `is_superuser` grants admin access.
|
|
* Use this for every admin route/UI gate instead of checking `is_admin` alone.
|
|
*/
|
|
export function isAdminUser(user: AuthUser | null | undefined): boolean {
|
|
return Boolean(user && (user.is_admin || user.is_staff || user.is_superuser));
|
|
}
|
|
|
|
/** Persist token + user on successful login. */
|
|
export function setAuth(token: string, user: AuthUser): void {
|
|
store.set(tokenAtom, token);
|
|
store.set(userAtom, user);
|
|
}
|
|
|
|
/** Clear token + user on logout / 401. */
|
|
export function clearAuth(): void {
|
|
store.set(tokenAtom, null);
|
|
store.set(userAtom, null);
|
|
}
|
|
|
|
/**
|
|
* True when a token is present and, if we know its expiry, it hasn't passed.
|
|
* Used by RouteGuard and the root redirect.
|
|
*/
|
|
export function isAuthenticated(): boolean {
|
|
const token = getToken();
|
|
if (!token) return false;
|
|
const user = getStoredUser();
|
|
if (user?.expiry) {
|
|
return new Date(user.expiry).getTime() > Date.now();
|
|
}
|
|
return true;
|
|
}
|