mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-05 20:11:13 +08:00
fix: 修复token过期未重新登录的问题 (#803)
* fix: 修复token过期未重新登录的问题 实现统一的401错误处理机制: - 创建httpClient封装fetch API,添加响应拦截器 - 401时自动清理localStorage和React状态 - 显示"请先登录"提示并延迟1.5秒后跳转登录页 - 保存当前URL到sessionStorage用于登录后返回 - 改造所有API调用使用httpClient统一处理 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: tinkle-community <tinklefund@gmail.com> * fix: 添加401处理的单例保护防止并发竞态 问题: - 多个API同时返回401会导致多个通知叠加 - 多个style元素被添加到DOM造成内存泄漏 - 可能触发多次登录页跳转 解决方案: - 添加静态标志位 isHandling401 防止重复处理 - 第一个401触发完整处理流程 - 后续401直接抛出错误,避免重复操作 - 确保只显示一次通知和一次跳转 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: tinkle-community <tinklefund@gmail.com> * fix: 修复isHandling401标志永不重置的问题 问题: - isHandling401标志在401处理后永不重置 - 导致用户重新登录后,后续401会被静默忽略 - 页面刷新或取消重定向后标志仍为true 解决方案: - 在HttpClient中添加reset401Flag()公开方法 - 登录成功后调用reset401Flag()重置标志 - 页面加载时调用reset401Flag()确保新会话正常 影响范围: - web/src/lib/httpClient.ts: 添加reset方法和导出函数 - web/src/contexts/AuthContext.tsx: 在登录和页面加载时重置 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: tinkle-community <tinklefund@gmail.com> * fix(auth): consume returnUrl after successful login (BLOCKING-1) 修复登录后未跳转回原页面的问题。 问题: - httpClient在401时保存returnUrl到sessionStorage - 但登录成功后没有读取和使用returnUrl - 导致用户登录后停留在登录页,无法回到原页面 修复: - 在loginAdmin、verifyOTP、completeRegistration三个登录方法中 - 添加returnUrl检查和跳转逻辑 - 登录成功后优先跳转到returnUrl,如果没有则使用默认页面 影响: - 用户token过期后重新登录,会自动返回之前访问的页面 - 提升用户体验,避免手动导航 测试场景: 1. 用户访问/traders → token过期 → 登录 → 自动回到/traders ✅ 2. 用户直接访问/login → 登录 → 跳转到默认页面(/dashboard或/traders) ✅ Related: BLOCKING-1 in PR #803 code review --------- Co-authored-by: sue <177699783@qq.com> Co-authored-by: tinkle-community <tinklefund@gmail.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import React, { createContext, useContext, useState, useEffect } from 'react'
|
||||
import { getSystemConfig } from '../lib/config'
|
||||
import { reset401Flag } from '../lib/httpClient'
|
||||
|
||||
interface User {
|
||||
id: string
|
||||
@@ -58,6 +59,9 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
// Reset 401 flag on page load to allow fresh 401 handling
|
||||
reset401Flag()
|
||||
|
||||
// 先检查是否为管理员模式(使用带缓存的系统配置获取)
|
||||
getSystemConfig()
|
||||
.then(() => {
|
||||
@@ -85,6 +89,23 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
})
|
||||
}, [])
|
||||
|
||||
// Listen for unauthorized events from httpClient (401 responses)
|
||||
useEffect(() => {
|
||||
const handleUnauthorized = () => {
|
||||
console.log('Unauthorized event received - clearing auth state')
|
||||
// Clear auth state when 401 is detected
|
||||
setUser(null)
|
||||
setToken(null)
|
||||
// Note: localStorage cleanup is already done in httpClient
|
||||
}
|
||||
|
||||
window.addEventListener('unauthorized', handleUnauthorized)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('unauthorized', handleUnauthorized)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const login = async (email: string, password: string) => {
|
||||
try {
|
||||
const response = await fetch('/api/login', {
|
||||
@@ -125,6 +146,9 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
})
|
||||
const data = await response.json()
|
||||
if (response.ok) {
|
||||
// Reset 401 flag on successful login
|
||||
reset401Flag()
|
||||
|
||||
const userInfo = {
|
||||
id: data.user_id || 'admin',
|
||||
email: data.email || 'admin@localhost',
|
||||
@@ -133,9 +157,18 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
setUser(userInfo)
|
||||
localStorage.setItem('auth_token', data.token)
|
||||
localStorage.setItem('auth_user', JSON.stringify(userInfo))
|
||||
// 跳转到仪表盘
|
||||
window.history.pushState({}, '', '/dashboard')
|
||||
window.dispatchEvent(new PopStateEvent('popstate'))
|
||||
|
||||
// Check and redirect to returnUrl if exists
|
||||
const returnUrl = sessionStorage.getItem('returnUrl')
|
||||
if (returnUrl) {
|
||||
sessionStorage.removeItem('returnUrl')
|
||||
window.history.pushState({}, '', returnUrl)
|
||||
window.dispatchEvent(new PopStateEvent('popstate'))
|
||||
} else {
|
||||
// 跳转到仪表盘
|
||||
window.history.pushState({}, '', '/dashboard')
|
||||
window.dispatchEvent(new PopStateEvent('popstate'))
|
||||
}
|
||||
return { success: true }
|
||||
} else {
|
||||
return { success: false, message: data.error || '登录失败' }
|
||||
@@ -199,6 +232,9 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const data = await response.json()
|
||||
|
||||
if (response.ok) {
|
||||
// Reset 401 flag on successful login
|
||||
reset401Flag()
|
||||
|
||||
// 登录成功,保存token和用户信息
|
||||
const userInfo = { id: data.user_id, email: data.email }
|
||||
setToken(data.token)
|
||||
@@ -206,9 +242,17 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
localStorage.setItem('auth_token', data.token)
|
||||
localStorage.setItem('auth_user', JSON.stringify(userInfo))
|
||||
|
||||
// 跳转到配置页面
|
||||
window.history.pushState({}, '', '/traders')
|
||||
window.dispatchEvent(new PopStateEvent('popstate'))
|
||||
// Check and redirect to returnUrl if exists
|
||||
const returnUrl = sessionStorage.getItem('returnUrl')
|
||||
if (returnUrl) {
|
||||
sessionStorage.removeItem('returnUrl')
|
||||
window.history.pushState({}, '', returnUrl)
|
||||
window.dispatchEvent(new PopStateEvent('popstate'))
|
||||
} else {
|
||||
// 跳转到配置页面
|
||||
window.history.pushState({}, '', '/traders')
|
||||
window.dispatchEvent(new PopStateEvent('popstate'))
|
||||
}
|
||||
|
||||
return { success: true, message: data.message }
|
||||
} else {
|
||||
@@ -232,6 +276,9 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const data = await response.json()
|
||||
|
||||
if (response.ok) {
|
||||
// Reset 401 flag on successful login
|
||||
reset401Flag()
|
||||
|
||||
// 注册完成,自动登录
|
||||
const userInfo = { id: data.user_id, email: data.email }
|
||||
setToken(data.token)
|
||||
@@ -239,9 +286,17 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
localStorage.setItem('auth_token', data.token)
|
||||
localStorage.setItem('auth_user', JSON.stringify(userInfo))
|
||||
|
||||
// 跳转到配置页面
|
||||
window.history.pushState({}, '', '/traders')
|
||||
window.dispatchEvent(new PopStateEvent('popstate'))
|
||||
// Check and redirect to returnUrl if exists
|
||||
const returnUrl = sessionStorage.getItem('returnUrl')
|
||||
if (returnUrl) {
|
||||
sessionStorage.removeItem('returnUrl')
|
||||
window.history.pushState({}, '', returnUrl)
|
||||
window.dispatchEvent(new PopStateEvent('popstate'))
|
||||
} else {
|
||||
// 跳转到配置页面
|
||||
window.history.pushState({}, '', '/traders')
|
||||
window.dispatchEvent(new PopStateEvent('popstate'))
|
||||
}
|
||||
|
||||
return { success: true, message: data.message }
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user