'use client' import React, { createContext, useContext, useEffect, useState, ReactNode } from 'react' import { AuthState, User, LoginCredentials, RegisterData, JWTPayload } from '@/types/user' import { useMutation, gql, useQuery, ApolloClient } from '@apollo/client' interface UserContextType extends AuthState { login: (credentials: LoginCredentials) => Promise register: (data: RegisterData) => Promise logout: () => void refreshToken: () => Promise resetApolloCache: () => void refetchUser: () => Promise } const UserContext = createContext(undefined) interface UserProviderProps { children: ReactNode } const TOKEN_KEY = 'auth_token' const GET_USER_QUERY = gql` query GetUser { currentUser { id username email } } ` const REGISTER_MUTATION = gql` mutation Register($username: String!, $password: String!) { register(username: $username, password: $password) { token } } ` // 全局 Apollo Client 实例引用 let globalApolloClient: ApolloClient | null = null; export function setGlobalApolloClient(client: ApolloClient) { globalApolloClient = client; } export function UserProvider({ children }: UserProviderProps) { const [authState, setAuthState] = useState({ user: null, token: null, isAuthenticated: false, isLoading: true }) const [registerMutation, { loading: registerLoading, error: registerError }] = useMutation(REGISTER_MUTATION) // 定期查询用户信息的 hook const { data: userData, loading: userLoading, error: userError, refetch: refetchUser } = useQuery(GET_USER_QUERY, { skip: !authState.isAuthenticated, pollInterval: 30000, // 每30秒查询一次 errorPolicy: 'all', notifyOnNetworkStatusChange: true }) const parseJWT = (token: string): JWTPayload | null => { try { const base64Url = token.split('.')[1] const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/') const jsonPayload = decodeURIComponent( atob(base64) .split('') .map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)) .join('') ) return JSON.parse(jsonPayload) } catch (error) { console.error('Failed to parse JWT:', error) return null } } const isTokenValid = (token: string): boolean => { const payload = parseJWT(token) if (!payload) return false const currentTime = Date.now() / 1000 return payload.exp > currentTime } const resetApolloCache = () => { if (globalApolloClient) { globalApolloClient.resetStore(); } } const initializeAuth = async () => { try { const token = localStorage.getItem(TOKEN_KEY) if (token && isTokenValid(token)) { const payload = parseJWT(token) if (payload) { const res = await fetch('/api/session/sync', { method: 'POST', body: JSON.stringify({ jwt: token }) }) if (!res.ok) { throw new Error('Failed to sync session') } const { permissionPairs } = await res.json() const user: User = { id: payload.sub, email: payload.email, name: payload.name, permissionPairs, } setAuthState({ user, token, isAuthenticated: true, isLoading: false }) return } } localStorage.removeItem(TOKEN_KEY) setAuthState({ user: null, token: null, isAuthenticated: false, isLoading: false }) } catch (error) { console.error('Auth initialization error:', error) setAuthState({ user: null, token: null, isAuthenticated: false, isLoading: false }) } } const login = async (credentials: LoginCredentials) => { try { setAuthState(prev => ({ ...prev, isLoading: true })) const response = await fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(credentials) }) const { token, ok } = await response.json() if (!ok) { throw new Error('Login failed') } if (!isTokenValid(token)) { throw new Error('Invalid token received') } const payload = parseJWT(token) if (!payload) { throw new Error('Failed to parse token') } const res = await fetch('/api/session/sync', { method: 'POST', body: JSON.stringify({ jwt: token }) }) const { permissionPairs } = await res.json() const user: User = { id: payload.sub, email: payload.email, name: payload.name, permissionPairs, } localStorage.setItem(TOKEN_KEY, token) setAuthState({ user, token, isAuthenticated: true, isLoading: false }) } catch (error) { console.error('Login error:', error) setAuthState({ user: null, token: null, isAuthenticated: false, isLoading: false }) throw error } } const register = async (data: RegisterData) => { try { setAuthState(prev => ({ ...prev, isLoading: true })) const response = await registerMutation({ variables: data }) if (registerError) { throw new Error('Registration failed') } const { token } = response.data.register if (!isTokenValid(token)) { throw new Error('Invalid token received') } const payload = parseJWT(token) if (!payload) { throw new Error('Failed to parse token') } const user: User = { id: payload.sub, email: payload.email, name: payload.name, } localStorage.setItem(TOKEN_KEY, token) setAuthState({ user, token, isAuthenticated: true, isLoading: false }) // 注册成功后重置 Apollo 缓存 resetApolloCache() } catch (error) { console.error('Registration error:', error) setAuthState({ user: null, token: null, isAuthenticated: false, isLoading: false }) throw error } } const logout = () => { localStorage.removeItem(TOKEN_KEY) setAuthState({ user: null, token: null, isAuthenticated: false, isLoading: false }) // 登出后重置 Apollo 缓存 resetApolloCache() } const refreshToken = async () => { try { const currentToken = authState.token if (!currentToken) { throw new Error('No token to refresh') } const response = await fetch('/api/auth/refresh', { method: 'POST', headers: { 'Authorization': `Bearer ${currentToken}`, 'Content-Type': 'application/json' } }) if (!response.ok) { throw new Error('Token refresh failed') } const { token } = await response.json() if (!isTokenValid(token)) { throw new Error('Invalid refreshed token') } const payload = parseJWT(token) if (!payload) { throw new Error('Failed to parse refreshed token') } const user: User = { id: payload.sub, email: payload.email, name: payload.name, } localStorage.setItem(TOKEN_KEY, token) setAuthState({ user, token, isAuthenticated: true, isLoading: false }) // Token 刷新后重置 Apollo 缓存 resetApolloCache() } catch (error) { console.error('Token refresh error:', error) logout() throw error } } // 更新用户信息的函数 const updateUserInfo = (userData: any) => { if (userData?.currentUser) { const updatedUser: User = { id: userData.currentUser.id, email: userData.currentUser.email, name: userData.currentUser.username, avatar: userData.currentUser.avatar, } setAuthState(prev => ({ ...prev, user: updatedUser })) } } useEffect(() => { initializeAuth() }, []) // 监听登录和注册的 loading 状态 useEffect(() => { setAuthState(prev => ({ ...prev, isLoading: registerLoading || userLoading })) }, [registerLoading, userLoading]) // 监听用户数据变化,定期更新用户信息 useEffect(() => { if (userData && authState.isAuthenticated) { console.log('userData', userData) updateUserInfo(userData) } }, [userData, authState.isAuthenticated]) // 处理用户查询错误 useEffect(() => { if (userError && authState.isAuthenticated) { console.error('User data fetch error:', userError) // 如果用户查询失败,可能是token过期,尝试刷新token if (userError.message.includes('Unauthorized') || userError.message.includes('401')) { refreshToken().catch(() => { logout() }) } } }, [userError, authState.isAuthenticated]) useEffect(() => { if (authState.token && authState.isAuthenticated) { const payload = parseJWT(authState.token) if (payload) { const timeUntilExpiry = (payload.exp * 1000) - Date.now() const refreshTime = Math.max(timeUntilExpiry - 5 * 60 * 1000, 30 * 1000) const refreshTimer = setTimeout(() => { refreshToken().catch(() => { logout() }) }, refreshTime) return () => clearTimeout(refreshTimer) } } }, [authState.token, authState.isAuthenticated]) const value: UserContextType = { ...authState, login, register, logout, refreshToken, resetApolloCache, refetchUser } return ( {children} ) } export function useUser() { const context = useContext(UserContext) if (context === undefined) { throw new Error('useUser must be used within a UserProvider') } return context } export function useAuth() { return useUser() }