Files
smart-crop-ui/crop-x/src/components/auth/AuthContext.tsx

341 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import React, { createContext, useContext, useState, ReactNode, useRef } from 'react';
import { getCurrentUserInfoApiV1AuthMeGet, refreshTokenApiV1AuthRefreshPost, listAdminSettingsApiV1AdminSettingsGet } from '@/lib/api/sdk.gen';
import { setAuthUser, getAuthUser, setSettings } from '@/stores/modules/auth';
// Cookie 操作工具
const setTokenCookie = (token: string) => {
if (typeof document !== 'undefined') {
document.cookie = `auth-token=${token}; path=/; max-age=${7 * 24 * 60 * 60}`;
console.log('🍪 Token已设置到cookie');
}
};
const removeTokenCookie = () => {
if (typeof document !== 'undefined') {
document.cookie = 'auth-token=; path=/; max-age=0';
console.log('🗑️ Token已从cookie中清除');
}
};
interface User {
id: string;
username: string;
realName: string;
email?: string;
phone?: string;
enterpriseId?: string;
token?: string;
}
interface AuthContextType {
user: User | null;
login: (user: User) => void;
logout: () => void;
isAuthenticated: boolean;
loading: boolean;
validateUser: () => Promise<void>;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
interface AuthProviderProps {
children: ReactNode;
}
export function AuthProvider({ children }: AuthProviderProps) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const refreshTimerRef = useRef<NodeJS.Timeout | null>(null);
const login = (userData: User) => {
setUser(userData);
// 存储到 localStorage
localStorage.setItem('user', JSON.stringify(userData));
// 同时设置 cookie供中间件使用
if (userData.token) {
setTokenCookie(userData.token);
}
setLoading(false);
// 登录成功后,启动 token 自动刷新定时器
startTokenRefresh();
};
const logout = () => {
setUser(null);
localStorage.removeItem('user');
removeTokenCookie(); // 清除 cookie
setLoading(false);
// 清除定时器
if (refreshTimerRef.current) {
clearInterval(refreshTimerRef.current);
refreshTimerRef.current = null;
}
// 跳转到登录页
if (typeof window !== 'undefined') {
window.location.href = '/login';
}
};
// 刷新 token 的函数
const refreshAccessToken = async () => {
try {
const storedUser = localStorage.getItem('user');
if (!storedUser) {
console.warn('⚠️ 未找到用户信息,无法刷新 token');
return;
}
const userData = JSON.parse(storedUser);
if (!userData.token) {
console.warn('⚠️ 用户信息中没有 token无法刷新');
return;
}
console.log('🔄 开始刷新 token...');
console.log('📝 当前 token (前20字符):', userData.token.substring(0, 20) + '...');
const response = await refreshTokenApiV1AuthRefreshPost({
headers: {
'Authorization': `Bearer ${userData.refreshToken}`,
},
});
console.log('📨 刷新接口响应:', response);
// 检查响应数据结构
if (response.data) {
console.log('📋 响应数据结构:', JSON.stringify(response.data, null, 2));
// 尝试多种可能的 token 字段名
const newToken = response.data.access_token || ""
const refreshToken = response.data.refresh_token || ""
if (newToken) {
// 更新用户信息中的 token
const updatedUserData = {
...userData,
token: newToken,
refreshToken:refreshToken,
// 如果有其他 token 相关字段也一并更新
};
// 更新 localStorage
localStorage.setItem('user', JSON.stringify(updatedUserData));
// 更新状态
setUser(updatedUserData);
// 更新 cookie
setTokenCookie(newToken);
console.log('✅ Token 刷新成功');
console.log('🔑 新 token (前20字符):', newToken.substring(0, 20) + '...');
} else {
console.error('❌ Token 刷新失败:响应中未找到 token 字段');
console.log('🔍 可用字段:', Object.keys(response.data));
// 先不登出,记录错误,让用户继续使用当前 token
console.log('⚠️ 暂不执行登出,继续使用当前 token');
}
} else {
console.error('❌ Token 刷新失败:响应数据为空');
console.log('📊 完整响应:', response);
// 先不登出,可能是接口问题
console.log('⚠️ 暂不执行登出,可能是接口问题');
}
} catch (error: any) {
console.error('❌ Token 刷新失败:', error);
console.log('📝 错误详情:', {
message: error.message,
status: error.response?.status,
statusText: error.response?.statusText,
data: error.response?.data
});
// 根据错误类型决定是否登出
if (error.response?.status === 401) {
console.log('🔒 Token 已过期,执行登出');
logout();
} else {
console.log('⚠️ 网络或服务器错误,暂不执行登出');
}
}
};
// 启动定时刷新 token
const startTokenRefresh = () => {
// 清除之前的定时器
if (refreshTimerRef.current) {
clearInterval(refreshTimerRef.current);
}
// 每 1 分钟刷新一次 token
refreshTimerRef.current = setInterval(() => {
refreshAccessToken();
}, 5 * 60 * 1000); // 60 秒 = 1 分钟
console.log('🕐 Token 自动刷新定时器已启动每5分钟');
};
// 停止定时刷新 token
const stopTokenRefresh = () => {
if (refreshTimerRef.current) {
clearInterval(refreshTimerRef.current);
refreshTimerRef.current = null;
console.log('⏹️ Token 自动刷新定时器已停止');
}
};
// 验证当前用户信息
const validateUser = async () => {
try {
const storedUser = localStorage.getItem('user');
if (!storedUser) {
setLoading(false);
return;
}
const userData = JSON.parse(storedUser);
// 使用 Promise.all 并行发起两个请求
const [userResponse, settingsResponse] = await Promise.all([
// 请求1: 调用 /api/v1/auth/me 验证用户信息
getCurrentUserInfoApiV1AuthMeGet({
headers: {
'Authorization': `Bearer ${userData.token}`,
},
}),
// 请求2: 调用 /api/v1/admin/settings 获取设置信息
listAdminSettingsApiV1AdminSettingsGet({
headers: {
'Authorization': `Bearer ${userData.token}`,
},
query: {
page: 1,
size: 100,
order_by: '',
sort_order: 'desc'
}
})
]);
if (userResponse.data) {
// 更新用户信息(可能包含最新的权限、角色等)
const updatedUserData = {
...userData,
...userResponse.data, // 合并最新的用户信息
};
setUser(updatedUserData);
// 存储到 Zustand store
setAuthUser(userResponse.data);
console.log('✅ 用户验证成功,最新用户信息:', userResponse.data);
console.log('📦 从 Zustand store 取出的用户数据:', getAuthUser());
// 存储设置数据到 Zustand store
if (settingsResponse && settingsResponse.data) {
setSettings(settingsResponse.data);
console.log('✅ 设置数据获取成功:', settingsResponse.data);
}
// 验证成功后,启动 token 自动刷新定时器
startTokenRefresh();
} else {
// Token无效清除用户信息
console.warn('⚠️ Token验证失败清除用户信息');
localStorage.removeItem('user');
setUser(null);
}
} catch (error: any) {
console.error('❌ 用户验证失败:', error);
// 验证失败时也清除用户信息,避免不一致状态
localStorage.removeItem('user');
setUser(null);
} finally {
setLoading(false);
}
};
// 统一的认证初始化和路由监听
React.useEffect(() => {
// 处理当前路径的逻辑
const handleCurrentPath = () => {
const currentPath = typeof window !== 'undefined' ? window.location.pathname : '';
// 如果在登录页面,跳过验证,直接设置为非加载状态
if (currentPath.startsWith('/login')) {
console.log('📄 检测到登录页面,跳过用户验证');
stopTokenRefresh(); // 停止任何正在进行的 token 刷新
setLoading(false);
return;
}
// 检查是否有存储的用户信息和 token并设置 cookie
const storedUser = localStorage.getItem('user');
if (storedUser) {
try {
const userData = JSON.parse(storedUser);
if (userData.token && !document.cookie.includes('auth-token')) {
setTokenCookie(userData.token);
console.log('🔄 初始化时设置cookie');
}
} catch (error) {
console.error('解析存储用户信息失败:', error);
localStorage.removeItem('user');
}
}
// 只有在非登录页面才进行用户验证
validateUser();
};
// 立即处理当前路径
handleCurrentPath();
// 监听路由变化
const handlePathChange = () => {
console.log('🔄 路由变化,重新检查认证状态');
handleCurrentPath();
};
// 监听 popstate 事件(浏览器前进后退)
window.addEventListener('popstate', handlePathChange);
return () => {
window.removeEventListener('popstate', handlePathChange);
};
}, []);
// 清理定时器
React.useEffect(() => {
return () => {
// 组件卸载时清理定时器
stopTokenRefresh();
};
}, []);
const value: AuthContextType = {
user,
login,
logout,
isAuthenticated: !!user,
loading,
validateUser, // 暴露验证方法供手动刷新使用
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}