chore: 完成全量功能迭代与部署准备
- 移除冗余依赖包 - 新增账号禁用校验与用户管理能力 - 新增文档下载与管理页面文件展示 - 新增API文档页面与用户管理前端页面 - 重构时区处理与docker-compose部署配置 - 完善测试用例与项目文档
This commit is contained in:
+58
-2
@@ -10,12 +10,12 @@ from typing import Any, Literal
|
||||
|
||||
import structlog
|
||||
from fastapi import APIRouter, Depends, Header
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, model_validator
|
||||
|
||||
from app.api import deps
|
||||
from app.api.response import ApiError, ok
|
||||
from app.core.sessions import SessionStoreError
|
||||
from app.core.users import UserExistsError, UserRecord, UserStoreError
|
||||
from app.core.users import UserExistsError, UserNotFoundError, UserRecord, UserStoreError
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
@@ -50,12 +50,26 @@ class PasswordResetRequest(BaseModel):
|
||||
new_password: str
|
||||
|
||||
|
||||
class UserUpdateRequest(BaseModel):
|
||||
"""管理员更新用户角色/启用状态(至少一项)"""
|
||||
|
||||
role: Literal["admin", "user"] | None = None
|
||||
enabled: bool | None = None
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _at_least_one(self) -> "UserUpdateRequest":
|
||||
if self.role is None and self.enabled is None:
|
||||
raise ValueError("至少需要提供一项更新字段(role 或 enabled)")
|
||||
return self
|
||||
|
||||
|
||||
def _public_user(user: UserRecord) -> dict[str, Any]:
|
||||
"""用户记录脱敏:剔除 password_hash/salt"""
|
||||
return {
|
||||
"username": user.username,
|
||||
"role": user.role,
|
||||
"must_change_password": user.must_change_password,
|
||||
"enabled": user.enabled,
|
||||
"created_at": user.created_at,
|
||||
}
|
||||
|
||||
@@ -69,6 +83,8 @@ async def login(req: LoginRequest) -> dict[str, Any]:
|
||||
raise ApiError(2001, "认证服务暂不可用") from exc
|
||||
if user is None:
|
||||
raise ApiError(1005, "用户名或密码错误")
|
||||
if not user.enabled:
|
||||
raise ApiError(1005, "账号已禁用")
|
||||
try:
|
||||
token = await deps._get_session_store().create(user.username, user.role)
|
||||
except SessionStoreError as exc:
|
||||
@@ -151,6 +167,46 @@ async def create_user(
|
||||
return ok(_public_user(user))
|
||||
|
||||
|
||||
@router.patch("/users/{username}")
|
||||
async def update_user(
|
||||
username: str,
|
||||
req: UserUpdateRequest,
|
||||
admin: UserRecord = Depends(deps.require_admin),
|
||||
) -> dict[str, Any]:
|
||||
"""更新用户角色或启用状态
|
||||
|
||||
用户不存在 1004;role 非法 1001;最后一个 admin 降级 role 或被禁用 1001;
|
||||
禁用用户时清除其全部 session(已登录态立即失效)。
|
||||
"""
|
||||
store = deps._get_user_store()
|
||||
try:
|
||||
target = await store.get(username)
|
||||
if target is None:
|
||||
raise ApiError(1004, "用户不存在")
|
||||
# 最后 admin 保护:目标当前是唯一 admin 且本次会使其失去 admin 身份或被禁用
|
||||
if target.role == "admin" and await store.count_admins() <= 1:
|
||||
will_lose_admin = (req.role is not None and req.role != "admin") or (req.enabled is False)
|
||||
if will_lose_admin:
|
||||
raise ApiError(1001, "禁止降级或禁用最后一个管理员")
|
||||
updated = await store.update_user(username, role=req.role, enabled=req.enabled)
|
||||
except UserStoreError as exc:
|
||||
raise ApiError(2001, "认证服务暂不可用") from exc
|
||||
except UserNotFoundError as exc:
|
||||
raise ApiError(1004, "用户不存在") from exc
|
||||
except ValueError as exc:
|
||||
raise ApiError(1001, str(exc)) from exc
|
||||
# 禁用用户 → 清除其全部 session(已登录态立即失效)
|
||||
if req.enabled is False:
|
||||
try:
|
||||
await deps._get_session_store().delete_by_username(username)
|
||||
except SessionStoreError as exc:
|
||||
raise ApiError(2001, "认证服务暂不可用") from exc
|
||||
logger.info(
|
||||
"管理员更新用户", username=username, operator=admin.username, role=req.role, enabled=req.enabled
|
||||
)
|
||||
return ok(_public_user(updated))
|
||||
|
||||
|
||||
@router.post("/users/{username}/password")
|
||||
async def reset_user_password(
|
||||
username: str, req: PasswordResetRequest, admin: UserRecord = Depends(deps.require_admin)
|
||||
|
||||
Reference in New Issue
Block a user