import { createContext, useContext, useState, useCallback, useEffect, type ReactNode } from 'react' import { PERMISSIONS_CHANGED_EVENT, SESSION_EXPIRED_EVENT, getToken, onTokenRenewed, setToken, resetSessionExpiredFlag, } from '@/services/request' import { getMe, login as loginApi, refreshSession, updateMyStatus, mePermissions, type AgentPresence, type LoginResult, } from '@/services/api' interface AuthState { user: LoginResult | null loading: boolean permissions: Set permissionsLoaded: boolean login: (username: string, password: string) => Promise logout: () => void /** 切换本人在线 / 忙碌 / 离线 */ setPresence: (status: AgentPresence) => Promise refreshPermissions: () => Promise } const AuthContext = createContext({ user: null, loading: false, permissions: new Set(), permissionsLoaded: false, login: async () => { throw new Error('认证上下文未初始化') }, logout: () => {}, setPresence: async () => {}, refreshPermissions: async () => {}, }) function persistUser(u: LoginResult | null) { if (!u) { localStorage.removeItem('auth_user') return } localStorage.setItem('auth_user', JSON.stringify(u)) } export function AuthProvider({ children }: { children: ReactNode }) { const [user, setUser] = useState(() => { const saved = localStorage.getItem('auth_user') if (saved) { try { const u = JSON.parse(saved) as LoginResult setToken(u.token) return u } catch { return null } } return null }) const [loading, setLoading] = useState(false) const [permissions, setPermissions] = useState>(new Set()) const [permissionsLoaded, setPermissionsLoaded] = useState(false) const refreshPermissions = useCallback(async () => { if (!user?.token) return try { const res = await mePermissions() if (res.data?.permissions) { setPermissions(new Set(res.data.permissions)) } } catch { setPermissions(new Set()) } finally { setPermissionsLoaded(true) } }, [user?.token]) useEffect(() => { const handlePermissionsChanged = () => { void refreshPermissions() } window.addEventListener(PERMISSIONS_CHANGED_EVENT, handlePermissionsChanged) return () => window.removeEventListener(PERMISSIONS_CHANGED_EVENT, handlePermissionsChanged) }, [refreshPermissions]) // 刷新后同步服务端真实状态(含 status + permissions) useEffect(() => { if (!user?.token) return let cancelled = false setPermissionsLoaded(false) Promise.all([getMe(), mePermissions()]) .then(([meRes, permRes]) => { if (cancelled) return if (meRes.data) { setUser(prev => { if (!prev) return prev const next = { ...prev, nickname: meRes.data.nickname || prev.nickname, role: meRes.data.role || prev.role, status: meRes.data.status || prev.status || 'offline', } persistUser(next) return next }) } if (permRes.data?.permissions) { setPermissions(new Set(permRes.data.permissions)) } setPermissionsLoaded(true) }) .catch(() => { if (!cancelled) { setPermissions(new Set()) setPermissionsLoaded(true) } }) return () => { cancelled = true } // eslint-disable-next-line react-hooks/exhaustive-deps }, [user?.token]) const login = useCallback(async (username: string, password: string) => { setLoading(true) try { const res = await loginApi({ username, password }) const u: LoginResult = { ...res.data, status: res.data.status || 'online' } resetSessionExpiredFlag() setToken(u.token) setUser(u) persistUser(u) setPermissionsLoaded(false) try { const permissionRes = await mePermissions() setPermissions(new Set(permissionRes.data?.permissions || [])) } catch { setPermissions(new Set()) } finally { setPermissionsLoaded(true) } return u } finally { setLoading(false) } }, []) // 所有续期入口共用:同步请求模块、React 状态和 localStorage 中的令牌 const applyRenewedToken = useCallback((nextToken: string) => { if (!nextToken) return setToken(nextToken) setUser(prev => { if (!prev || prev.token === nextToken) return prev const updated = { ...prev, token: nextToken } persistUser(updated) return updated }) }, []) // 服务端滑动续期(X-Renewed-Token) useEffect(() => onTokenRenewed(applyRenewedToken), [applyRenewedToken]) // 定时续期:长挂机场景下每 10 分钟主动换新令牌,避免 WS 重连/请求时已过期 useEffect(() => { if (!user?.token) return const sessionToken = user.token const timer = window.setInterval(() => { void refreshSession().then(res => { // 请求期间已退出、重新登录或由其它响应完成续期时,不再应用旧响应。 if (getToken() !== sessionToken) return applyRenewedToken(res.data?.token || '') }).catch(() => { /* 401 已由 request 层统一处理;网络错误等待下一轮 */ }) }, 10 * 60 * 1000) return () => window.clearInterval(timer) }, [applyRenewedToken, user?.token]) // 会话过期(所有 401 的统一出口):清理登录态并回到登录页 useEffect(() => { const handleExpired = () => { setToken('') setUser(null) setPermissions(new Set()) setPermissionsLoaded(false) persistUser(null) if (window.location.pathname !== '/login') { window.location.replace('/login?expired=1') } } window.addEventListener(SESSION_EXPIRED_EVENT, handleExpired) return () => window.removeEventListener(SESSION_EXPIRED_EVENT, handleExpired) }, []) const setPresence = useCallback(async (status: AgentPresence) => { const res = await updateMyStatus(status) const nextStatus = (res.data?.status || status) as string setUser(prev => { if (!prev) return prev const next = { ...prev, status: nextStatus } persistUser(next) return next }) }, []) const logout = useCallback(() => { void updateMyStatus('offline').catch(() => {}) setToken('') setUser(null) setPermissions(new Set()) setPermissionsLoaded(false) persistUser(null) }, []) return ( {children} ) } export const useAuth = () => useContext(AuthContext) export function usePermission(code: string): boolean { const { permissions } = useAuth() return permissions.has(code) }