"""认证与用户管理 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 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 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 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, "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, "用户名或密码错误") 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.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})