Files
vf_react/src/contexts/AuthContext.js
2025-10-16 15:54:57 +08:00

502 lines
13 KiB
JavaScript
Executable File
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.

// src/contexts/AuthContext.js - Session版本
import React, { createContext, useContext, useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useToast } from '@chakra-ui/react';
// 创建认证上下文
const AuthContext = createContext();
// 自定义Hook
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};
// 认证提供者组件
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true); // ⚡ 串行执行,阻塞渲染直到 Session 检查完成
const [isAuthenticated, setIsAuthenticated] = useState(false);
const navigate = useNavigate();
const toast = useToast();
// 检查Session状态
const checkSession = async () => {
try {
console.log('🔍 检查Session状态...');
// 创建超时控制器
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5秒超时
const response = await fetch(`/api/auth/session`, {
method: 'GET',
credentials: 'include', // 重要包含cookie
headers: {
'Content-Type': 'application/json',
},
signal: controller.signal // 添加超时信号
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error('Session检查失败');
}
const data = await response.json();
console.log('📦 Session数据:', data);
if (data.isAuthenticated && data.user) {
setUser(data.user);
setIsAuthenticated(true);
} else {
setUser(null);
setIsAuthenticated(false);
}
} catch (error) {
console.error('❌ Session检查错误:', error);
// 网络错误或超时,设置为未登录状态
setUser(null);
setIsAuthenticated(false);
} finally {
// ⚡ Session 检查完成后,停止加载状态
setIsLoading(false);
}
};
// ⚡ 初始化时检查Session - 并行执行,不阻塞页面渲染
useEffect(() => {
checkSession(); // 直接调用,与页面渲染并行
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// 监听路由变化检查session处理微信登录回调
useEffect(() => {
const handleRouteChange = () => {
// 如果是从微信回调返回的重新检查session
if (window.location.pathname === '/home' && !isAuthenticated) {
checkSession();
}
};
window.addEventListener('popstate', handleRouteChange);
return () => window.removeEventListener('popstate', handleRouteChange);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isAuthenticated]);
// 更新本地用户的便捷方法
const updateUser = (partial) => {
setUser((prev) => ({ ...(prev || {}), ...partial }));
};
// 传统登录方法
const login = async (credential, password, loginType = 'email') => {
try {
setIsLoading(true);
console.log('🔐 开始登录流程:', { credential, loginType });
const formData = new URLSearchParams();
formData.append('password', password);
if (loginType === 'username') {
formData.append('username', credential);
} else if (loginType === 'email') {
formData.append('email', credential);
} else if (loginType === 'phone') {
formData.append('username', credential);
}
console.log('📤 发送登录请求到:', `/api/auth/login`);
console.log('📝 请求数据:', {
credential,
loginType,
formData: formData.toString()
});
const response = await fetch(`/api/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
credentials: 'include', // 包含cookie
body: formData
});
console.log('📨 响应状态:', response.status, response.statusText);
console.log('📨 响应头:', Object.fromEntries(response.headers.entries()));
// 获取响应文本然后尝试解析JSON
const responseText = await response.text();
console.log('📨 响应原始内容:', responseText);
let data;
try {
data = JSON.parse(responseText);
console.log('✅ JSON解析成功:', data);
} catch (parseError) {
console.error('❌ JSON解析失败:', parseError);
console.error('📄 响应内容:', responseText);
throw new Error(`服务器响应格式错误: ${responseText.substring(0, 100)}...`);
}
if (!response.ok || !data.success) {
throw new Error(data.error || '登录失败');
}
// 更新状态
setUser(data.user);
setIsAuthenticated(true);
// ⚡ 移除toast让调用者处理UI反馈避免并发更新冲突
// toast({
// title: "登录成功",
// description: "欢迎回来!",
// status: "success",
// duration: 3000,
// isClosable: true,
// });
return { success: true };
} catch (error) {
console.error('❌ 登录错误:', error);
// ⚡ 移除toast让调用者处理错误显示避免重复toast和并发更新
// toast({
// title: "登录失败",
// description: error.message || "请检查您的登录信息",
// status: "error",
// duration: 3000,
// isClosable: true,
// });
return { success: false, error: error.message };
} finally {
setIsLoading(false);
}
};
// 注册方法
const register = async (username, email, password) => {
try {
setIsLoading(true);
const formData = new URLSearchParams();
formData.append('username', username);
formData.append('email', email);
formData.append('password', password);
const response = await fetch(`/api/auth/register`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
credentials: 'include',
body: formData
});
const data = await response.json();
if (!response.ok || !data.success) {
throw new Error(data.error || '注册失败');
}
// 注册成功后自动登录
setUser(data.user);
setIsAuthenticated(true);
toast({
title: "注册成功",
description: "欢迎加入价值前沿!",
status: "success",
duration: 3000,
isClosable: true,
});
return { success: true };
} catch (error) {
console.error('注册错误:', error);
toast({
title: "注册失败",
description: error.message || "注册失败,请稍后重试",
status: "error",
duration: 3000,
isClosable: true,
});
return { success: false, error: error.message };
} finally {
setIsLoading(false);
}
};
// 手机号注册
const registerWithPhone = async (phone, code, username, password) => {
try {
setIsLoading(true);
const response = await fetch(`/api/auth/register/phone`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({
phone,
code,
username,
password
})
});
const data = await response.json();
if (!response.ok || !data.success) {
throw new Error(data.error || '注册失败');
}
// 注册成功后自动登录
setUser(data.user);
setIsAuthenticated(true);
toast({
title: "注册成功",
description: "欢迎加入价值前沿!",
status: "success",
duration: 3000,
isClosable: true,
});
return { success: true };
} catch (error) {
console.error('手机注册错误:', error);
toast({
title: "注册失败",
description: error.message || "注册失败,请稍后重试",
status: "error",
duration: 3000,
isClosable: true,
});
return { success: false, error: error.message };
} finally {
setIsLoading(false);
}
};
// 邮箱注册
const registerWithEmail = async (email, code, username, password) => {
try {
setIsLoading(true);
const response = await fetch(`/api/auth/register/email`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({
email,
code,
username,
password
})
});
const data = await response.json();
if (!response.ok || !data.success) {
throw new Error(data.error || '注册失败');
}
// 注册成功后自动登录
setUser(data.user);
setIsAuthenticated(true);
toast({
title: "注册成功",
description: "欢迎加入价值前沿!",
status: "success",
duration: 3000,
isClosable: true,
});
return { success: true };
} catch (error) {
console.error('邮箱注册错误:', error);
toast({
title: "注册失败",
description: error.message || "注册失败,请稍后重试",
status: "error",
duration: 3000,
isClosable: true,
});
return { success: false, error: error.message };
} finally {
setIsLoading(false);
}
};
// 发送手机验证码
const sendSmsCode = async (phone) => {
try {
const response = await fetch(`/api/auth/send-sms-code`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include', // 必须包含以支持跨域 session cookie
body: JSON.stringify({ phone })
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || '发送失败');
}
toast({
title: "验证码已发送",
description: "请查收短信",
status: "success",
duration: 3000,
isClosable: true,
});
return { success: true };
} catch (error) {
console.error('SMS code error:', error);
toast({
title: "发送失败",
description: error.message || "请稍后重试",
status: "error",
duration: 3000,
isClosable: true,
});
return { success: false, error: error.message };
}
};
// 发送邮箱验证码
const sendEmailCode = async (email) => {
try {
const response = await fetch(`/api/auth/send-email-code`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include', // 必须包含以支持跨域 session cookie
body: JSON.stringify({ email })
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || '发送失败');
}
toast({
title: "验证码已发送",
description: "请查收邮件",
status: "success",
duration: 3000,
isClosable: true,
});
return { success: true };
} catch (error) {
console.error('Email code error:', error);
toast({
title: "发送失败",
description: error.message || "请稍后重试",
status: "error",
duration: 3000,
isClosable: true,
});
return { success: false, error: error.message };
}
};
// 登出方法
const logout = async () => {
try {
// 调用后端登出API
await fetch(`/api/auth/logout`, {
method: 'POST',
credentials: 'include'
});
// 清除本地状态
setUser(null);
setIsAuthenticated(false);
toast({
title: "已登出",
description: "您已成功退出登录",
status: "info",
duration: 2000,
isClosable: true,
});
// 不再跳转,用户留在当前页面
} catch (error) {
console.error('Logout error:', error);
// 即使API调用失败也清除本地状态
setUser(null);
setIsAuthenticated(false);
// 不再跳转,用户留在当前页面
}
};
// 检查用户是否有特定权限
const hasRole = (role) => {
return user && user.role === role;
};
// 刷新session可选
const refreshSession = async () => {
await checkSession();
};
// 提供给子组件的值
const value = {
user,
isAuthenticated,
isLoading,
updateUser,
login,
register,
registerWithPhone,
registerWithEmail,
sendSmsCode,
sendEmailCode,
logout,
hasRole,
refreshSession,
checkSession
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
};