fix(web): fix 401 unauthorized redirect not working properly (#997)

修复了token过期后页面一直遇到401错误、无法自动跳转登录页的问题
主要改动:
1. httpClient.ts
   - 去掉延迟跳转的setTimeout,改为立即跳转
   - 返回pending promise阻止SWR捕获401错误
   - 保存from401标记到sessionStorage,由登录页显示提示
2. LoginPage.tsx
   - 检测from401标记,显示"登录已过期"提示(永久显示)
   - 在登录成功时手动关闭过期提示toast
   - 支持管理员登录、普通登录、OTP验证三种场景
3. TraderConfigModal.tsx
   - 修复3处直接使用fetch()的问题,改为httpClient.get()
   - 确保所有API请求都经过统一的401拦截器
4. translations.ts
   - 添加sessionExpired的中英文翻译
修复效果:
- token过期时立即跳转登录页(无延迟)
- 登录页持续显示过期提示,直到用户登录成功或手动关闭
- 不会再看到401错误页面或重复的错误提示
Co-authored-by: tinkle-community <tinklefund@gmail.com>
This commit is contained in:
Diego
2025-11-13 23:32:26 -05:00
committed by tangmengqiu
parent b96c86fce4
commit 4bb65397f6
4 changed files with 53 additions and 36 deletions

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react'
import React, { useState, useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { useAuth } from '../contexts/AuthContext'
import { useLanguage } from '../contexts/LanguageContext'
@@ -24,6 +24,18 @@ export function LoginPage() {
const adminMode = false
const { config: systemConfig } = useSystemConfig()
const registrationEnabled = systemConfig?.registration_enabled !== false
const [expiredToastId, setExpiredToastId] = useState<string | number | null>(null)
// Show notification if user was redirected here due to 401
useEffect(() => {
if (sessionStorage.getItem('from401') === 'true') {
const id = toast.warning(t('sessionExpired', language), {
duration: Infinity // Keep showing until user dismisses or logs in
})
setExpiredToastId(id)
sessionStorage.removeItem('from401')
}
}, [language])
const handleAdminLogin = async (e: React.FormEvent) => {
e.preventDefault()
@@ -34,6 +46,11 @@ export function LoginPage() {
const msg = result.message || t('loginFailed', language)
setError(msg)
toast.error(msg)
} else {
// Dismiss the "login expired" toast on successful login
if (expiredToastId) {
toast.dismiss(expiredToastId)
}
}
setLoading(false)
}
@@ -49,6 +66,11 @@ export function LoginPage() {
if (result.requiresOTP && result.userID) {
setUserID(result.userID)
setStep('otp')
} else {
// Dismiss the "login expired" toast on successful login (no OTP required)
if (expiredToastId) {
toast.dismiss(expiredToastId)
}
}
} else {
const msg = result.message || t('loginFailed', language)
@@ -70,6 +92,11 @@ export function LoginPage() {
const msg = result.message || t('verificationFailed', language)
setError(msg)
toast.error(msg)
} else {
// Dismiss the "login expired" toast on successful OTP verification
if (expiredToastId) {
toast.dismiss(expiredToastId)
}
}
// 成功的话AuthContext会自动处理登录状态

View File

@@ -4,6 +4,7 @@ import { useLanguage } from '../contexts/LanguageContext'
import { t } from '../i18n/translations'
import { toast } from 'sonner'
import { Pencil, Plus, X as IconX } from 'lucide-react'
import { httpClient } from '../lib/httpClient'
// 提取下划线后面的名称部分
function getShortName(fullName: string): string {
@@ -114,7 +115,7 @@ export function TraderConfigModal({
useEffect(() => {
const fetchConfig = async () => {
try {
const response = await fetch('/api/config')
const response = await httpClient.get('/api/config')
const config = await response.json()
if (config.default_coins) {
setAvailableCoins(config.default_coins)
@@ -140,7 +141,7 @@ export function TraderConfigModal({
useEffect(() => {
const fetchPromptTemplates = async () => {
try {
const response = await fetch('/api/prompt-templates')
const response = await httpClient.get('/api/prompt-templates')
const data = await response.json()
if (data.templates) {
setPromptTemplates(data.templates)
@@ -198,19 +199,13 @@ export function TraderConfigModal({
throw new Error('未登录,请先登录')
}
const response = await fetch(
const response = await httpClient.get(
`/api/account?trader_id=${traderData.trader_id}`,
{
headers: {
Authorization: `Bearer ${token}`,
},
Authorization: `Bearer ${token}`,
}
)
if (!response.ok) {
throw new Error('获取账户余额失败')
}
const data = await response.json()
// total_equity = 当前账户净值(包含未实现盈亏)