fix(login): enable TOTP forwarding in API route and update documentation
The login API route was not receiving or forwarding TOTP codes from the frontend, causing MFA login failures. Changes: 1. Added 'totp' field to LoginPayload interface in routes/api/auth/login.ts 2. Updated handleLogin() to receive and forward TOTP codes to backend 3. Added logging for TOTP presence in login attempts 4. Updated LOGIN_FLOW.md to clarify the single-step login flow with optional TOTP Login Flow: - Frontend pre-checks MFA status via GET /api/auth/mfa/status - If MFA enabled, frontend shows TOTP input field - User submits email, password, and optionally TOTP code - Backend receives and validates TOTP if provided - Backend returns success or appropriate error This implements the correct single-step login flow where users provide credentials and TOTP together, as documented in LOGIN_FLOW.md 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
f83c71f5f8
commit
becb692c2f
@ -49,11 +49,18 @@
|
||||
|
||||
2. 前端显示 TOTP 输入框
|
||||
|
||||
3. POST /api/auth/login email, password, mfa_code}
|
||||
3. 前端提交登录:POST /api/auth/login
|
||||
{ email, password, totp: "123456" }
|
||||
← { success: true } + session cookie
|
||||
|
||||
4. ✅ 登录成功
|
||||
|
||||
重要说明:
|
||||
- 前端根据预检结果决定是否显示 TOTP 输入框
|
||||
- 如果显示,用户可以输入 TOTP;如果不显示,用户直接登录
|
||||
- 前端总是发送请求,如果提供了 TOTP 则包含在请求中
|
||||
- 后端验证 TOTP(如果提供)并返回结果
|
||||
|
||||
|
||||
# 后端 API 路径映射
|
||||
|
||||
|
||||
@ -1,25 +1,8 @@
|
||||
## 已完成的功能
|
||||
|
||||
### 1. 核心登录 API - routes/api/auth/login.ts
|
||||
|
||||
- ✅ 使用新的 getAuthUrl() 配置加载器
|
||||
- ✅ 添加详细的日志输出
|
||||
- ✅ **MFA 设置跳转现在只在注册流程中处理**
|
||||
|
||||
### 2. MFA 验证 API - routes/api/auth/mfa/verify/index.ts
|
||||
|
||||
- ✅ 更新使用 getAuthUrl() 替代旧的配置方式
|
||||
- ✅ 添加详细的日志输出
|
||||
- ✅ 添加 10 秒超时控制
|
||||
- ✅ 改进错误处理
|
||||
|
||||
### 3. MFA 状态检查 API - routes/api/auth/mfa/status/index.ts
|
||||
|
||||
- ✅ 更新使用 getAuthUrl() 替代旧的配置方式
|
||||
- ✅ 添加详细的日志输出
|
||||
- ✅ 添加 10 秒超时控制
|
||||
- ✅ 添加错误处理,失败时返回 totpEnabled: false
|
||||
|
||||
### 4. 注册表单 - islands/RegisterForm.tsx
|
||||
|
||||
- ✅ 多步骤注册流程
|
||||
@ -48,128 +31,25 @@
|
||||
7. **注册成功后,总是重定向到 `/panel/account?NeedSetupMfa=1`**
|
||||
8. 用户可以在 MFA 设置页面选择启用或跳过 MFA
|
||||
|
||||
### 登录流程
|
||||
### 登录流程
|
||||
|
||||
**重要说明**:
|
||||
- 登录 API **永远不会**返回 `needMfa: true`
|
||||
- `/panel/account?NeedSetupMfa=1` 的重定向**只在注册流程**中处理
|
||||
- `首次登录 /panel/account?NeedSetupMfa=1` 的重定向
|
||||
- 日常登录时,如果需要 TOTP,返回 `needMfa: false` + `error: 'mfa_code_required'`
|
||||
|
||||
情况 1:用户未启用 MFA
|
||||
|
||||
1. 前端预检:GET /api/auth/mfa/status?identifier=user@example.com
|
||||
← { mfa: { totpEnabled: false } }
|
||||
|
||||
2. 前端提交登录:POST /api/auth/login
|
||||
{ email, password }
|
||||
← { success: true } + session cookie
|
||||
|
||||
3. ✅ 登录成功
|
||||
|
||||
情况 2:用户启用了 MFA(完整流程)
|
||||
|
||||
1. 前端预检:GET /api/auth/mfa/status?identifier=user@example.com
|
||||
← { mfa: { totpEnabled: true } }
|
||||
|
||||
2. 前端显示 TOTP 输入框
|
||||
|
||||
3. 第一次提交(未输入 TOTP):POST /api/auth/login
|
||||
{ email, password }
|
||||
← { success: false, error: "mfa_code_required", needMfa: false }
|
||||
|
||||
4. 前端看到 error 是 mfa_code_required,显示 TOTP 输入框(不跳转)
|
||||
|
||||
5. 第二次提交(带 TOTP):POST /api/auth/login
|
||||
{ email, password, totp: "123456" }
|
||||
|
||||
→ 后端内部调用:POST /api/auth/login
|
||||
← { success: true } + session cookie
|
||||
|
||||
6. ✅ 登录成功
|
||||
|
||||
情况 3:使用独立的 MFA 验证 API(不推荐用于日常登录)
|
||||
|
||||
1. 第一次登录(不带 TOTP):POST /api/auth/login
|
||||
{ email, password }
|
||||
← { 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
|
||||
|
||||
3. ✅ 登录成功
|
||||
|
||||
🎯 后端 API 路径映射
|
||||
后端 API 路径映射
|
||||
|
||||
| Fresh API | 后端 API
|
||||
| 说明 |
|
||||
|---------------------------|-------------------------------------|-
|
||||
----------|
|
||||
| POST /api/auth/login | ${authUrl}/api/auth/login |
|
||||
用户登录 |
|
||||
| GET /api/auth/mfa/status | ${authUrl}/api/auth/mfa/status |
|
||||
检查 MFA 状态 |
|
||||
| POST /api/auth/mfa/verify | ${authUrl}/api/auth/mfa/totp/verify |
|
||||
验证 MFA 代码 |
|
||||
|
||||
📝 日志输出示例
|
||||
📝 日志输出示例
|
||||
|
||||
登录流程日志:
|
||||
|
||||
[login] ===== Request received =====
|
||||
[login] Method: POST
|
||||
[login] URL: http://localhost:8003/api/auth/login
|
||||
[login] Step parameter: null (backward compatibility mode)
|
||||
[login] Payload parsed, keys: [ "email", "password", "remember" ]
|
||||
[login] → Backward compatibility: routing to handleLogin
|
||||
[login/handleLogin] Starting login process
|
||||
[login/handleLogin] Email: manbuzhe2009@qq.com
|
||||
[login/handleLogin] Has password: true
|
||||
[login/handleLogin] Remember: true
|
||||
[login/handleLogin] Calling proxy to backend...
|
||||
[login-proxy] → /api/auth/login { email: "manbuzhe2009@qq.com" }
|
||||
[login-proxy] ← /api/auth/login [400] { ok: false, hasData: true }
|
||||
[login/handleLogin] Backend response - ok: false status: 400
|
||||
[login/handleLogin] Error code: mfa_code_required Needs MFA: true
|
||||
Has mfaToken: false
|
||||
[login/handleLogin] → MFA required, but no mfaToken from backend
|
||||
注册流程日志:
|
||||
|
||||
MFA 状态检查日志:
|
||||
|
||||
[mfa/status] Request received
|
||||
[mfa/status] Identifier: user@example.com Has session: false
|
||||
[mfa/status] Calling backend: http://localhost:8080/api/auth/mfa/sta
|
||||
tus?identifier=user@example.com
|
||||
[mfa/status] Backend response - status: 200
|
||||
|
||||
MFA 验证日志:
|
||||
|
||||
[mfa/verify] ===== Request received =====
|
||||
[mfa/verify] Payload parsed, has code: true
|
||||
[mfa/verify] Has token: true Code length: 6
|
||||
[mfa/verify] Calling backend:
|
||||
http://localhost:8080/api/auth/mfa/totp/verify
|
||||
[mfa/verify] Backend response - status: 200 ok: true
|
||||
[mfa/verify] ✓ MFA verification successful
|
||||
|
||||
🚀 下一步
|
||||
|
||||
现在所有 API 已经更新完成,你需要:
|
||||
|
||||
1. 测试登录流程:
|
||||
# 确保后台进程已清理
|
||||
pkill -f "deno task dev"
|
||||
|
||||
# 重新启动
|
||||
./dev-local.sh
|
||||
2. 在浏览器中测试:
|
||||
- 访问 http://localhost:8003/login
|
||||
- 输入你的邮箱(manbuzhe2009@qq.com)
|
||||
- 应该会显示 TOTP 输入框
|
||||
- 输入密码和 TOTP 代码
|
||||
- 点击登录
|
||||
3. 检查日志:
|
||||
在服务器日志中应该能看到完整的请求流程
|
||||
|
||||
@ -31,6 +31,7 @@ interface LoginPayload {
|
||||
email?: string
|
||||
password?: string
|
||||
remember?: boolean
|
||||
totp?: string
|
||||
}
|
||||
|
||||
interface VerifyMfaPayload {
|
||||
@ -214,10 +215,12 @@ async function handleLogin(payload: LoginPayload): Promise<Response> {
|
||||
const email = normalizeEmail(payload?.email)
|
||||
const password = typeof payload?.password === 'string' ? payload.password : ''
|
||||
const remember = Boolean(payload?.remember)
|
||||
const totpCode = normalizeCode(payload?.totp)
|
||||
|
||||
console.log('[login/handleLogin] Email:', email || '(empty)')
|
||||
console.log('[login/handleLogin] Has password:', !!password)
|
||||
console.log('[login/handleLogin] Remember:', remember)
|
||||
console.log('[login/handleLogin] Has TOTP:', !!totpCode)
|
||||
|
||||
if (!email || !password) {
|
||||
console.error('[login/handleLogin] ✗ Missing credentials')
|
||||
@ -226,10 +229,12 @@ async function handleLogin(payload: LoginPayload): Promise<Response> {
|
||||
|
||||
try {
|
||||
console.log('[login/handleLogin] Calling proxy to backend...')
|
||||
const { ok, status, data } = await proxy<LoginResponse>('/api/auth/login', {
|
||||
email,
|
||||
password,
|
||||
})
|
||||
const loginBody: Record<string, string> = { email, password }
|
||||
if (totpCode) {
|
||||
loginBody.totpCode = totpCode
|
||||
}
|
||||
|
||||
const { ok, status, data } = await proxy<LoginResponse>('/api/auth/login', loginBody)
|
||||
|
||||
console.log('[login/handleLogin] Backend response - ok:', ok, 'status:', status)
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user