Files
vf_react/src/contexts/AuthContext.js
zdl 25cc28e03b feat: 完全移除邮箱登录代码
移除 registerWithEmail 方法
     移除 sendEmailCode 方法
     已从导出对象中移除 registerWithEmail 和 sendEmailCode。
2025-11-19 16:15:50 +08:00

412 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';
import { logger } from '../utils/logger';
import { useNotification } from '../contexts/NotificationContext';
import { identifyUser, resetUser, trackEvent } from '@lib/posthog';
import { SPECIAL_EVENTS } from '@lib/constants';
// 创建认证上下文
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();
const { showWelcomeGuide } = useNotification();
// ⚡ 使用 ref 保存最新的 isAuthenticated 值,避免事件监听器重复注册
const isAuthenticatedRef = React.useRef(isAuthenticated);
// ⚡ 请求节流:记录上次请求时间,防止短时间内重复请求
const lastCheckTimeRef = React.useRef(0);
const MIN_CHECK_INTERVAL = 1000; // 最少间隔1秒
// 检查Session状态
const checkSession = async () => {
// 节流检查
const now = Date.now();
const timeSinceLastCheck = now - lastCheckTimeRef.current;
if (timeSinceLastCheck < MIN_CHECK_INTERVAL) {
logger.warn('AuthContext', 'checkSession 请求被节流(防止频繁请求)', {
timeSinceLastCheck: `${timeSinceLastCheck}ms`,
minInterval: `${MIN_CHECK_INTERVAL}ms`,
reason: '距离上次请求间隔太短'
});
return;
}
lastCheckTimeRef.current = now;
try {
logger.debug('AuthContext', '开始检查Session状态', {
timestamp: new Date().toISOString(),
timeSinceLastCheck: timeSinceLastCheck > 0 ? `${timeSinceLastCheck}ms` : '首次请求'
});
// 创建超时控制器
const controller = new AbortController();
const timeoutId = setTimeout(() => {
controller.abort(new Error('Session check timeout after 5 seconds'));
}, 5000); // 5秒超时
const response = await fetch(`/api/auth/session`, {
method: 'GET',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error('Session检查失败');
}
const data = await response.json();
logger.debug('AuthContext', 'Session数据', {
isAuthenticated: data.isAuthenticated,
userId: data.user?.id
});
if (data.isAuthenticated && data.user) {
// ⚡ 只在 user 数据真正变化时才更新状态,避免无限循环
setUser((prevUser) => {
// 比较用户 ID如果相同则不更新
if (prevUser && prevUser.id === data.user.id) {
return prevUser;
}
// ✅ 识别用户身份到 PostHog
identifyUser(data.user.id, {
email: data.user.email,
username: data.user.username,
subscription_tier: data.user.subscription_tier,
role: data.user.role,
registration_date: data.user.created_at
});
return data.user;
});
setIsAuthenticated((prev) => prev === true ? prev : true);
} else {
setUser((prev) => prev === null ? prev : null);
setIsAuthenticated((prev) => prev === false ? prev : false);
}
} catch (error) {
// ✅ 区分AbortError和真实错误
if (error.name === 'AbortError') {
logger.debug('AuthContext', 'Session check aborted', {
reason: error.message || 'Request cancelled',
isTimeout: error.message?.includes('timeout')
});
// AbortError不改变登录状态保持原状态
return;
}
// 只有真实错误才标记为未登录
logger.error('AuthContext', 'checkSession failed', error);
setUser((prev) => prev === null ? prev : null);
setIsAuthenticated((prev) => prev === false ? prev : false);
} finally {
// ⚡ 只在 isLoading 为 true 时才设置为 false避免不必要的状态更新
setIsLoading((prev) => prev === false ? prev : false);
}
};
// ⚡ 初始化时检查Session - 并行执行,不阻塞页面渲染
useEffect(() => {
const controller = new AbortController();
// 传递signal给checkSession需要修改checkSession签名
// 暂时使用原有方式但添加cleanup防止组件卸载时的内存泄漏
checkSession(); // 直接调用,与页面渲染并行
// ✅ Cleanup: 组件卸载时abort可能正在进行的请求
return () => {
controller.abort(new Error('AuthProvider unmounted'));
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// ⚡ 同步 isAuthenticated 到 ref
useEffect(() => {
isAuthenticatedRef.current = isAuthenticated;
}, [isAuthenticated]);
// 监听路由变化检查session处理微信登录回调
// ⚡ 移除 isAuthenticated 依赖,使用 ref 避免重复注册事件监听器
useEffect(() => {
const handleRouteChange = () => {
// 使用 ref 获取最新的认证状态
if (window.location.pathname === '/home' && !isAuthenticatedRef.current) {
checkSession();
}
};
window.addEventListener('popstate', handleRouteChange);
return () => window.removeEventListener('popstate', handleRouteChange);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // ✅ 空依赖数组,只注册一次事件监听器
// 更新本地用户的便捷方法
const updateUser = (partial) => {
setUser((prev) => ({ ...(prev || {}), ...partial }));
};
// 传统登录方法
const login = async (credential, password, loginType = 'email') => {
try {
setIsLoading(true);
logger.debug('AuthContext', '开始登录流程', { credential: credential.substring(0, 3) + '***', 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);
}
logger.api.request('POST', '/api/auth/login', {
credential: credential.substring(0, 3) + '***',
loginType
});
const response = await fetch(`/api/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
credentials: 'include',
body: formData
});
// 获取响应文本然后尝试解析JSON
const responseText = await response.text();
let data;
try {
data = JSON.parse(responseText);
logger.api.response('POST', '/api/auth/login', response.status, data);
} catch (parseError) {
logger.error('AuthContext', 'login', parseError, { responseText: responseText.substring(0, 100) });
throw new Error(`服务器响应格式错误: ${responseText.substring(0, 100)}...`);
}
if (!response.ok || !data.success) {
throw new Error(data.error || '登录失败');
}
// 更新状态
setUser(data.user);
setIsAuthenticated(true);
// ❌ 过时的追踪代码已移除(新代码在组件中使用 useAuthEvents 追踪)
// 正确的事件追踪在 AuthFormContent.js 中调用 authEvents.trackLoginSuccess()
// 事件名:'User Logged In' 或 'User Signed Up'
// 属性名login_method (不是 loginType)
// ⚡ 移除toast让调用者处理UI反馈避免并发更新冲突
// toast({
// title: "登录成功",
// description: "欢迎回来!",
// status: "success",
// duration: 3000,
// isClosable: true,
// });
// ⚡ 登录成功后显示欢迎引导延迟2秒避免与登录Toast冲突
setTimeout(() => {
showWelcomeGuide();
}, 2000);
return { success: true };
} catch (error) {
logger.error('AuthContext', 'login', error, { loginType });
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);
// ❌ 过时的追踪代码已移除(新代码在组件中使用 useAuthEvents 追踪)
// 正确的事件追踪在 AuthFormContent.js 中调用 authEvents.trackLoginSuccess()
// 事件名:'User Signed Up'(不是 'user_registered'
// 属性名login_method不是 method
toast({
title: "注册成功",
description: "欢迎加入价值前沿!",
status: "success",
duration: 3000,
isClosable: true,
});
// ⚡ 注册成功后显示欢迎引导延迟2秒
setTimeout(() => {
showWelcomeGuide();
}, 2000);
return { success: true };
} catch (error) {
logger.error('AuthContext', 'registerWithPhone', error, { phone: phone.substring(0, 3) + '****' });
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',
body: JSON.stringify({ phone })
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || '发送失败');
}
// ❌ 移除成功 toast
logger.info('AuthContext', '验证码已发送', { phone: phone.substring(0, 3) + '****' });
return { success: true };
} catch (error) {
// ❌ 移除错误 toast
logger.error('AuthContext', 'sendSmsCode', error, { phone: phone.substring(0, 3) + '****' });
return { success: false, error: error.message };
}
};
// 登出方法
const logout = async () => {
try {
// 调用后端登出API
await fetch(`/api/auth/logout`, {
method: 'POST',
credentials: 'include'
});
// ✅ 追踪登出事件(必须在 resetUser() 之前,否则会丢失用户身份)
trackEvent(SPECIAL_EVENTS.USER_LOGGED_OUT, {
timestamp: new Date().toISOString(),
user_id: user?.id || null,
session_duration_minutes: user?.session_start
? Math.round((Date.now() - new Date(user.session_start).getTime()) / 60000)
: null,
});
// ✅ 重置 PostHog 用户会话
resetUser();
// 清除本地状态
setUser(null);
setIsAuthenticated(false);
// ✅ 保留登出成功 toast关键操作提示
toast({
title: "已登出",
description: "您已成功退出登录",
status: "info",
duration: 2000,
isClosable: true,
});
} catch (error) {
logger.error('AuthContext', 'logout', 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,
registerWithPhone,
sendSmsCode,
logout,
hasRole,
refreshSession,
checkSession
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
};