fix: P2 security and correctness issues

P2-10: Change ActivateEmail from GET to POST - token now passed in
request body instead of URL query parameter for better security

P2-11: Change ValidateResetToken from GET to POST - token now passed
in request body instead of URL query parameter to prevent log leakage

P2-12: Note - /uploads static exposure remains (requires architectural
decision about file serving)

P2-13: cursor.Encode() now checks and returns empty string on JSON
marshaling error instead of silently ignoring

P2-14: initDefaultData and ensurePermissions now properly check and
propagate errors from RolePermission creation, and createDefaultPermissions
aggregates errors instead of silently continuing

P2-15: NewJWT now returns (nil, error) on initialization failure
instead of a partially initialized object. All callers updated to handle
the error return.

Backend routes updated:
- POST /auth/activate-email (was GET /activate)
- POST /auth/password/validate (was GET /reset-password)

Frontend updated to match new API endpoints.
This commit is contained in:
2026-04-18 20:48:11 +08:00
parent a754545072
commit adb251e4ad
13 changed files with 75 additions and 48 deletions

View File

@@ -20,6 +20,11 @@ func newBackgroundCtx(timeoutSec int) (context.Context, context.CancelFunc) {
return context.WithTimeout(context.Background(), time.Duration(timeoutSec)*time.Second)
}
// ActivateEmailRequest 邮箱激活请求
type ActivateEmailRequest struct {
Token string `json:"token" binding:"required"`
}
// AuthHandler handles authentication requests
type AuthHandler struct {
authService *service.AuthService
@@ -353,19 +358,20 @@ func (h *AuthHandler) GetEnabledOAuthProviders(c *gin.Context) {
// @Summary 激活用户邮箱
// @Description 使用邮箱激活token激活用户账号
// @Tags 邮箱认证
// @Accept json
// @Produce json
// @Param token query string true "激活token"
// @Param request body ActivateEmailRequest true "激活请求"
// @Success 200 {object} Response "激活成功"
// @Failure 400 {object} Response "token缺失"
// @Failure 401 {object} Response "token无效或已过期"
// @Router /api/v1/auth/activate-email [post]
func (h *AuthHandler) ActivateEmail(c *gin.Context) {
token := c.Query("token")
if token == "" {
var req ActivateEmailRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "token is required"})
return
}
if err := h.authService.ActivateEmail(c.Request.Context(), token); err != nil {
if err := h.authService.ActivateEmail(c.Request.Context(), req.Token); err != nil {
handleError(c, err)
return
}

View File

@@ -27,6 +27,11 @@ func NewPasswordResetHandlerWithSMS(passwordResetService *service.PasswordResetS
}
}
// ValidateResetTokenRequest 验证重置令牌请求
type ValidateResetTokenRequest struct {
Token string `json:"token" binding:"required"`
}
// ForgotPassword 忘记密码
// @Summary 忘记密码
// @Description 请求密码重置邮件
@@ -59,19 +64,20 @@ func (h *PasswordResetHandler) ForgotPassword(c *gin.Context) {
// @Summary 验证密码重置 Token
// @Description 验证密码重置链接中的 Token 是否有效
// @Tags 密码重置
// @Accept json
// @Produce json
// @Param token query string true "重置 Token"
// @Param request body ValidateResetTokenRequest true "重置 Token"
// @Success 200 {object} Response{data=ValidateTokenResponse} "Token验证结果"
// @Failure 400 {object} Response "请求参数错误"
// @Router /api/v1/auth/password/validate [get]
// @Router /api/v1/auth/password/validate [post]
func (h *PasswordResetHandler) ValidateResetToken(c *gin.Context) {
token := c.Query("token")
if token == "" {
var req ValidateResetTokenRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "token is required"})
return
}
valid, err := h.passwordResetService.ValidateResetToken(c.Request.Context(), token)
valid, err := h.passwordResetService.ValidateResetToken(c.Request.Context(), req.Token)
if err != nil {
handleError(c, err)
return

View File

@@ -141,7 +141,7 @@ func (r *Router) Setup() *gin.Engine {
authGroup.POST("/refresh", r.rateLimitMiddleware.Refresh(), r.authHandler.RefreshToken)
authGroup.GET("/capabilities", r.authHandler.GetAuthCapabilities)
authGroup.GET("/activate", r.authHandler.ActivateEmail)
authGroup.POST("/activate-email", r.authHandler.ActivateEmail)
authGroup.POST("/resend-activation", r.authHandler.ResendActivationEmail)
if r.authHandler.SupportsEmailCodeLogin() {
@@ -156,7 +156,7 @@ func (r *Router) Setup() *gin.Engine {
if r.passwordResetHandler != nil {
authGroup.POST("/forgot-password", r.passwordResetHandler.ForgotPassword)
authGroup.GET("/reset-password", r.passwordResetHandler.ValidateResetToken)
authGroup.POST("/password/validate", r.passwordResetHandler.ValidateResetToken)
authGroup.POST("/reset-password", r.passwordResetHandler.ResetPassword)
// 短信密码重置
authGroup.POST("/forgot-password/phone", r.passwordResetHandler.ForgotPasswordByPhone)