import React from "react"; import { toast } from "sonner"; import { ApiError } from "../../lib/apiClient"; import { clearTokens, getAccessToken } from "../../lib/authStorage"; import type { CurrentUserMenuNodeDto, CurrentUserBriefDto } from "../../types/authSession"; import { getMyMenus, logout as logoutApi } from "../../services/authSessionService"; export type AuthState = { token: string | null; user: CurrentUserBriefDto | null; /** `/auth-session/my-menus` 根级 `role`:人类可读角色名,用于顶栏/欢迎语等 */ roleDisplay: string | null; menus: CurrentUserMenuNodeDto[]; permissionCodes: string[]; roleCodes: string[]; /** 角色勾选的访问权限(manage_people 等) */ roleAccessPermissionCodes: string[]; loading: boolean; }; type AuthContextValue = AuthState & { refresh: () => Promise; clearLocalAuth: () => void; logout: () => Promise; }; const AuthContext = React.createContext(null); export function useAuth() { const ctx = React.useContext(AuthContext); if (!ctx) throw new Error("useAuth must be used within AuthProvider"); return ctx; } function normalizeId(x: any): string { return String(x?.id ?? x?.Id ?? ""); } function normalizeMenuNode(n: any): CurrentUserMenuNodeDto { const childrenRaw = n?.children ?? n?.Children; const children = Array.isArray(childrenRaw) ? childrenRaw.map(normalizeMenuNode) : undefined; return { id: normalizeId(n), parentId: (n?.parentId ?? n?.ParentId ?? null) as any, menuName: (n?.menuName ?? n?.MenuName ?? null) as any, routerName: (n?.routerName ?? n?.RouterName ?? null) as any, // doc: rbac-menu 新增 router;平台端历史字段为 routeUrl routeUrl: (n?.routeUrl ?? n?.RouteUrl ?? n?.router ?? n?.Router ?? null) as any, menuIcon: (n?.menuIcon ?? n?.MenuIcon ?? null) as any, menuType: (n?.menuType ?? n?.MenuType ?? null) as any, permissionCode: (n?.permissionCode ?? n?.PermissionCode ?? null) as any, orderNum: (n?.orderNum ?? n?.OrderNum ?? null) as any, state: (n?.state ?? n?.State ?? null) as any, component: (n?.component ?? n?.Component ?? null) as any, isCache: (n?.isCache ?? n?.IsCache ?? null) as any, isShow: (n?.isShow ?? n?.IsShow ?? null) as any, query: (n?.query ?? n?.Query ?? null) as any, remark: (n?.remark ?? n?.Remark ?? null) as any, link: (n?.link ?? n?.Link ?? null) as any, menuSource: (n?.menuSource ?? n?.MenuSource ?? null) as any, concurrencyStamp: (n?.concurrencyStamp ?? n?.ConcurrencyStamp ?? null) as any, children, }; } export function AuthProvider({ children }: { children: React.ReactNode }) { const [state, setState] = React.useState(() => ({ token: getAccessToken(), user: null, roleDisplay: null, menus: [], permissionCodes: [], roleCodes: [], roleAccessPermissionCodes: [], loading: false, })); const refresh = React.useCallback(async () => { const token = getAccessToken(); if (!token) { setState((s) => ({ ...s, token: null, user: null, roleDisplay: null, menus: [], permissionCodes: [], roleCodes: [], roleAccessPermissionCodes: [], loading: false, })); return; } setState((s) => ({ ...s, token, loading: true })); try { const res = await getMyMenus(); const menus = Array.isArray((res as any)?.menus) ? (res as any).menus.map(normalizeMenuNode) : []; const permissionCodes = Array.isArray((res as any)?.permissionCodes) ? (res as any).permissionCodes : []; const roleCodes = Array.isArray((res as any)?.roleCodes) ? (res as any).roleCodes : []; const roleAccessRaw = (res as any)?.accessPermissionCodes ?? (res as any)?.AccessPermissionCodes ?? []; const roleAccessPermissionCodes = Array.isArray(roleAccessRaw) ? roleAccessRaw.map((x: unknown) => String(x ?? "").trim()).filter(Boolean) : []; const u0 = (res as any)?.user ?? null; const roleRaw = (res as any)?.role ?? (res as any)?.Role ?? null; const roleDisplay = roleRaw != null && String(roleRaw).trim() ? String(roleRaw).trim() : null; // 后端常把 fullName / lastUpdated 放在与 user 同级(与抓包 data 下结构一致),需与 user 内字段合并 const rootFullName = (res as any)?.fullName ?? (res as any)?.FullName ?? null; const rootLastUpdated = (res as any)?.lastUpdated ?? (res as any)?.LastUpdated ?? null; const user: CurrentUserBriefDto | null = u0 ? { id: String(u0.id ?? u0.Id ?? ""), userName: String(u0.userName ?? u0.UserName ?? ""), fullName: (u0.fullName ?? u0.FullName ?? rootFullName ?? null) as any, nick: (u0.nick ?? u0.Nick ?? null) as any, email: (u0.email ?? u0.Email ?? null) as any, icon: (u0.icon ?? u0.Icon ?? null) as any, lastUpdated: (u0.lastUpdated ?? u0.LastUpdated ?? rootLastUpdated ?? null) as any, } : null; setState((s) => ({ ...s, token, user, roleDisplay, menus, permissionCodes, roleCodes, roleAccessPermissionCodes, loading: false, })); } catch (e: unknown) { const isApi = e instanceof ApiError; const status = isApi ? e.status : 0; const isAuthFailure = isApi && (status === 401 || status === 403); const description = e instanceof Error ? e.message : typeof e === "string" ? e : "Please try again later."; toast.error("Failed to load menu permissions", { description }); if (isAuthFailure) { clearTokens(); setState((s) => ({ ...s, loading: false, token: null, user: null, roleDisplay: null, menus: [], permissionCodes: [], roleCodes: [], roleAccessPermissionCodes: [], })); } else { // 500/网络错误等:保留 token,不退出;menus 为空时 Sidebar 使用静态 fallback setState((s) => ({ ...s, loading: false })); } } }, []); React.useEffect(() => { refresh(); }, [refresh]); const clearLocalAuth = React.useCallback(() => { clearTokens(); setState((s) => ({ ...s, token: null, user: null, roleDisplay: null, menus: [], permissionCodes: [], roleCodes: [], roleAccessPermissionCodes: [], })); }, []); const logout = React.useCallback(async () => { try { await logoutApi(); } catch { // ignore: server-side cache clear best-effort } finally { clearLocalAuth(); toast.success("Logged out"); } }, [clearLocalAuth]); const value: AuthContextValue = React.useMemo( () => ({ ...state, refresh, clearLocalAuth, logout, }), [state, refresh, clearLocalAuth, logout], ); return {children}; }