fix: close auth, permission, contract and e2e review blockers

This commit is contained in:
Your Name
2026-05-28 15:19:13 +08:00
parent f33e39a702
commit 6be90ddff8
29 changed files with 1356 additions and 259 deletions

View File

@@ -30,11 +30,74 @@ type AuthHandler struct {
authService *service.AuthService
}
const (
refreshTokenCookieName = "ums_refresh_token"
sessionPresenceCookieName = "ums_session_present"
)
// NewAuthHandler creates a new AuthHandler
func NewAuthHandler(authService *service.AuthService) *AuthHandler {
return &AuthHandler{authService: authService}
}
func isSecureRequest(c *gin.Context) bool {
if c == nil || c.Request == nil {
return false
}
if c.Request.TLS != nil {
return true
}
return strings.EqualFold(c.GetHeader("X-Forwarded-Proto"), "https")
}
func (h *AuthHandler) setSessionCookies(c *gin.Context, resp *service.LoginResponse) {
if c == nil || resp == nil || strings.TrimSpace(resp.RefreshToken) == "" || h == nil || h.authService == nil {
return
}
maxAge := int(h.authService.RefreshTokenTTLSeconds())
secure := isSecureRequest(c)
http.SetCookie(c.Writer, &http.Cookie{
Name: refreshTokenCookieName,
Value: resp.RefreshToken,
Path: "/",
HttpOnly: true,
Secure: secure,
SameSite: http.SameSiteLaxMode,
MaxAge: maxAge,
})
http.SetCookie(c.Writer, &http.Cookie{
Name: sessionPresenceCookieName,
Value: "1",
Path: "/",
HttpOnly: false,
Secure: secure,
SameSite: http.SameSiteLaxMode,
MaxAge: maxAge,
})
}
func clearCookie(c *gin.Context, name string) {
if c == nil {
return
}
http.SetCookie(c.Writer, &http.Cookie{
Name: name,
Value: "",
Path: "/",
HttpOnly: name == refreshTokenCookieName,
Secure: isSecureRequest(c),
SameSite: http.SameSiteLaxMode,
MaxAge: -1,
Expires: time.Unix(0, 0),
})
}
func clearSessionCookies(c *gin.Context) {
clearCookie(c, refreshTokenCookieName)
clearCookie(c, sessionPresenceCookieName)
}
// Register 用户注册
// @Summary 用户注册
// @Description 用户注册新账号,支持用户名+密码或手机号注册
@@ -130,6 +193,7 @@ func (h *AuthHandler) Login(c *gin.Context) {
return
}
h.setSessionCookies(c, resp)
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "success",
@@ -150,21 +214,23 @@ func (h *AuthHandler) Login(c *gin.Context) {
// @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"`
UserID int64 `json:"user_id" binding:"required"`
Code string `json:"code" binding:"required"`
DeviceID string `json:"device_id"`
TempToken string `json:"temp_token" binding:"required"`
}
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)
resp, err := h.authService.VerifyTOTPAfterPasswordLogin(c.Request.Context(), req.UserID, req.Code, req.DeviceID, req.TempToken)
if err != nil {
handleError(c, err)
return
}
h.setSessionCookies(c, resp)
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "success",
@@ -197,6 +263,12 @@ func (h *AuthHandler) Logout(c *gin.Context) {
}
}
if req.RefreshToken == "" {
if cookie, err := c.Request.Cookie(refreshTokenCookieName); err == nil {
req.RefreshToken = cookie.Value
}
}
username, _ := c.Get("username")
usernameStr, _ := username.(string)
@@ -204,7 +276,11 @@ func (h *AuthHandler) Logout(c *gin.Context) {
AccessToken: req.AccessToken,
RefreshToken: req.RefreshToken,
}
_ = h.authService.Logout(c.Request.Context(), usernameStr, logoutReq)
if err := h.authService.Logout(c.Request.Context(), usernameStr, logoutReq); err != nil {
handleError(c, err)
return
}
clearSessionCookies(c)
c.JSON(http.StatusOK, gin.H{"message": "logged out"})
}
@@ -222,20 +298,28 @@ func (h *AuthHandler) Logout(c *gin.Context) {
// @Router /api/v1/auth/refresh-token [post]
func (h *AuthHandler) RefreshToken(c *gin.Context) {
var req struct {
RefreshToken string `json:"refresh_token" binding:"required"`
RefreshToken string `json:"refresh_token"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
_ = c.ShouldBindJSON(&req)
if strings.TrimSpace(req.RefreshToken) == "" {
if cookie, err := c.Request.Cookie(refreshTokenCookieName); err == nil {
req.RefreshToken = cookie.Value
}
}
if strings.TrimSpace(req.RefreshToken) == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "refresh_token is required"})
return
}
resp, err := h.authService.RefreshToken(c.Request.Context(), req.RefreshToken)
if err != nil {
clearSessionCookies(c)
handleError(c, err)
return
}
h.setSessionCookies(c, resp)
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "success",
@@ -315,7 +399,7 @@ func (h *AuthHandler) GetAuthCapabilities(c *gin.Context) {
// @Router /api/v1/auth/oauth/{provider} [get]
func (h *AuthHandler) OAuthLogin(c *gin.Context) {
provider := c.Param("provider")
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "OAuth not configured", "data": gin.H{"provider": provider}})
c.JSON(http.StatusServiceUnavailable, gin.H{"code": http.StatusServiceUnavailable, "message": "OAuth login is not configured", "data": gin.H{"provider": provider}})
}
// OAuthCallback OAuth回调
@@ -327,7 +411,7 @@ func (h *AuthHandler) OAuthLogin(c *gin.Context) {
// @Success 200 {object} Response "OAuth未配置"
// @Router /api/v1/auth/oauth/{provider}/callback [get]
func (h *AuthHandler) OAuthCallback(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "OAuth not configured"})
c.JSON(http.StatusServiceUnavailable, gin.H{"code": http.StatusServiceUnavailable, "message": "OAuth callback is not configured"})
}
// OAuthExchange OAuth令牌交换
@@ -340,7 +424,7 @@ func (h *AuthHandler) OAuthCallback(c *gin.Context) {
// @Success 200 {object} Response "OAuth未配置"
// @Router /api/v1/auth/oauth/{provider}/exchange [post]
func (h *AuthHandler) OAuthExchange(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "OAuth not configured"})
c.JSON(http.StatusServiceUnavailable, gin.H{"code": http.StatusServiceUnavailable, "message": "OAuth exchange is not configured"})
}
// GetEnabledOAuthProviders 获取已启用的OAuth提供商
@@ -481,6 +565,7 @@ func (h *AuthHandler) LoginByEmailCode(c *gin.Context) {
}()
}
h.setSessionCookies(c, resp)
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "success",
@@ -545,6 +630,7 @@ func (h *AuthHandler) BootstrapAdmin(c *gin.Context) {
return
}
h.setSessionCookies(c, resp)
c.JSON(http.StatusCreated, gin.H{
"code": 0,
"message": "success",
@@ -561,7 +647,7 @@ func (h *AuthHandler) BootstrapAdmin(c *gin.Context) {
// @Success 200 {object} Response "功能未配置"
// @Router /api/v1/auth/email/bind/send [post]
func (h *AuthHandler) SendEmailBindCode(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "email bind not configured"})
c.JSON(http.StatusServiceUnavailable, gin.H{"code": http.StatusServiceUnavailable, "message": "email binding is not configured"})
}
// BindEmail 绑定邮箱
@@ -573,7 +659,7 @@ func (h *AuthHandler) SendEmailBindCode(c *gin.Context) {
// @Success 200 {object} Response "功能未配置"
// @Router /api/v1/auth/email/bind [post]
func (h *AuthHandler) BindEmail(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "email bind not configured"})
c.JSON(http.StatusServiceUnavailable, gin.H{"code": http.StatusServiceUnavailable, "message": "email binding is not configured"})
}
// UnbindEmail 解绑邮箱
@@ -585,7 +671,7 @@ func (h *AuthHandler) BindEmail(c *gin.Context) {
// @Success 200 {object} Response "功能未配置"
// @Router /api/v1/auth/email/unbind [post]
func (h *AuthHandler) UnbindEmail(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "email unbind not configured"})
c.JSON(http.StatusServiceUnavailable, gin.H{"code": http.StatusServiceUnavailable, "message": "email binding is not configured"})
}
// SendPhoneBindCode 发送手机绑定验证码
@@ -597,7 +683,7 @@ func (h *AuthHandler) UnbindEmail(c *gin.Context) {
// @Success 200 {object} Response "功能未配置"
// @Router /api/v1/auth/phone/bind/send [post]
func (h *AuthHandler) SendPhoneBindCode(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "phone bind not configured"})
c.JSON(http.StatusServiceUnavailable, gin.H{"code": http.StatusServiceUnavailable, "message": "phone binding is not configured"})
}
// BindPhone 绑定手机号
@@ -609,7 +695,7 @@ func (h *AuthHandler) SendPhoneBindCode(c *gin.Context) {
// @Success 200 {object} Response "功能未配置"
// @Router /api/v1/auth/phone/bind [post]
func (h *AuthHandler) BindPhone(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "phone bind not configured"})
c.JSON(http.StatusServiceUnavailable, gin.H{"code": http.StatusServiceUnavailable, "message": "phone binding is not configured"})
}
// UnbindPhone 解绑手机号
@@ -621,7 +707,7 @@ func (h *AuthHandler) BindPhone(c *gin.Context) {
// @Success 200 {object} Response "功能未配置"
// @Router /api/v1/auth/phone/unbind [post]
func (h *AuthHandler) UnbindPhone(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "phone unbind not configured"})
c.JSON(http.StatusServiceUnavailable, gin.H{"code": http.StatusServiceUnavailable, "message": "phone binding is not configured"})
}
// GetSocialAccounts 获取社交账号列表
@@ -645,7 +731,7 @@ func (h *AuthHandler) GetSocialAccounts(c *gin.Context) {
// @Success 200 {object} Response "功能未配置"
// @Router /api/v1/auth/social/bind [post]
func (h *AuthHandler) BindSocialAccount(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "social binding not configured"})
c.JSON(http.StatusServiceUnavailable, gin.H{"code": http.StatusServiceUnavailable, "message": "social binding is not configured"})
}
// UnbindSocialAccount 解绑社交账号
@@ -657,7 +743,7 @@ func (h *AuthHandler) BindSocialAccount(c *gin.Context) {
// @Success 200 {object} Response "功能未配置"
// @Router /api/v1/auth/social/unbind [post]
func (h *AuthHandler) UnbindSocialAccount(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "social unbinding not configured"})
c.JSON(http.StatusServiceUnavailable, gin.H{"code": http.StatusServiceUnavailable, "message": "social binding is not configured"})
}
func (h *AuthHandler) SupportsEmailCodeLogin() bool {