feat: embed permission claims in JWT and add captcha verification

- Add GroupClaims model for JWT permission snapshots
- Add JWTPayload model for typed JWT decoding
- Refactor auth middleware: jwt_required (no DB) -> admin_required (no DB) -> auth_required (DB)
- Add UserBanStore for instant ban enforcement via Redis + memory fallback
- Fix status check bug: StrEnum is always truthy, use explicit != ACTIVE
- Shorten access_token expiry from 3h to 1h
- Add CaptchaScene enum and verify_captcha_if_needed service
- Add require_captcha dependency injection factory
- Add CLA document and new default settings
- Update all tests for new JWT API

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-10 19:07:00 +08:00
parent 209cb24ab4
commit a99091ea7a
20 changed files with 766 additions and 244 deletions

View File

@@ -6,13 +6,14 @@ from sqlalchemy import func
from middleware.auth import admin_required
from middleware.dependencies import SessionDep, TableViewRequestDep, UserFilterParamsDep
from service.redis.user_ban_store import UserBanStore
from sqlmodels import (
User, ResponseBase, UserPublic, ListResponse,
Group, Object, ObjectType, Setting, SettingsType,
BatchDeleteRequest,
)
from sqlmodels.user import (
UserAdminCreateRequest, UserAdminUpdateRequest, UserCalibrateResponse,
UserAdminCreateRequest, UserAdminUpdateRequest, UserCalibrateResponse, UserStatus,
)
from utils import Password, http_exceptions
@@ -159,11 +160,21 @@ async def router_admin_update_user(
if len(update_data['two_factor']) != 32:
raise HTTPException(status_code=400, detail="两步验证密钥必须为32位字符串")
# 记录旧 status 以便检测变更
old_status = user.status
# 更新字段
for key, value in update_data.items():
setattr(user, key, value)
user = await user.save(session)
# 封禁状态变更 → 更新 BanStore
new_status = user.status
if old_status == UserStatus.ACTIVE and new_status != UserStatus.ACTIVE:
await UserBanStore.ban(str(user_id))
elif old_status != UserStatus.ACTIVE and new_status == UserStatus.ACTIVE:
await UserBanStore.unban(str(user_id))
l.info(f"管理员更新了用户: {request.email}")