"""认证与用户管理 API:/api/v1/auth 下的登录/退出/改密与用户管理端点 会话制认证:登录签发 session token(存储见 app/core/sessions.py,TTL 12h), 请求经 Authorization: Bearer 携带,鉴权依赖见 app/api/deps.py。 用户管理端点(/users*)全部要求 admin 角色;/logout 与 /password 对 must_change_password 用户放行(改密是被拦截用户唯一能用的接口)。 """ from typing import Any, Literal import structlog from fastapi import APIRouter, Depends, Header 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, UserNotFoundError, UserRecord, UserStoreError logger = structlog.get_logger() router = APIRouter(prefix="/api/v1/auth", tags=["auth"]) class LoginRequest(BaseModel): """登录请求""" username: str password: str class PasswordChangeRequest(BaseModel): """修改自己的密码""" old_password: str new_password: str class UserCreateRequest(BaseModel): """管理员创建用户""" username: str password: str role: Literal["admin", "user"] = "user" 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, } @router.post("/login") async def login(req: LoginRequest) -> dict[str, Any]: """用户名密码登录:成功签发 session token(must_change_password 用户也可登录)""" try: user = await deps._get_user_store().verify_password(req.username, req.password) except UserStoreError as exc: 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: raise ApiError(2001, "认证服务暂不可用") from exc logger.info("用户登录成功", username=user.username) return ok( { "token": token, "username": user.username, "role": user.role, "must_change_password": user.must_change_password, } ) @router.get("/me") async def me(user: UserRecord = Depends(deps.get_current_user)) -> dict[str, Any]: """返回当前登录用户信息(脱敏),供管理页面校验登录态""" return ok(_public_user(user)) @router.post("/logout") async def logout(authorization: str | None = Header(None)) -> dict[str, Any]: """退出登录:删除当前 session(自解析 Bearer,不拦截 must_change_password)""" _, token = await deps._resolve_user(authorization) try: await deps._get_session_store().delete(token) except SessionStoreError as exc: raise ApiError(2001, "认证服务暂不可用") from exc return ok({"logged_out": True}) @router.post("/password") async def change_password( req: PasswordChangeRequest, authorization: str | None = Header(None) ) -> dict[str, Any]: """修改自己的密码(自解析 Bearer,不拦截 must_change_password——这是被拦截用户唯一可用接口) 旧密码错误 1005;新密码不足 8 位 1001。改密不清除既有 session(Task1 语义)。 """ user, _ = await deps._resolve_user(authorization) store = deps._get_user_store() try: verified = await store.verify_password(user.username, req.old_password) if verified is None: raise ApiError(1005, "旧密码错误") await store.set_password(user.username, req.new_password) except (UserStoreError, SessionStoreError) as exc: raise ApiError(2001, "认证服务暂不可用") from exc except ValueError as exc: raise ApiError(1001, str(exc)) from exc logger.info("用户修改密码", username=user.username) return ok({"username": user.username}) @router.get("/users") async def list_users(admin: UserRecord = Depends(deps.require_admin)) -> dict[str, Any]: """列出全部用户(脱敏:不含 password_hash/salt)""" try: users = await deps._get_user_store().list() except UserStoreError as exc: raise ApiError(2001, "认证服务暂不可用") from exc return ok([_public_user(user) for user in users]) @router.post("/users") async def create_user( req: UserCreateRequest, admin: UserRecord = Depends(deps.require_admin) ) -> dict[str, Any]: """创建用户:重名/非法用户名/弱密码 1001;成功返回脱敏后的创建记录""" try: user = await deps._get_user_store().create(req.username, req.password, role=req.role) except UserExistsError as exc: raise ApiError(1001, str(exc)) from exc except ValueError as exc: raise ApiError(1001, str(exc)) from exc except UserStoreError as exc: raise ApiError(2001, "认证服务暂不可用") from exc logger.info("管理员创建用户", username=user.username, role=user.role, operator=admin.username) 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) ) -> dict[str, Any]: """重置指定用户密码:用户不存在 1004;成功后清除该用户全部 session""" store = deps._get_user_store() try: if await store.get(username) is None: raise ApiError(1004, "用户不存在") await store.set_password(username, req.new_password) await deps._get_session_store().delete_by_username(username) except (UserStoreError, SessionStoreError) as exc: raise ApiError(2001, "认证服务暂不可用") from exc except ValueError as exc: raise ApiError(1001, str(exc)) from exc logger.info("管理员重置用户密码", username=username, operator=admin.username) return ok({"username": username}) @router.delete("/users/{username}") async def delete_user( username: str, admin: UserRecord = Depends(deps.require_admin) ) -> dict[str, Any]: """删除用户:不存在 1004;删除自己 1001;最后一个 admin 禁止删除 1001;成功后清除其 session""" store = deps._get_user_store() try: target = await store.get(username) if target is None: raise ApiError(1004, "用户不存在") if target.username == admin.username: raise ApiError(1001, "不能删除当前登录账号") if target.role == "admin" and await store.count_admins() <= 1: raise ApiError(1001, "禁止删除最后一个管理员") await store.delete(username) await deps._get_session_store().delete_by_username(username) except (UserStoreError, SessionStoreError) as exc: raise ApiError(2001, "认证服务暂不可用") from exc logger.info("管理员删除用户", username=username, operator=admin.username) return ok({"username": username})