Files
OSGAccountServer/admin-web/src/features/auth/auth-context.tsx
T
Rocky 231c5040a5
CI / verify (push) Has been cancelled
CI / publish (push) Has been cancelled
Add StoreKit history and modernize admin console
Expose ledger-backed cross-device purchase history while shipping the tested React admin redesign in the same reproducible deployment revision.
2026-08-19 22:13:13 +08:00

110 lines
3.0 KiB
TypeScript

import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useState,
type ReactNode,
} from "react";
import { adminApi, ApiError, setCsrfToken } from "../../api/client";
import type { AdminRole, AuthState } from "../../api/types";
interface AuthContextValue {
auth: AuthState;
checking: boolean;
login: (username: string, password: string, totpCode: string) => Promise<void>;
logout: () => Promise<void>;
}
const AuthContext = createContext<AuthContextValue | null>(null);
export function AuthProvider({ children }: { children: ReactNode }) {
const [auth, setAuth] = useState<AuthState>({ status: "anonymous" });
const [checking, setChecking] = useState(true);
const expireSession = useCallback(() => {
setCsrfToken();
setAuth({ status: "anonymous" });
}, []);
useEffect(() => {
let active = true;
void adminApi
.session()
.then((session) => {
if (
active &&
session.authenticated &&
session.operatorName &&
isAdminRole(session.role)
) {
setAuth({
status: "authenticated",
operatorName: session.operatorName,
role: session.role,
});
}
})
.catch((error: unknown) => {
if (!(error instanceof ApiError) || error.status !== 401) {
window.dispatchEvent(
new CustomEvent("admin:toast", {
detail: { message: "暂时无法确认登录状态", tone: "error" },
}),
);
}
})
.finally(() => {
if (active) setChecking(false);
});
window.addEventListener("admin:unauthorized", expireSession);
return () => {
active = false;
window.removeEventListener("admin:unauthorized", expireSession);
};
}, [expireSession]);
const login = useCallback(
async (username: string, password: string, totpCode: string) => {
const response = await adminApi.login(username, password, totpCode);
setCsrfToken(response.csrfToken);
setAuth({
status: "authenticated",
operatorName: response.operatorName,
role: response.role,
});
},
[],
);
const logout = useCallback(async () => {
try {
await adminApi.logout();
} catch {
// 即使服务端退出失败,也立即清理前端认证状态。
} finally {
expireSession();
window.history.replaceState(null, "", window.location.pathname);
}
}, [expireSession]);
const value = useMemo(
() => ({ auth, checking, login, logout }),
[auth, checking, login, logout],
);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export function useAuth(): AuthContextValue {
const context = useContext(AuthContext);
if (!context) throw new Error("useAuth 必须在 AuthProvider 中使用");
return context;
}
function isAdminRole(role?: AdminRole): role is AdminRole {
return role === "SUPER_ADMIN" || role === "SUPPORT" || role === "ANALYST";
}