2025-03-29 16:50:39 +08:00
|
|
|
|
"use client"
|
|
|
|
|
|
|
|
|
|
|
|
import { createContext, useContext, useEffect, useState, type ReactNode } from "react"
|
|
|
|
|
|
import { useRouter } from "next/navigation"
|
2025-03-29 17:02:40 +08:00
|
|
|
|
import { validateToken } from "@/lib/api"
|
|
|
|
|
|
|
|
|
|
|
|
// 安全的localStorage访问方法
|
|
|
|
|
|
const safeLocalStorage = {
|
|
|
|
|
|
getItem: (key: string): string | null => {
|
|
|
|
|
|
if (typeof window !== 'undefined') {
|
|
|
|
|
|
return localStorage.getItem(key)
|
|
|
|
|
|
}
|
|
|
|
|
|
return null
|
|
|
|
|
|
},
|
|
|
|
|
|
setItem: (key: string, value: string): void => {
|
|
|
|
|
|
if (typeof window !== 'undefined') {
|
|
|
|
|
|
localStorage.setItem(key, value)
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
removeItem: (key: string): void => {
|
|
|
|
|
|
if (typeof window !== 'undefined') {
|
|
|
|
|
|
localStorage.removeItem(key)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface User {
|
|
|
|
|
|
id: number;
|
|
|
|
|
|
username: string;
|
|
|
|
|
|
account?: string;
|
|
|
|
|
|
avatar?: string;
|
|
|
|
|
|
}
|
2025-03-29 16:50:39 +08:00
|
|
|
|
|
|
|
|
|
|
interface AuthContextType {
|
|
|
|
|
|
isAuthenticated: boolean
|
|
|
|
|
|
token: string | null
|
2025-03-29 17:02:40 +08:00
|
|
|
|
user: User | null
|
|
|
|
|
|
login: (token: string, userData: User) => void
|
2025-03-29 16:50:39 +08:00
|
|
|
|
logout: () => void
|
2025-03-29 17:02:40 +08:00
|
|
|
|
updateToken: (newToken: string) => void
|
2025-03-29 16:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-31 12:04:59 +08:00
|
|
|
|
// 创建默认上下文
|
2025-03-29 16:50:39 +08:00
|
|
|
|
const AuthContext = createContext<AuthContextType>({
|
|
|
|
|
|
isAuthenticated: false,
|
|
|
|
|
|
token: null,
|
2025-03-29 17:02:40 +08:00
|
|
|
|
user: null,
|
2025-03-29 16:50:39 +08:00
|
|
|
|
login: () => {},
|
|
|
|
|
|
logout: () => {},
|
2025-03-29 17:02:40 +08:00
|
|
|
|
updateToken: () => {}
|
2025-03-29 16:50:39 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
export const useAuth = () => useContext(AuthContext)
|
|
|
|
|
|
|
|
|
|
|
|
interface AuthProviderProps {
|
|
|
|
|
|
children: ReactNode
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function AuthProvider({ children }: AuthProviderProps) {
|
2025-03-31 12:04:59 +08:00
|
|
|
|
// 避免在服务端渲染时设置初始状态
|
2025-03-29 16:50:39 +08:00
|
|
|
|
const [token, setToken] = useState<string | null>(null)
|
2025-03-29 17:02:40 +08:00
|
|
|
|
const [user, setUser] = useState<User | null>(null)
|
2025-03-29 16:50:39 +08:00
|
|
|
|
const [isAuthenticated, setIsAuthenticated] = useState(false)
|
2025-03-31 12:04:59 +08:00
|
|
|
|
// 初始页面加载时显示为false,避免在服务端渲染和客户端水合时不匹配
|
|
|
|
|
|
const [isLoading, setIsLoading] = useState(false)
|
|
|
|
|
|
const [isInitialized, setIsInitialized] = useState(false)
|
2025-03-29 16:50:39 +08:00
|
|
|
|
const router = useRouter()
|
|
|
|
|
|
|
2025-03-31 12:04:59 +08:00
|
|
|
|
// 初始化认证状态
|
2025-03-29 16:50:39 +08:00
|
|
|
|
useEffect(() => {
|
2025-03-31 12:04:59 +08:00
|
|
|
|
// 仅在客户端执行初始化
|
|
|
|
|
|
setIsLoading(true)
|
|
|
|
|
|
|
2025-03-29 17:02:40 +08:00
|
|
|
|
const initAuth = async () => {
|
2025-03-31 12:04:59 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const storedToken = safeLocalStorage.getItem("token")
|
|
|
|
|
|
|
|
|
|
|
|
if (storedToken) {
|
2025-05-08 14:51:22 +08:00
|
|
|
|
// 首先尝试从localStorage获取用户信息
|
2025-05-08 15:56:07 +08:00
|
|
|
|
const userDataStr = safeLocalStorage.getItem("userInfo")
|
2025-05-08 14:51:22 +08:00
|
|
|
|
if (userDataStr) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 如果能解析用户数据,先设置登录状态
|
2025-03-29 17:02:40 +08:00
|
|
|
|
const userData = JSON.parse(userDataStr) as User
|
|
|
|
|
|
setToken(storedToken)
|
|
|
|
|
|
setUser(userData)
|
|
|
|
|
|
setIsAuthenticated(true)
|
2025-05-08 14:51:22 +08:00
|
|
|
|
|
|
|
|
|
|
// 然后在后台尝试验证token,但不影响当前登录状态
|
|
|
|
|
|
validateToken().then(isValid => {
|
|
|
|
|
|
// 只有在确认token绝对无效时才登出
|
|
|
|
|
|
// 网络错误等情况默认保持登录状态
|
|
|
|
|
|
if (isValid === false) {
|
|
|
|
|
|
console.warn('验证token失败,但仍允许用户保持登录状态')
|
|
|
|
|
|
}
|
|
|
|
|
|
}).catch(error => {
|
|
|
|
|
|
// 捕获所有验证过程中的错误,并记录日志
|
|
|
|
|
|
console.error('验证token过程中出错:', error)
|
|
|
|
|
|
// 网络错误等不会导致登出
|
|
|
|
|
|
})
|
|
|
|
|
|
} catch (parseError) {
|
|
|
|
|
|
// 用户数据无法解析,需要清除
|
|
|
|
|
|
console.error('解析用户数据失败:', parseError)
|
2025-03-29 17:02:40 +08:00
|
|
|
|
handleLogout()
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
2025-05-08 14:51:22 +08:00
|
|
|
|
// 有token但没有用户信息,可能是部分数据丢失
|
|
|
|
|
|
console.warn('找到token但没有用户信息,尝试保持登录状态')
|
|
|
|
|
|
|
|
|
|
|
|
// 尝试验证token并获取用户信息
|
|
|
|
|
|
try {
|
|
|
|
|
|
const isValid = await validateToken()
|
|
|
|
|
|
if (isValid) {
|
|
|
|
|
|
// 如果token有效,尝试从API获取用户信息
|
|
|
|
|
|
// 这里简化处理,直接使用token
|
|
|
|
|
|
setToken(storedToken)
|
|
|
|
|
|
setIsAuthenticated(true)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// token确认无效,清除
|
|
|
|
|
|
handleLogout()
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
// 验证过程出错,记录日志但不登出
|
|
|
|
|
|
console.error('验证token过程中出错:', error)
|
|
|
|
|
|
// 保留token,允许用户继续使用
|
|
|
|
|
|
setToken(storedToken)
|
|
|
|
|
|
setIsAuthenticated(true)
|
|
|
|
|
|
}
|
2025-03-29 17:02:40 +08:00
|
|
|
|
}
|
2025-03-29 16:50:39 +08:00
|
|
|
|
}
|
2025-03-31 12:04:59 +08:00
|
|
|
|
} catch (error) {
|
2025-05-08 14:51:22 +08:00
|
|
|
|
console.error("初始化认证状态时出错:", error)
|
|
|
|
|
|
// 非401错误不应强制登出
|
|
|
|
|
|
if (error instanceof Error &&
|
|
|
|
|
|
(error.message.includes('401') ||
|
|
|
|
|
|
error.message.includes('未授权') ||
|
|
|
|
|
|
error.message.includes('token'))) {
|
|
|
|
|
|
handleLogout()
|
|
|
|
|
|
}
|
2025-03-31 12:04:59 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
setIsLoading(false)
|
|
|
|
|
|
setIsInitialized(true)
|
2025-03-29 16:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-03-29 17:02:40 +08:00
|
|
|
|
|
|
|
|
|
|
initAuth()
|
2025-03-31 12:04:59 +08:00
|
|
|
|
}, []) // 空依赖数组,仅在组件挂载时执行一次
|
2025-03-29 16:50:39 +08:00
|
|
|
|
|
2025-03-29 17:02:40 +08:00
|
|
|
|
const handleLogout = () => {
|
2025-05-23 16:47:37 +08:00
|
|
|
|
// 先清除所有认证相关的状态
|
2025-03-29 17:02:40 +08:00
|
|
|
|
safeLocalStorage.removeItem("token")
|
2025-05-19 12:05:35 +08:00
|
|
|
|
safeLocalStorage.removeItem("token_expired")
|
|
|
|
|
|
safeLocalStorage.removeItem("s2_accountId")
|
|
|
|
|
|
safeLocalStorage.removeItem("userInfo")
|
2025-03-29 17:02:40 +08:00
|
|
|
|
safeLocalStorage.removeItem("user")
|
|
|
|
|
|
setToken(null)
|
|
|
|
|
|
setUser(null)
|
|
|
|
|
|
setIsAuthenticated(false)
|
2025-05-23 16:47:37 +08:00
|
|
|
|
|
|
|
|
|
|
// 使用 window.location 而不是 router.push,避免状态更新和路由跳转的竞态条件
|
|
|
|
|
|
if (typeof window !== 'undefined') {
|
|
|
|
|
|
window.location.href = '/login'
|
|
|
|
|
|
}
|
2025-03-29 17:02:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const login = (newToken: string, userData: User) => {
|
|
|
|
|
|
safeLocalStorage.setItem("token", newToken)
|
2025-05-23 16:47:37 +08:00
|
|
|
|
safeLocalStorage.setItem("userInfo", JSON.stringify(userData))
|
2025-03-29 16:50:39 +08:00
|
|
|
|
setToken(newToken)
|
2025-03-29 17:02:40 +08:00
|
|
|
|
setUser(userData)
|
2025-03-29 16:50:39 +08:00
|
|
|
|
setIsAuthenticated(true)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const logout = () => {
|
2025-03-29 17:02:40 +08:00
|
|
|
|
handleLogout()
|
2025-03-29 16:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-29 17:02:40 +08:00
|
|
|
|
// 用于刷新 token 的方法
|
|
|
|
|
|
const updateToken = (newToken: string) => {
|
|
|
|
|
|
safeLocalStorage.setItem("token", newToken)
|
|
|
|
|
|
setToken(newToken)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<AuthContext.Provider value={{ isAuthenticated, token, user, login, logout, updateToken }}>
|
2025-03-31 12:04:59 +08:00
|
|
|
|
{isLoading && isInitialized ? (
|
|
|
|
|
|
<div className="flex h-screen w-screen items-center justify-center">加载中...</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
children
|
|
|
|
|
|
)}
|
2025-03-29 17:02:40 +08:00
|
|
|
|
</AuthContext.Provider>
|
|
|
|
|
|
)
|
2025-03-29 16:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|