fix(auth): register CAP scope, 400 on captcha, 403 for permission

Navigating from login reused a send_email_code token on register.
Captcha failure used 401 so the client stored /register as the
post-login target and never left the page. Permission denials now
return 403, and the API client no longer wipes the session on 401.
This commit is contained in:
ryan
2026-09-02 18:26:22 +08:00
parent df7ad453cc
commit b7e5e811d1
11 changed files with 129 additions and 52 deletions
+3 -5
View File
@@ -30,11 +30,9 @@ const (
errExternalAccountBindingIncomplete = "外部帐号绑定信息不完整"
errExternalAccountAlreadyBoundToAnother = "该外部帐号已被其他用户绑定"
errExternalAccountBindingIDRequired = "外部帐号绑定记录 ID 不能为空"
errAdminRequired = "无权访问"
//nolint:gosec // error message, not hardcoded credentials
errTokenAdminRequired = "令牌无管理员权限"
errBannedAccount = "账号已被封禁"
errUnAuthorized = "未登录"
errInsufficientPermission = "权限不足"
errBannedAccount = "账号已被封禁"
errUnAuthorized = "未登录"
)
// Service 层与鉴权中间件内部错误文案(保持与重构前逐字一致)
+3 -5
View File
@@ -162,15 +162,13 @@ func AdminRequired() gin.HandlerFunc {
isTokenAuth, _ := ginutil.GetFromContext[bool](c, contracts.AuthTokenAuthKey)
isTokenAdmin, _ := ginutil.GetFromContext[bool](c, contracts.AuthTokenAdminKey)
// 如果是通过 Token 鉴权,要求该 Token 具备管理员权限或者用户本身为管理员
// Logged-in but lacking admin permission is 403, not 401/404.
if isTokenAuth && !isTokenAdmin && !user.IsAdmin {
response.AbortNotFound(c, errTokenAdminRequired)
response.AbortForbidden(c, errInsufficientPermission)
return
}
// 如果是通过 Session 鉴权,直接检查用户的 is_admin 属性
if !isTokenAuth && !user.IsAdmin {
response.AbortNotFound(c, errAdminRequired)
response.AbortForbidden(c, errInsufficientPermission)
return
}
@@ -40,6 +40,7 @@ type testUser struct {
ID uint64 `gorm:"primaryKey"`
Username string
IsActive bool
IsAdmin bool
LastLoginAt time.Time
}
@@ -399,3 +399,30 @@ func TestAuthWhitelistMiddleware(t *testing.T) {
engine.ServeHTTP(w2, req2)
assert.Equal(t, http.StatusUnauthorized, w2.Code)
}
func TestAdminRequiredReturnsForbiddenForLoggedInNonAdmin(t *testing.T) {
gin.SetMode(gin.TestMode)
db := setupTestDB(t)
require.NoError(t, db.Create(&testUser{ID: 42, Username: "member", IsActive: true, IsAdmin: false}).Error)
svc := newTestAuthService(t, db)
mw, ok := svc.RequireAdminMiddleware().(gin.HandlerFunc)
require.True(t, ok)
engine := newSessionEngine()
engine.Use(func(c *gin.Context) {
session := sessions.Default(c)
session.Set(auth.UserIDKey, uint64(42))
require.NoError(t, session.Save())
c.Next()
})
engine.Use(mw)
engine.GET("/admin-only", func(c *gin.Context) {
c.JSON(http.StatusOK, response.OK("ok"))
})
w := httptest.NewRecorder()
engine.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/admin-only", nil))
assert.Equal(t, http.StatusForbidden, w.Code)
assert.Contains(t, w.Body.String(), "权限不足")
}