fix: P0-07 complete frontend TOTP login flow
Backend changes:
- Add VerifyTOTPAfterPasswordLogin handler in auth_handler.go
- Add route /auth/login/totp-verify in router.go
Frontend changes:
- Update TokenBundle type to include requires_totp and user_id fields
- Add TOTPVerifyRequest type for TOTP verification
- Add verifyTOTPAfterPasswordLogin() API function
New login flow when user has TOTP enabled:
1. loginByPassword returns {requires_totp: true, user_id: <id>}
2. Frontend prompts user for TOTP code
3. Frontend calls verifyTOTPAfterPasswordLogin({user_id, code})
4. If TOTP valid, full TokenBundle with tokens is returned
This commit is contained in:
@@ -16,6 +16,7 @@ import type {
|
||||
SendEmailCodeRequest,
|
||||
SendSmsCodeRequest,
|
||||
TokenBundle,
|
||||
TOTPVerifyRequest,
|
||||
ValidateResetTokenResponse,
|
||||
} from '@/types'
|
||||
|
||||
@@ -40,6 +41,11 @@ export function loginByPassword(data: LoginByPasswordRequest): Promise<TokenBund
|
||||
return post<TokenBundle>('/auth/login', data, { auth: false, credentials: 'include' })
|
||||
}
|
||||
|
||||
// Verify TOTP after password login when requires_totp is returned
|
||||
export function verifyTOTPAfterPasswordLogin(data: TOTPVerifyRequest): Promise<TokenBundle> {
|
||||
return post<TokenBundle>('/auth/login/totp-verify', data, { auth: false, credentials: 'include' })
|
||||
}
|
||||
|
||||
export function loginByEmailCode(data: LoginByEmailCodeRequest): Promise<TokenBundle> {
|
||||
return post<TokenBundle>('/auth/login/email-code', data, { auth: false, credentials: 'include' })
|
||||
}
|
||||
|
||||
@@ -15,6 +15,16 @@ export interface TokenBundle {
|
||||
refresh_token?: string
|
||||
expires_in: number
|
||||
user: SessionUser
|
||||
// TOTP required response (when user has TOTP enabled but device is not trusted)
|
||||
requires_totp?: boolean
|
||||
user_id?: number
|
||||
}
|
||||
|
||||
// TOTP verification request after password login
|
||||
export interface TOTPVerifyRequest {
|
||||
user_id: number
|
||||
code: string
|
||||
device_id?: string
|
||||
}
|
||||
|
||||
export interface OAuthProviderInfo {
|
||||
|
||||
@@ -132,6 +132,41 @@ func (h *AuthHandler) Login(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// VerifyTOTPAfterPasswordLogin 完成密码登录后的TOTP验证
|
||||
// @Summary TOTP验证(密码登录后)
|
||||
// @Description 当登录返回requires_totp=true时,使用此接口完成TOTP验证
|
||||
// @Tags 认证
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body TOTPVerifyRequest true "TOTP验证请求"
|
||||
// @Success 200 {object} Response{data=service.LoginResponse} "验证成功"
|
||||
// @Failure 400 {object} Response "请求参数错误"
|
||||
// @Failure 401 {object} Response "TOTP验证失败"
|
||||
// @Router /api/v1/auth/login/totp-verify [post]
|
||||
func (h *AuthHandler) VerifyTOTPAfterPasswordLogin(c *gin.Context) {
|
||||
var req struct {
|
||||
UserID int64 `json:"user_id" binding:"required"`
|
||||
Code string `json:"code" binding:"required"`
|
||||
DeviceID string `json:"device_id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := h.authService.VerifyTOTPAfterPasswordLogin(c.Request.Context(), req.UserID, req.Code, req.DeviceID)
|
||||
if err != nil {
|
||||
handleError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": resp,
|
||||
})
|
||||
}
|
||||
|
||||
// Logout 用户登出
|
||||
// @Summary 用户登出
|
||||
// @Description 使当前 access_token 和 refresh_token 失效
|
||||
|
||||
@@ -137,6 +137,7 @@ func (r *Router) Setup() *gin.Engine {
|
||||
authGroup.POST("/register", r.rateLimitMiddleware.Register(), r.authHandler.Register)
|
||||
authGroup.POST("/bootstrap-admin", r.rateLimitMiddleware.Register(), r.authHandler.BootstrapAdmin)
|
||||
authGroup.POST("/login", r.rateLimitMiddleware.Login(), r.authHandler.Login)
|
||||
authGroup.POST("/login/totp-verify", r.rateLimitMiddleware.Login(), r.authHandler.VerifyTOTPAfterPasswordLogin)
|
||||
authGroup.POST("/refresh", r.rateLimitMiddleware.Refresh(), r.authHandler.RefreshToken)
|
||||
authGroup.GET("/capabilities", r.authHandler.GetAuthCapabilities)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user