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,
|
SendEmailCodeRequest,
|
||||||
SendSmsCodeRequest,
|
SendSmsCodeRequest,
|
||||||
TokenBundle,
|
TokenBundle,
|
||||||
|
TOTPVerifyRequest,
|
||||||
ValidateResetTokenResponse,
|
ValidateResetTokenResponse,
|
||||||
} from '@/types'
|
} from '@/types'
|
||||||
|
|
||||||
@@ -40,6 +41,11 @@ export function loginByPassword(data: LoginByPasswordRequest): Promise<TokenBund
|
|||||||
return post<TokenBundle>('/auth/login', data, { auth: false, credentials: 'include' })
|
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> {
|
export function loginByEmailCode(data: LoginByEmailCodeRequest): Promise<TokenBundle> {
|
||||||
return post<TokenBundle>('/auth/login/email-code', data, { auth: false, credentials: 'include' })
|
return post<TokenBundle>('/auth/login/email-code', data, { auth: false, credentials: 'include' })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,16 @@ export interface TokenBundle {
|
|||||||
refresh_token?: string
|
refresh_token?: string
|
||||||
expires_in: number
|
expires_in: number
|
||||||
user: SessionUser
|
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 {
|
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 用户登出
|
// Logout 用户登出
|
||||||
// @Summary 用户登出
|
// @Summary 用户登出
|
||||||
// @Description 使当前 access_token 和 refresh_token 失效
|
// @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("/register", r.rateLimitMiddleware.Register(), r.authHandler.Register)
|
||||||
authGroup.POST("/bootstrap-admin", r.rateLimitMiddleware.Register(), r.authHandler.BootstrapAdmin)
|
authGroup.POST("/bootstrap-admin", r.rateLimitMiddleware.Register(), r.authHandler.BootstrapAdmin)
|
||||||
authGroup.POST("/login", r.rateLimitMiddleware.Login(), r.authHandler.Login)
|
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.POST("/refresh", r.rateLimitMiddleware.Refresh(), r.authHandler.RefreshToken)
|
||||||
authGroup.GET("/capabilities", r.authHandler.GetAuthCapabilities)
|
authGroup.GET("/capabilities", r.authHandler.GetAuthCapabilities)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user