From ca7669af2e5e49d4fcf010726973f5cf48dc2a40 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Wed, 5 Nov 2025 14:24:30 +0800 Subject: [PATCH] docs(login): update LOGIN_FLOW.md and clarify MFA setup flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove needMfa field from login API responses - MFA setup redirection now only occurs in registration flow - Registration always redirects to /panel/account?NeedSetupMfa=1 - Update documentation to reflect simplified login flow - Clarify that login API returns error: 'mfa_code_required' instead of needMfa: true 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- dashboard-fresh/docs/LOGIN_FLOW.md | 58 +++++++++++++++++------- dashboard-fresh/islands/RegisterForm.tsx | 13 +++--- dashboard-fresh/routes/api/auth/login.ts | 49 ++------------------ 3 files changed, 51 insertions(+), 69 deletions(-) diff --git a/dashboard-fresh/docs/LOGIN_FLOW.md b/dashboard-fresh/docs/LOGIN_FLOW.md index 50b3018..4e33444 100644 --- a/dashboard-fresh/docs/LOGIN_FLOW.md +++ b/dashboard-fresh/docs/LOGIN_FLOW.md @@ -1,26 +1,36 @@ - 1. 核心登录 API - routes/api/auth/login.ts + ## 已完成的功能 - - ✅ 重构为多步骤登录流程 +### 1. 核心登录 API - routes/api/auth/login.ts + + - ✅ 重构为多步骤登录流程(支持 3 个步骤) - ✅ 使用新的 getAuthUrl() 配置加载器 - ✅ 添加详细的日志输出 - - ✅ 识别 mfa_code_required 错误 - - ✅ 即使没有 mfaToken 也正确返回 needMfa: true + - ✅ **移除 MFA 设置重定向逻辑(needMfa: true)** + - ✅ **MFA 设置跳转现在只在注册流程中处理** + - ✅ 向后兼容:无 step 参数时默认为登录行为 - 2. MFA 验证 API - routes/api/auth/mfa/verify/index.ts +### 2. MFA 验证 API - routes/api/auth/mfa/verify/index.ts - - ✅ 更新使用 getAuthUrl() 替代旧的 getAccountServiceApiBaseUrl() + - ✅ 更新使用 getAuthUrl() 替代旧的配置方式 - ✅ 添加详细的日志输出 - ✅ 添加 10 秒超时控制 - ✅ 改进错误处理 - 3. MFA 状态检查 API - routes/api/auth/mfa/status/index.ts +### 3. MFA 状态检查 API - routes/api/auth/mfa/status/index.ts - ✅ 更新使用 getAuthUrl() 替代旧的配置方式 - ✅ 添加详细的日志输出 - ✅ 添加 10 秒超时控制 - ✅ 添加错误处理,失败时返回 totpEnabled: false - 4. 运行时配置加载器 - server/runtime-loader.deno.ts +### 4. 注册表单 - islands/RegisterForm.tsx + + - ✅ 多步骤注册流程 + - ✅ 邮箱验证码验证 + - ✅ 自动登录 + - ✅ **注册成功后总是重定向到 MFA 设置页面** + +### 5. 运行时配置加载器 - config/runtime-loader.ts - ✅ 纯 Deno 实现 - ✅ 支持 SIT/PROD 环境切换 @@ -28,12 +38,25 @@ - ✅ 环境变量覆盖 - ✅ 配置缓存 - 5. 开发工具 + 📊 完整的流程说明 - - ✅ dev-local.sh - 本地开发启动脚本 - - ✅ test-login.sh - 登录 API 测试脚本 + ### 注册流程 (RegisterForm.tsx) - 📊 完整的登录流程 + 1. 用户填写邮箱和密码 + 2. 请求发送验证码:POST /api/auth/register/send + 3. 用户输入验证码 + 4. 验证邮箱:POST /api/auth/register/verify + 5. 完成注册:POST /api/auth/register + 6. 自动登录:POST /api/auth/login + 7. **注册成功后,总是重定向到 `/panel/account?NeedSetupMfa=1`** + 8. 用户可以在 MFA 设置页面选择启用或跳过 MFA + + ### 登录流程 + + **重要说明**: + - 登录 API **永远不会**返回 `needMfa: true` + - `/panel/account?NeedSetupMfa=1` 的重定向**只在注册流程**中处理 + - 日常登录时,如果需要 TOTP,返回 `needMfa: false` + `error: 'mfa_code_required'` 情况 1:用户未启用 MFA @@ -55,9 +78,9 @@ 3. 第一次提交(未输入 TOTP):POST /api/auth/login { email, password } - ← { success: false, error: "mfa_code_required", needMfa: true } + ← { success: false, error: "mfa_code_required", needMfa: false } - 4. 前端显示错误,要求输入 TOTP + 4. 前端看到 error 是 mfa_code_required,显示 TOTP 输入框(不跳转) 5. 第二次提交(带 TOTP):POST /api/auth/login { email, password, totp: "123456" } @@ -67,14 +90,15 @@ 6. ✅ 登录成功 - 情况 3:使用独立的 MFA 验证 API + 情况 3:使用独立的 MFA 验证 API(不推荐用于日常登录) 1. 第一次登录(不带 TOTP):POST /api/auth/login { email, password } - ← { success: false, error: "mfa_code_required", needMfa: true } - + Set-Cookie: mfa_token=xxx + ← { success: false, error: "mfa_code_required", needMfa: false } + 注意:日常登录时不会返回 mfa_token cookie 2. MFA 验证:POST /api/auth/mfa/verify + 注意:此API主要用于MFA设置流程,日常登录推荐使用情况2的方式 Cookie: mfa_token=xxx { code: "123456" } ← { success: true } + session cookie diff --git a/dashboard-fresh/islands/RegisterForm.tsx b/dashboard-fresh/islands/RegisterForm.tsx index 4c8c8f5..a2aa89f 100644 --- a/dashboard-fresh/islands/RegisterForm.tsx +++ b/dashboard-fresh/islands/RegisterForm.tsx @@ -304,13 +304,12 @@ export default function RegisterForm({ language, onSuccess }: RegisterFormProps) if (loginResponse.ok) { success.value = t.registrationComplete - if (onSuccess) { - onSuccess() - } else { - setTimeout(() => { - globalThis.location.href = '/' - }, 1000) - } + + // For first-time registration, always redirect to MFA setup page + // Users can choose to skip MFA setup if they want + setTimeout(() => { + globalThis.location.href = '/panel/account?NeedSetupMfa=1' + }, 1000) } else { // Registration succeeded but login failed, redirect to login page setTimeout(() => { diff --git a/dashboard-fresh/routes/api/auth/login.ts b/dashboard-fresh/routes/api/auth/login.ts index 04bfe84..244966d 100644 --- a/dashboard-fresh/routes/api/auth/login.ts +++ b/dashboard-fresh/routes/api/auth/login.ts @@ -49,7 +49,6 @@ interface LoginResponse { expiresAt?: string error?: string mfaToken?: string - needMfa?: boolean mfaEnabled?: boolean } @@ -63,7 +62,6 @@ interface VerifyMfaResponse { interface ApiResponse { success: boolean error?: string | null - needMfa?: boolean mfaEnabled?: boolean exists?: boolean [key: string]: unknown @@ -116,7 +114,6 @@ function errorResponse( { success: false, error, - needMfa: false, ...additionalData, }, status, @@ -253,61 +250,26 @@ async function handleLogin(payload: LoginPayload): Promise { { success: true, error: null, - needMfa: false, }, 200, headers, ) } - // MFA required + // Authentication failed - MFA is determined by frontend precheck + // Note: Frontend should call GET /api/auth/mfa/status?identifier=email to check MFA status const errorCode = typeof data?.error === 'string' ? data.error : 'authentication_failed' - const needsMfa = Boolean( - data?.needMfa || - errorCode === 'mfa_required' || - errorCode === 'mfa_code_required' || - errorCode === 'mfa_setup_required', - ) - console.log('[login/handleLogin] Error code:', errorCode, 'Needs MFA:', needsMfa, 'Has mfaToken:', !!data?.mfaToken) + console.log('[login/handleLogin] ✗ Authentication failed:', errorCode, 'Has mfaToken:', !!data?.mfaToken) - // If MFA is required, return appropriate response - if (needsMfa) { - const headers = new Headers() - - // If backend provided mfaToken, set it as cookie - if (data?.mfaToken) { - applyMfaCookie(headers, data.mfaToken) - console.log('[login/handleLogin] → MFA required, mfa_token set') - } else { - console.log('[login/handleLogin] → MFA required, but no mfaToken from backend') - } - - clearSessionCookie(headers) - - return jsonResponse( - { - success: false, - error: errorCode, - needMfa: true, - }, - 401, - headers, - ) - } - - // Authentication failed const headers = new Headers() clearSessionCookie(headers) clearMfaCookie(headers) - console.log('[login/handleLogin] ✗ Authentication failed:', errorCode) - return jsonResponse( { success: false, error: errorCode, - needMfa: false, }, status || 401, headers, @@ -323,7 +285,6 @@ async function handleLogin(payload: LoginPayload): Promise { { success: false, error: 'account_service_unreachable', - needMfa: false, }, 502, headers, @@ -376,7 +337,6 @@ async function handleVerifyMfa( { success: true, error: null, - needMfa: false, }, 200, headers, @@ -388,7 +348,7 @@ async function handleVerifyMfa( console.log('[login] ✗ MFA verification failed:', errorCode) - return errorResponse(errorCode, status || 401, { needMfa: true }) + return errorResponse(errorCode, status || 401) } catch (error) { console.error('[login] verify_mfa error:', error) return errorResponse('account_service_unreachable', 502) @@ -476,7 +436,6 @@ export const handler: Handlers = { { success: true, error: null, - needMfa: false, }, 200, headers,