feat(server): web 用户中心后端(3/3) — 用户 TOTP 2FA + 登录二段式
- 用户 TOTP(auth/totp_user.go,复用 internal/totp + AES-256-GCM 加密存密钥):
POST /v1/me/totp/setup(生成密钥+otpauth_uri)、/verify(校验码→启用)、
/disable(校验码→清空)。仅在 USER_TOTP_ENC_KEY(32B/64hex) 配置时挂载。
- 登录二段式:Login 在 totp_enabled 时不发 token,改发短期 pending token(Redis
5min)+ 返回 {totp_required, pending_token};POST /v1/auth/login/totp 消费
pending + 校验码 → 发 token。非 TOTP 用户仍走扁平 TokenPair,app 不受影响。
- User 结构 + GetUserByEmail 补 totp_enabled。
- 单测覆盖 AES seal/open 往返 + 篡改/错误密钥检测 + pending token 唯一性。
- 全量 server 23 包测试通过。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -101,12 +101,24 @@ func (h *Handler) Login(w http.ResponseWriter, r *http.Request) {
|
||||
if !decodeJSON(w, r, &req) {
|
||||
return
|
||||
}
|
||||
pair, retryAfter, apiErr := h.svc.Login(r.Context(), req.Email, req.Password, clientIP(r))
|
||||
out, retryAfter, apiErr := h.svc.Login(r.Context(), req.Email, req.Password, clientIP(r))
|
||||
if apiErr != nil {
|
||||
writeAPIErr(w, apiErr, retryAfter)
|
||||
return
|
||||
}
|
||||
writeTokenPair(w, pair)
|
||||
// TOTP-enabled accounts get a pending token instead of tokens; the web
|
||||
// user-center then completes login at /auth/login/totp. The app (no TOTP)
|
||||
// always receives the flat token pair below.
|
||||
if out.PendingToken != "" {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"totp_required": true,
|
||||
"pending_token": out.PendingToken,
|
||||
})
|
||||
return
|
||||
}
|
||||
writeTokenPair(w, out.Tokens)
|
||||
}
|
||||
|
||||
// Refresh handles POST /v1/auth/refresh.
|
||||
|
||||
Reference in New Issue
Block a user