""" 认证相关 API 包括注册、登录、退出、刷新令牌、修改密码等接口。 """ from typing import Annotated from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession from app.api.deps import ActiveUser, DbSession, get_auth_service from app.core.exceptions import ( InvalidCredentialsError, PasswordValidationError, ResourceConflictError, TokenError, UserDisabledError, ) from app.schemas.auth import ( LoginRequest, PasswordChangeRequest, RefreshTokenRequest, TokenResponse, ) from app.schemas.base import APIResponse from app.schemas.user import UserCreate, UserResponse from app.services.auth import AuthService from app.services.user import UserService router = APIRouter() @router.post( "/register", response_model=APIResponse[UserResponse], status_code=status.HTTP_201_CREATED, summary="用户注册", description="创建新用户账户", ) async def register( user_data: UserCreate, session: DbSession, ) -> APIResponse[UserResponse]: """ 用户注册接口 - **username**: 用户名(字母开头,3-32位) - **email**: 邮箱(可选) - **password**: 密码(8位以上,需包含大小写字母和数字) - **nickname**: 昵称(可选) """ user_service = UserService(session) try: user = await user_service.create_user(user_data) return APIResponse.ok( data=UserResponse.model_validate(user), message="注册成功", ) except ResourceConflictError as e: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail=e.message, ) @router.post( "/login", response_model=APIResponse[TokenResponse], summary="用户登录", description="使用用户名/邮箱和密码登录", ) async def login( login_data: LoginRequest, auth_service: Annotated[AuthService, Depends(get_auth_service)], ) -> APIResponse[TokenResponse]: """ 用户登录接口 - **username**: 用户名或邮箱 - **password**: 密码 返回访问令牌和刷新令牌。 """ try: _, tokens = await auth_service.login( username=login_data.username, password=login_data.password, ) return APIResponse.ok(data=tokens, message="登录成功") except InvalidCredentialsError as e: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=e.message, headers={"WWW-Authenticate": "Bearer"}, ) except UserDisabledError as e: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=e.message, ) @router.post( "/logout", response_model=APIResponse[None], summary="用户退出", description="退出登录(客户端应删除本地令牌)", ) async def logout( current_user: ActiveUser, ) -> APIResponse[None]: """ 用户退出接口 由于使用的是无状态 JWT,服务端不存储令牌, 因此退出登录主要由客户端删除本地存储的令牌实现。 如果需要实现令牌黑名单,可以在后续版本中添加。 """ # 可以在这里添加令牌黑名单逻辑 # 或者记录退出日志 return APIResponse.ok(message="退出成功") @router.post( "/refresh", response_model=APIResponse[TokenResponse], summary="刷新令牌", description="使用刷新令牌获取新的访问令牌", ) async def refresh_token( token_data: RefreshTokenRequest, auth_service: Annotated[AuthService, Depends(get_auth_service)], ) -> APIResponse[TokenResponse]: """ 刷新令牌接口 使用刷新令牌获取新的访问令牌和刷新令牌。 """ try: tokens = await auth_service.refresh_tokens(token_data.refresh_token) return APIResponse.ok(data=tokens, message="刷新成功") except TokenError as e: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=e.message, headers={"WWW-Authenticate": "Bearer"}, ) except UserDisabledError as e: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=e.message, ) @router.post( "/change-password", response_model=APIResponse[None], summary="修改密码", description="修改当前用户密码", ) async def change_password( password_data: PasswordChangeRequest, current_user: ActiveUser, auth_service: Annotated[AuthService, Depends(get_auth_service)], ) -> APIResponse[None]: """ 修改密码接口 - **current_password**: 当前密码 - **new_password**: 新密码 """ try: await auth_service.change_password( user_id=current_user.id, password_data=password_data, ) return APIResponse.ok(message="密码修改成功") except InvalidCredentialsError as e: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=e.message, ) except PasswordValidationError as e: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=e.message, )