Merge pull request #7052 from ouchihao/codex/issue-2912-transient-auth-failures

fix(auth): preserve sessions during temporary service failures
This commit is contained in:
Wesley Liddick
2026-09-12 19:23:12 +08:00
committed by GitHub
5 changed files with 141 additions and 4 deletions
@@ -176,7 +176,11 @@ func validateJWTForAdmin(
// 从数据库获取用户
user, err := userService.GetByID(c.Request.Context(), claims.UserID)
if err != nil {
AbortWithError(c, 401, "USER_NOT_FOUND", "User not found")
if errors.Is(err, service.ErrUserNotFound) {
AbortWithError(c, 401, "USER_NOT_FOUND", "User not found")
} else {
AbortWithError(c, 500, "INTERNAL_ERROR", "Failed to load user")
}
return false
}
@@ -71,7 +71,11 @@ func jwtAuth(
// 从数据库获取最新的用户信息
user, err := userService.GetByID(c.Request.Context(), claims.UserID)
if err != nil {
AbortWithError(c, 401, "USER_NOT_FOUND", "User not found")
if errors.Is(err, service.ErrUserNotFound) {
AbortWithError(c, 401, "USER_NOT_FOUND", "User not found")
} else {
AbortWithError(c, 500, "INTERNAL_ERROR", "Failed to load user")
}
return
}
@@ -26,7 +26,7 @@ type stubJWTUserRepo struct {
func (r *stubJWTUserRepo) GetByID(_ context.Context, id int64) (*service.User, error) {
u, ok := r.users[id]
if !ok {
return nil, errors.New("user not found")
return nil, service.ErrUserNotFound
}
return u, nil
}
@@ -232,6 +232,67 @@ func TestJWTAuth_TamperedToken(t *testing.T) {
require.Equal(t, "INVALID_TOKEN", body.Code)
}
func TestJWTAuth_UserLookupErrors(t *testing.T) {
gin.SetMode(gin.TestMode)
cfg := &config.Config{JWT: config.JWTConfig{Secret: "test-secret", ExpireHour: 1}}
authSvc := service.NewAuthService(nil, nil, nil, nil, cfg, nil, nil, nil, nil, nil, nil, nil, nil)
token, err := authSvc.GenerateToken(context.Background(), &service.User{ID: 1, Role: service.RoleAdmin})
require.NoError(t, err)
for _, tc := range []struct {
name string
err error
status int
code string
}{
{"missing user", service.ErrUserNotFound, http.StatusUnauthorized, "USER_NOT_FOUND"},
{"database timeout", context.DeadlineExceeded, http.StatusInternalServerError, "INTERNAL_ERROR"},
{"database failure", errors.New("database unavailable"), http.StatusInternalServerError, "INTERNAL_ERROR"},
} {
t.Run(tc.name, func(t *testing.T) {
userRepo := &stubUserRepo{getByID: func(context.Context, int64) (*service.User, error) {
return nil, tc.err
}}
userSvc := service.NewUserService(userRepo, nil, nil, nil)
for _, route := range []struct {
name string
handler gin.HandlerFunc
websocket bool
}{
{"user", gin.HandlerFunc(NewJWTAuthMiddleware(authSvc, userSvc, nil, nil)), false},
{"admin", gin.HandlerFunc(NewAdminAuthMiddleware(authSvc, userSvc, nil, nil)), false},
{"admin websocket", gin.HandlerFunc(NewAdminAuthMiddleware(authSvc, userSvc, nil, nil)), true},
} {
t.Run(route.name, func(t *testing.T) {
called := false
router := gin.New()
router.Use(route.handler)
router.GET("/protected", func(c *gin.Context) {
called = true
c.Status(http.StatusOK)
})
req := httptest.NewRequest(http.MethodGet, "/protected", nil)
if route.websocket {
req.Header.Set("Upgrade", "websocket")
req.Header.Set("Connection", "Upgrade")
req.Header.Set("Sec-WebSocket-Protocol", "sub2api-admin, jwt."+token)
} else {
req.Header.Set("Authorization", "Bearer "+token)
}
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
require.Equal(t, tc.status, w.Code)
var body ErrorResponse
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
require.Equal(t, tc.code, body.Code)
require.False(t, called)
})
}
})
}
}
func TestJWTAuth_UserNotFound(t *testing.T) {
// 使用 user ID=1 的 token,但 repo 中没有该用户
fakeUser := &service.User{