192 lines
5.2 KiB
Python
192 lines
5.2 KiB
Python
"""
|
||
认证相关 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,
|
||
)
|
||
|