502 lines
13 KiB
JavaScript
Executable File
502 lines
13 KiB
JavaScript
Executable File
// 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>
|
||
);
|
||
}; |