fix: 2026-06-09 full-codebase audit hardening
Validation: shared NaN-guarded Zod coercion helpers in schemas/common.ts replace the raw number|string transform idiom across every schema (the root-cause NaN bug class); emailOrEmpty + lenient isoDateString/timeString. Security: roles privilege-escalation closed; refresh-token family revocation on reuse; TOTP uses config params; read endpoints permission-guarded; received-invoices gross VAT on all paths; orders-pdf custom-items authz. Concurrency: $queryRaw SELECT...FOR UPDATE locks in ascending-id order (warehouse confirm/cancel, attendance lockUserRow); uniqueness checks moved into create transactions (TOCTOU -> 409); deterministic id tiebreak on second-precision timestamp ordering (plan resolveCell/resolveGrid, warehouse FIFO). Frontend: Rules-of-Hooks fixed across ~14 pages + PlanCellModal; UTC-date persisted fields; dashboard invalidation gaps; stale-closure confirm bugs. Tooling/tests: ESLint flat config (react-hooks/rules-of-hooks = error) + Prettier; tsconfig.test.json so tsc -b type-checks the tests; removed 3 dead deps; npm audit fix (8 -> 3). Suite 195 -> 247 (happy-path auth, FIFO oldest-first, flakiness fixes), isolated on app_test via .env.test with a hard-throw setup guard. Gates: tsc 0 | build 0 | vitest 247/247 | eslint 0 errors. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import {
|
||||
createContext,
|
||||
useContext,
|
||||
useState,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
@@ -12,22 +13,40 @@ interface ThemeContextValue {
|
||||
toggleTheme: () => void;
|
||||
}
|
||||
|
||||
export type ThemeMode = "light" | "dark";
|
||||
|
||||
/**
|
||||
* The first-paint theme fallback used before any stored preference is read.
|
||||
*
|
||||
* SINGLE SOURCE OF TRUTH for the default mode. `MuiProvider`'s
|
||||
* `<ThemeProvider defaultMode="…">` MUST use this same value (import it from
|
||||
* here) — if the two defaults drift, MUI and this context pick different modes
|
||||
* on the first frame and the page/toggle desync until a reload. The two
|
||||
* `localStorage.getItem("mui-mode")` fallbacks below both reference this so
|
||||
* they can never diverge from each other either.
|
||||
*/
|
||||
export const DEFAULT_THEME_MODE: ThemeMode = "dark";
|
||||
|
||||
const ThemeContext = createContext<ThemeContextValue | null>(null);
|
||||
|
||||
function readStoredTheme(): ThemeMode {
|
||||
// Single source of truth = MUI's own mode key. MUI's cssVariables provider
|
||||
// reads `mui-mode` on mount and sets `data-theme` from it, so we read/write
|
||||
// that SAME key (no separate boha-theme) — page and toggle stay in sync
|
||||
// across reloads. Fall back to the legacy key for pre-existing sessions.
|
||||
const stored =
|
||||
localStorage.getItem("mui-mode") || localStorage.getItem("boha-theme");
|
||||
return stored === "light" || stored === "dark" ? stored : DEFAULT_THEME_MODE;
|
||||
}
|
||||
|
||||
type ViewTransitionDocument = Document & {
|
||||
startViewTransition?: (callback: () => void) => unknown;
|
||||
};
|
||||
|
||||
export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||
const [theme, setTheme] = useState(() => {
|
||||
if (typeof window === "undefined") return "dark";
|
||||
// Single source of truth = MUI's own mode key. MUI's cssVariables provider
|
||||
// reads `mui-mode` on mount and sets `data-theme` from it, so we read/write
|
||||
// that SAME key (no separate boha-theme) — page and toggle stay in sync
|
||||
// across reloads. Fall back to the legacy key for pre-existing sessions.
|
||||
const stored =
|
||||
localStorage.getItem("mui-mode") || localStorage.getItem("boha-theme");
|
||||
return stored === "light" || stored === "dark" ? stored : "dark";
|
||||
const [theme, setTheme] = useState<ThemeMode>(() => {
|
||||
if (typeof window === "undefined") return DEFAULT_THEME_MODE;
|
||||
return readStoredTheme();
|
||||
});
|
||||
|
||||
// Apply synchronously (layout effect) so the `data-theme` attribute flips
|
||||
@@ -43,6 +62,22 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||
?.setAttribute("content", themeColor);
|
||||
}, [theme]);
|
||||
|
||||
// Cross-tab sync: when another tab toggles the theme it writes `mui-mode`,
|
||||
// which fires a `storage` event here. Mirror the new value (instant — no
|
||||
// View Transition, since this tab didn't initiate the change) so all open
|
||||
// tabs stay visually consistent without a reload.
|
||||
useEffect(() => {
|
||||
const onStorage = (e: StorageEvent) => {
|
||||
if (e.key !== "mui-mode") return;
|
||||
const next = e.newValue;
|
||||
if (next === "light" || next === "dark") {
|
||||
setTheme((prev) => (prev === next ? prev : next));
|
||||
}
|
||||
};
|
||||
window.addEventListener("storage", onStorage);
|
||||
return () => window.removeEventListener("storage", onStorage);
|
||||
}, []);
|
||||
|
||||
const toggleTheme = () => {
|
||||
const apply = () =>
|
||||
setTheme((prev) => (prev === "dark" ? "light" : "dark"));
|
||||
|
||||
Reference in New Issue
Block a user