Files
SatoNano/app/api/v1/endpoints/auth.py
2026-01-06 23:49:23 +08:00

192 lines
5.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
认证相关 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,
)