rename web/ to admin/, along with the web-named build and cli identifiers
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode } from "react";
|
||||
import { ApiError, login as apiLogin, logout as apiLogout } from "@/lib/api";
|
||||
import type { LoginResponse } from "@/lib/types";
|
||||
|
||||
const STORAGE_KEY = "nxdns_auth_required";
|
||||
|
||||
export function readStoredAuthRequired(): boolean | null {
|
||||
try {
|
||||
const raw = sessionStorage.getItem(STORAGE_KEY);
|
||||
return raw === null ? null : raw === "true";
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function rememberAuthRequired(value: boolean): void {
|
||||
try {
|
||||
sessionStorage.setItem(STORAGE_KEY, String(value));
|
||||
} catch {
|
||||
// Storage unavailable; the probe will run again next load.
|
||||
}
|
||||
}
|
||||
|
||||
// Deduped across StrictMode double-effects: one empty-password login answers
|
||||
// whether auth is on (401 → on; 200 with auth_required=false → off).
|
||||
let probePromise: Promise<boolean> | null = null;
|
||||
|
||||
function probeAuthRequired(): Promise<boolean> {
|
||||
probePromise ??= apiLogin({ password: "" }).then(
|
||||
(response) => {
|
||||
rememberAuthRequired(response.auth_required);
|
||||
return response.auth_required;
|
||||
},
|
||||
(error: unknown) => {
|
||||
probePromise = null;
|
||||
if (error instanceof ApiError && error.status === 401) {
|
||||
rememberAuthRequired(true);
|
||||
return true;
|
||||
}
|
||||
throw error;
|
||||
},
|
||||
);
|
||||
return probePromise;
|
||||
}
|
||||
|
||||
export function resetAuthProbeForTests(): void {
|
||||
probePromise = null;
|
||||
}
|
||||
|
||||
export interface AuthStore {
|
||||
/** null until a login response, a probe, or a stored value settles it. */
|
||||
authRequired: boolean | null;
|
||||
/** Resolves true when a password is required (form must be shown). */
|
||||
probe: () => Promise<boolean>;
|
||||
login: (password: string) => Promise<LoginResponse>;
|
||||
/** Ends the session server-side; swallows an already-dead session's 401. */
|
||||
logout: () => Promise<void>;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthStore | null>(null);
|
||||
|
||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const [authRequired, setAuthRequired] = useState<boolean | null>(readStoredAuthRequired);
|
||||
|
||||
const probe = useCallback(async () => {
|
||||
const required = await probeAuthRequired();
|
||||
setAuthRequired(required);
|
||||
return required;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (authRequired !== null) return;
|
||||
probe().catch(() => {
|
||||
// Server unreachable; LoginPage's own probe surfaces the error.
|
||||
});
|
||||
}, [authRequired, probe]);
|
||||
|
||||
const login = useCallback(async (password: string) => {
|
||||
const response = await apiLogin({ password });
|
||||
rememberAuthRequired(response.auth_required);
|
||||
setAuthRequired(response.auth_required);
|
||||
return response;
|
||||
}, []);
|
||||
|
||||
const logout = useCallback(async () => {
|
||||
try {
|
||||
await apiLogout();
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError && error.status === 401) return;
|
||||
throw error;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const value = useMemo<AuthStore>(
|
||||
() => ({ authRequired, probe, login, logout }),
|
||||
[authRequired, probe, login, logout],
|
||||
);
|
||||
return <AuthContext value={value}>{children}</AuthContext>;
|
||||
}
|
||||
|
||||
export function useAuth(): AuthStore {
|
||||
const store = useContext(AuthContext);
|
||||
if (store === null) throw new Error("useAuth requires an AuthProvider");
|
||||
return store;
|
||||
}
|
||||
Reference in New Issue
Block a user