From 4bb65397f6ee9b7a42587415310e4bba0fedf7e4 Mon Sep 17 00:00:00 2001 From: Diego <45224689+tangmengqiu@users.noreply.github.com> Date: Thu, 13 Nov 2025 23:32:26 -0500 Subject: [PATCH] =?UTF-8?q?fix(web):=20fix=20401=20unauthorized=20redirect?= =?UTF-8?q?=20not=20working=20properly=20(#997)=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E4=BA=86token=E8=BF=87=E6=9C=9F=E5=90=8E=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E4=B8=80=E7=9B=B4=E9=81=87=E5=88=B0401=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E3=80=81=E6=97=A0=E6=B3=95=E8=87=AA=E5=8A=A8=E8=B7=B3=E8=BD=AC?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E9=A1=B5=E7=9A=84=E9=97=AE=E9=A2=98=20?= =?UTF-8?q?=E4=B8=BB=E8=A6=81=E6=94=B9=E5=8A=A8=EF=BC=9A=201.=20httpClient?= =?UTF-8?q?.ts=20=20=20=20-=20=E5=8E=BB=E6=8E=89=E5=BB=B6=E8=BF=9F?= =?UTF-8?q?=E8=B7=B3=E8=BD=AC=E7=9A=84setTimeout=EF=BC=8C=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E7=AB=8B=E5=8D=B3=E8=B7=B3=E8=BD=AC=20=20=20=20-=20?= =?UTF-8?q?=E8=BF=94=E5=9B=9Epending=20promise=E9=98=BB=E6=AD=A2SWR?= =?UTF-8?q?=E6=8D=95=E8=8E=B7401=E9=94=99=E8=AF=AF=20=20=20=20-=20?= =?UTF-8?q?=E4=BF=9D=E5=AD=98from401=E6=A0=87=E8=AE=B0=E5=88=B0sessionStor?= =?UTF-8?q?age=EF=BC=8C=E7=94=B1=E7=99=BB=E5=BD=95=E9=A1=B5=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E6=8F=90=E7=A4=BA=202.=20LoginPage.tsx=20=20=20=20-?= =?UTF-8?q?=20=E6=A3=80=E6=B5=8Bfrom401=E6=A0=87=E8=AE=B0=EF=BC=8C?= =?UTF-8?q?=E6=98=BE=E7=A4=BA"=E7=99=BB=E5=BD=95=E5=B7=B2=E8=BF=87?= =?UTF-8?q?=E6=9C=9F"=E6=8F=90=E7=A4=BA(=E6=B0=B8=E4=B9=85=E6=98=BE?= =?UTF-8?q?=E7=A4=BA)=20=20=20=20-=20=E5=9C=A8=E7=99=BB=E5=BD=95=E6=88=90?= =?UTF-8?q?=E5=8A=9F=E6=97=B6=E6=89=8B=E5=8A=A8=E5=85=B3=E9=97=AD=E8=BF=87?= =?UTF-8?q?=E6=9C=9F=E6=8F=90=E7=A4=BAtoast=20=20=20=20-=20=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E7=AE=A1=E7=90=86=E5=91=98=E7=99=BB=E5=BD=95=E3=80=81?= =?UTF-8?q?=E6=99=AE=E9=80=9A=E7=99=BB=E5=BD=95=E3=80=81OTP=E9=AA=8C?= =?UTF-8?q?=E8=AF=81=E4=B8=89=E7=A7=8D=E5=9C=BA=E6=99=AF=203.=20TraderConf?= =?UTF-8?q?igModal.tsx=20=20=20=20-=20=E4=BF=AE=E5=A4=8D3=E5=A4=84?= =?UTF-8?q?=E7=9B=B4=E6=8E=A5=E4=BD=BF=E7=94=A8fetch()=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98=EF=BC=8C=E6=94=B9=E4=B8=BAhttpClient.get()=20=20=20?= =?UTF-8?q?=20-=20=E7=A1=AE=E4=BF=9D=E6=89=80=E6=9C=89API=E8=AF=B7?= =?UTF-8?q?=E6=B1=82=E9=83=BD=E7=BB=8F=E8=BF=87=E7=BB=9F=E4=B8=80=E7=9A=84?= =?UTF-8?q?401=E6=8B=A6=E6=88=AA=E5=99=A8=204.=20translations.ts=20=20=20?= =?UTF-8?q?=20-=20=E6=B7=BB=E5=8A=A0sessionExpired=E7=9A=84=E4=B8=AD?= =?UTF-8?q?=E8=8B=B1=E6=96=87=E7=BF=BB=E8=AF=91=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E6=95=88=E6=9E=9C=EF=BC=9A=20-=20token=E8=BF=87=E6=9C=9F?= =?UTF-8?q?=E6=97=B6=E7=AB=8B=E5=8D=B3=E8=B7=B3=E8=BD=AC=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E9=A1=B5(=E6=97=A0=E5=BB=B6=E8=BF=9F)=20-=20=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E9=A1=B5=E6=8C=81=E7=BB=AD=E6=98=BE=E7=A4=BA=E8=BF=87=E6=9C=9F?= =?UTF-8?q?=E6=8F=90=E7=A4=BA=EF=BC=8C=E7=9B=B4=E5=88=B0=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E6=88=90=E5=8A=9F=E6=88=96=E6=89=8B=E5=8A=A8?= =?UTF-8?q?=E5=85=B3=E9=97=AD=20-=20=E4=B8=8D=E4=BC=9A=E5=86=8D=E7=9C=8B?= =?UTF-8?q?=E5=88=B0401=E9=94=99=E8=AF=AF=E9=A1=B5=E9=9D=A2=E6=88=96?= =?UTF-8?q?=E9=87=8D=E5=A4=8D=E7=9A=84=E9=94=99=E8=AF=AF=E6=8F=90=E7=A4=BA?= =?UTF-8?q?=20Co-authored-by:=20tinkle-community=20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/components/LoginPage.tsx | 29 +++++++++++++++- web/src/components/TraderConfigModal.tsx | 15 +++------ web/src/i18n/translations.ts | 2 ++ web/src/lib/httpClient.ts | 43 ++++++++++-------------- 4 files changed, 53 insertions(+), 36 deletions(-) diff --git a/web/src/components/LoginPage.tsx b/web/src/components/LoginPage.tsx index 65c0aa02..a7d04d1a 100644 --- a/web/src/components/LoginPage.tsx +++ b/web/src/components/LoginPage.tsx @@ -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(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会自动处理登录状态 diff --git a/web/src/components/TraderConfigModal.tsx b/web/src/components/TraderConfigModal.tsx index 30e104a6..2fa53a7f 100644 --- a/web/src/components/TraderConfigModal.tsx +++ b/web/src/components/TraderConfigModal.tsx @@ -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 = 当前账户净值(包含未实现盈亏) diff --git a/web/src/i18n/translations.ts b/web/src/i18n/translations.ts index 9d17ace9..26f7102a 100644 --- a/web/src/i18n/translations.ts +++ b/web/src/i18n/translations.ts @@ -474,6 +474,7 @@ export const translations = { registrationFailed: 'Registration failed. Please try again.', verificationFailed: 'OTP verification failed. Please check the code and try again.', + sessionExpired: 'Session expired, please login again', invalidCredentials: 'Invalid email or password', weak: 'Weak', medium: 'Medium', @@ -1287,6 +1288,7 @@ export const translations = { loginFailed: '登录失败,请检查您的邮箱和密码。', registrationFailed: '注册失败,请重试。', verificationFailed: 'OTP 验证失败,请检查验证码后重试。', + sessionExpired: '登录已过期,请重新登录', invalidCredentials: '邮箱或密码错误', weak: '弱', medium: '中', diff --git a/web/src/lib/httpClient.ts b/web/src/lib/httpClient.ts index 15ebc16c..079e6bdf 100644 --- a/web/src/lib/httpClient.ts +++ b/web/src/lib/httpClient.ts @@ -6,10 +6,9 @@ * - Automatic 401 token expiration handling * - Auth state cleanup on unauthorized * - Automatic redirect to login page + * - Notification shown on login page after redirect */ -import { toast } from 'sonner' - export class HttpClient { // Singleton flag to prevent duplicate 401 handling private static isHandling401 = false @@ -21,13 +20,6 @@ export class HttpClient { HttpClient.isHandling401 = false } - /** - * Show login required notification to user - */ - private showLoginRequiredNotification(): void { - toast.warning('登录已过期,请先登录', { duration: 1800 }) - } - /** * Response interceptor - handles common HTTP errors * @@ -53,23 +45,24 @@ export class HttpClient { // Notify global listeners (AuthContext will react to this) window.dispatchEvent(new Event('unauthorized')) - // Show user-friendly notification (only once) - this.showLoginRequiredNotification() - - // Delay redirect to let user see the notification - setTimeout(() => { - // Only redirect if not already on login page - if (!window.location.pathname.includes('/login')) { - // Save current location for post-login redirect - const returnUrl = window.location.pathname + window.location.search - if (returnUrl !== '/login' && returnUrl !== '/') { - sessionStorage.setItem('returnUrl', returnUrl) - } - - window.location.href = '/login' + // Only redirect if not already on login page + if (!window.location.pathname.includes('/login')) { + // Save current location for post-login redirect + const returnUrl = window.location.pathname + window.location.search + if (returnUrl !== '/login' && returnUrl !== '/') { + sessionStorage.setItem('returnUrl', returnUrl) } - // Note: No need to reset flag since we're redirecting - }, 1500) // 1.5秒延迟,让用户看到提示 + + // Mark that user came from 401 (login page will show notification) + sessionStorage.setItem('from401', 'true') + + // Redirect immediately to login page + window.location.href = '/login' + + // Return pending promise to prevent error from being caught by SWR/React + // The notification will be shown on the login page + return new Promise(() => {}) as Promise + } throw new Error('登录已过期,请重新登录') }