AuthProvider.tsx 6.38 KB
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[];
  loading: boolean;
};

type AuthContextValue = AuthState & {
  refresh: () => Promise<void>;
  clearLocalAuth: () => void;
  logout: () => Promise<void>;
};

const AuthContext = React.createContext<AuthContextValue | null>(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<AuthState>(() => ({
    token: getAccessToken(),
    user: null,
    roleDisplay: null,
    menus: [],
    permissionCodes: [],
    roleCodes: [],
    loading: false,
  }));

  const refresh = React.useCallback(async () => {
    const token = getAccessToken();
    if (!token) {
      setState((s) => ({
        ...s,
        token: null,
        user: null,
        roleDisplay: null,
        menus: [],
        permissionCodes: [],
        roleCodes: [],
        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 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,
        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: [],
        }));
      } 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: [],
    }));
  }, []);

  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 <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}