Consolidate frontend storage keys into constants module
- Move magic strings from AuthProvider, MainLayout, and ThemeProvider to frontend/src/utils/constants.ts - Add STORAGE_KEY_AUTHENTICATED, STORAGE_KEY_SIDEBAR_COLLAPSED, and STORAGE_KEY_THEME constants with JSDoc descriptions - Update all three files to import and use centralized keys - Prevents key drift and typo regressions across the frontend Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1,21 +1,3 @@
|
|||||||
## 21) Silent auth error swallow in fetch error utility
|
|
||||||
- Where found:
|
|
||||||
- [frontend/src/utils/fetchError.ts](frontend/src/utils/fetchError.ts)
|
|
||||||
- Why this is needed:
|
|
||||||
- Silent return can drop actionable errors when no auth handler is registered.
|
|
||||||
- Goal:
|
|
||||||
- Ensure auth errors are handled deterministically with fallback logging/handling.
|
|
||||||
- What to do:
|
|
||||||
- Enforce handler registration or fallback behavior.
|
|
||||||
- Possible traps and issues:
|
|
||||||
- Duplicate redirect flows if multiple auth handlers exist.
|
|
||||||
- Docs changes needed:
|
|
||||||
- Add auth error handling contract for utilities.
|
|
||||||
- Doc references:
|
|
||||||
- [frontend/src/providers/AuthProvider.tsx](frontend/src/providers/AuthProvider.tsx)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 22) Magic strings are scattered in frontend storage keys
|
## 22) Magic strings are scattered in frontend storage keys
|
||||||
- Where found:
|
- Where found:
|
||||||
- [frontend/src/providers/AuthProvider.tsx](frontend/src/providers/AuthProvider.tsx)
|
- [frontend/src/providers/AuthProvider.tsx](frontend/src/providers/AuthProvider.tsx)
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import { useAuth } from "../hooks/useAuth";
|
|||||||
import { useServerStatus } from "../hooks/useServerStatus";
|
import { useServerStatus } from "../hooks/useServerStatus";
|
||||||
import { useBlocklistStatus } from "../hooks/useBlocklistStatus";
|
import { useBlocklistStatus } from "../hooks/useBlocklistStatus";
|
||||||
import { useThemeMode } from "../providers/ThemeProvider";
|
import { useThemeMode } from "../providers/ThemeProvider";
|
||||||
|
import { STORAGE_KEY_SIDEBAR_COLLAPSED } from "../utils/constants";
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Styles
|
// Styles
|
||||||
@@ -43,7 +44,6 @@ import { useThemeMode } from "../providers/ThemeProvider";
|
|||||||
|
|
||||||
const SIDEBAR_FULL = "240px";
|
const SIDEBAR_FULL = "240px";
|
||||||
const SIDEBAR_COLLAPSED = "48px";
|
const SIDEBAR_COLLAPSED = "48px";
|
||||||
const SIDEBAR_COLLAPSED_STORAGE_KEY = "bangui_sidebar_collapsed";
|
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
const useStyles = makeStyles({
|
||||||
root: {
|
root: {
|
||||||
@@ -237,7 +237,7 @@ export function MainLayout(): React.JSX.Element {
|
|||||||
|
|
||||||
const readSavedCollapsed = (): boolean => {
|
const readSavedCollapsed = (): boolean => {
|
||||||
try {
|
try {
|
||||||
const savedValue = localStorage.getItem(SIDEBAR_COLLAPSED_STORAGE_KEY);
|
const savedValue = localStorage.getItem(STORAGE_KEY_SIDEBAR_COLLAPSED);
|
||||||
if (savedValue === "true") {
|
if (savedValue === "true") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -259,14 +259,14 @@ export function MainLayout(): React.JSX.Element {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(SIDEBAR_COLLAPSED_STORAGE_KEY, String(collapsed));
|
localStorage.setItem(STORAGE_KEY_SIDEBAR_COLLAPSED, String(collapsed));
|
||||||
} catch {
|
} catch {
|
||||||
// Local storage may be unavailable in some environments.
|
// Local storage may be unavailable in some environments.
|
||||||
}
|
}
|
||||||
}, [collapsed]);
|
}, [collapsed]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const savedValue = localStorage.getItem(SIDEBAR_COLLAPSED_STORAGE_KEY);
|
const savedValue = localStorage.getItem(STORAGE_KEY_SIDEBAR_COLLAPSED);
|
||||||
if (savedValue !== null) {
|
if (savedValue !== null) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import * as authApi from "../api/auth";
|
import * as authApi from "../api/auth";
|
||||||
import { setUnauthorizedHandler } from "../api/client";
|
import { setUnauthorizedHandler } from "../api/client";
|
||||||
import { setAuthErrorHandler } from "../utils/fetchError";
|
import { setAuthErrorHandler } from "../utils/fetchError";
|
||||||
|
import { STORAGE_KEY_AUTHENTICATED } from "../utils/constants";
|
||||||
import { SessionValidationLoading } from "../components/SessionValidationLoading";
|
import { SessionValidationLoading } from "../components/SessionValidationLoading";
|
||||||
import { useSessionValidation } from "../hooks/useSessionValidation";
|
import { useSessionValidation } from "../hooks/useSessionValidation";
|
||||||
|
|
||||||
@@ -65,8 +66,6 @@ export interface AuthContextValue {
|
|||||||
|
|
||||||
export const AuthContext = createContext<AuthContextValue | null>(null);
|
export const AuthContext = createContext<AuthContextValue | null>(null);
|
||||||
|
|
||||||
const IS_AUTHENTICATED_KEY = "bangui_authenticated";
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Provider
|
// Provider
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -87,14 +86,14 @@ export function AuthProvider({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}): React.JSX.Element {
|
}): React.JSX.Element {
|
||||||
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(() => {
|
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(() => {
|
||||||
const stored = sessionStorage.getItem(IS_AUTHENTICATED_KEY);
|
const stored = sessionStorage.getItem(STORAGE_KEY_AUTHENTICATED);
|
||||||
return stored === "true";
|
return stored === "true";
|
||||||
});
|
});
|
||||||
const [isValidating, setIsValidating] = useState<boolean>(isAuthenticated);
|
const [isValidating, setIsValidating] = useState<boolean>(isAuthenticated);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const handleSessionExpired = useCallback((): void => {
|
const handleSessionExpired = useCallback((): void => {
|
||||||
sessionStorage.removeItem(IS_AUTHENTICATED_KEY);
|
sessionStorage.removeItem(STORAGE_KEY_AUTHENTICATED);
|
||||||
setIsAuthenticated(false);
|
setIsAuthenticated(false);
|
||||||
navigate("/login", { replace: true });
|
navigate("/login", { replace: true });
|
||||||
}, [navigate]);
|
}, [navigate]);
|
||||||
@@ -136,7 +135,7 @@ export function AuthProvider({
|
|||||||
|
|
||||||
const login = useCallback(async (password: string): Promise<void> => {
|
const login = useCallback(async (password: string): Promise<void> => {
|
||||||
await authApi.login(password);
|
await authApi.login(password);
|
||||||
sessionStorage.setItem(IS_AUTHENTICATED_KEY, "true");
|
sessionStorage.setItem(STORAGE_KEY_AUTHENTICATED, "true");
|
||||||
setIsAuthenticated(true);
|
setIsAuthenticated(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -145,7 +144,7 @@ export function AuthProvider({
|
|||||||
await authApi.logout();
|
await authApi.logout();
|
||||||
} finally {
|
} finally {
|
||||||
// Always clear local state even if the API call fails (e.g. expired session).
|
// Always clear local state even if the API call fails (e.g. expired session).
|
||||||
sessionStorage.removeItem(IS_AUTHENTICATED_KEY);
|
sessionStorage.removeItem(STORAGE_KEY_AUTHENTICATED);
|
||||||
setIsAuthenticated(false);
|
setIsAuthenticated(false);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
import { STORAGE_KEY_THEME } from "../utils/constants";
|
||||||
|
|
||||||
type ThemeMode = "light" | "dark";
|
type ThemeMode = "light" | "dark";
|
||||||
|
|
||||||
@@ -10,12 +11,11 @@ interface ThemeProviderContext {
|
|||||||
hasExplicitPreference: boolean;
|
hasExplicitPreference: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const THEME_STORAGE_KEY = "bangui_theme";
|
|
||||||
const ThemeModeContext = createContext<ThemeProviderContext | null>(null);
|
const ThemeModeContext = createContext<ThemeProviderContext | null>(null);
|
||||||
|
|
||||||
function readSavedTheme(): ThemeMode | null {
|
function readSavedTheme(): ThemeMode | null {
|
||||||
try {
|
try {
|
||||||
const value = localStorage.getItem(THEME_STORAGE_KEY);
|
const value = localStorage.getItem(STORAGE_KEY_THEME);
|
||||||
if (value === "dark" || value === "light") {
|
if (value === "dark" || value === "light") {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,7 @@ export function ThemeProvider({ children }: ThemeProviderProps): React.JSX.Eleme
|
|||||||
|
|
||||||
const setColorMode = useCallback((mode: ThemeMode): void => {
|
const setColorMode = useCallback((mode: ThemeMode): void => {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(THEME_STORAGE_KEY, mode);
|
localStorage.setItem(STORAGE_KEY_THEME, mode);
|
||||||
} catch {
|
} catch {
|
||||||
// Ignore storage failures.
|
// Ignore storage failures.
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,10 @@
|
|||||||
*/
|
*/
|
||||||
import type { BanOriginFilter, TimeRange } from "../types/ban";
|
import type { BanOriginFilter, TimeRange } from "../types/ban";
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// UI Constants
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export const BAN_ORIGIN_FILTER_LABELS: Record<BanOriginFilter, string> = {
|
export const BAN_ORIGIN_FILTER_LABELS: Record<BanOriginFilter, string> = {
|
||||||
all: "All",
|
all: "All",
|
||||||
blocklist: "Blocklist",
|
blocklist: "Blocklist",
|
||||||
@@ -21,3 +25,16 @@ export const BAN_PAGE_SIZE = 100 as const;
|
|||||||
|
|
||||||
/** Items per page for the history table. */
|
/** Items per page for the history table. */
|
||||||
export const HISTORY_PAGE_SIZE = 50 as const;
|
export const HISTORY_PAGE_SIZE = 50 as const;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Storage Keys
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** SessionStorage key for authentication state persistence. */
|
||||||
|
export const STORAGE_KEY_AUTHENTICATED = "bangui_authenticated" as const;
|
||||||
|
|
||||||
|
/** LocalStorage key for sidebar collapsed state. */
|
||||||
|
export const STORAGE_KEY_SIDEBAR_COLLAPSED = "bangui_sidebar_collapsed" as const;
|
||||||
|
|
||||||
|
/** LocalStorage key for theme preference. */
|
||||||
|
export const STORAGE_KEY_THEME = "bangui_theme" as const;
|
||||||
|
|||||||
Reference in New Issue
Block a user