231 lines
7.3 KiB
Python
231 lines
7.3 KiB
Python
"""
|
|
Canonical authentication API routes.
|
|
|
|
The route layer is intentionally thin: repository construction lives in
|
|
app.dependencies and authentication behavior lives in application use cases.
|
|
"""
|
|
|
|
from typing import Optional
|
|
|
|
from app.auth import AuthenticatedUser, get_current_user
|
|
from app.config import settings
|
|
from app.dependencies import get_auth_email_service, get_auth_session_store, get_user_repository
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from pydantic import BaseModel, EmailStr
|
|
|
|
from packages.adapters.redis import NoopSessionStore
|
|
from packages.adapters.smtp import NoopEmailService
|
|
from packages.application.auth.login_use_case import LoginRequest as LoginUseCaseRequest
|
|
from packages.application.auth.login_use_case import LoginUseCase
|
|
from packages.application.auth.password_reset_use_case import RequestPasswordResetRequest as PasswordResetUseCaseRequest
|
|
from packages.application.auth.password_reset_use_case import (
|
|
RequestPasswordResetUseCase,
|
|
ResetPasswordRequest,
|
|
ResetPasswordUseCase,
|
|
)
|
|
from packages.application.auth.register_user_use_case import RegisterUserRequest as RegisterUseCaseRequest
|
|
from packages.application.auth.register_user_use_case import RegisterUserUseCase, VerifyEmailRequest, VerifyEmailUseCase
|
|
from packages.ports.user_repository import UserRepository
|
|
|
|
router = APIRouter(prefix="/auth", tags=["认证"])
|
|
|
|
|
|
class RegisterRequest(BaseModel):
|
|
email: EmailStr
|
|
password: str
|
|
username: str
|
|
display_name: Optional[str] = None
|
|
|
|
|
|
class RegisterResponse(BaseModel):
|
|
user_id: str
|
|
email: str
|
|
username: str
|
|
display_name: str
|
|
message: str
|
|
|
|
|
|
class LoginRequest(BaseModel):
|
|
email: EmailStr
|
|
password: str
|
|
|
|
|
|
class LoginResponse(BaseModel):
|
|
access_token: str
|
|
token_type: str = "bearer"
|
|
user_id: str
|
|
email: str
|
|
username: str
|
|
display_name: str
|
|
expires_in: int
|
|
|
|
|
|
class CurrentUserResponse(BaseModel):
|
|
user_id: str
|
|
email: str
|
|
username: str
|
|
display_name: str
|
|
email_verified: bool
|
|
|
|
|
|
class PasswordResetRequestModel(BaseModel):
|
|
email: EmailStr
|
|
|
|
|
|
class ResetPasswordModel(BaseModel):
|
|
token: str
|
|
new_password: str
|
|
|
|
|
|
class VerifyEmailRequestModel(BaseModel):
|
|
token: str
|
|
|
|
|
|
class MessageResponse(BaseModel):
|
|
message: str
|
|
|
|
|
|
@router.post("/register", response_model=RegisterResponse, status_code=status.HTTP_201_CREATED)
|
|
async def register(
|
|
request: RegisterRequest,
|
|
user_repository: UserRepository = Depends(get_user_repository),
|
|
email_service=Depends(get_auth_email_service),
|
|
):
|
|
use_case = RegisterUserUseCase(
|
|
user_repository=user_repository,
|
|
base_url="http://localhost:3000",
|
|
email_service=email_service,
|
|
)
|
|
response, error = use_case.execute(
|
|
RegisterUseCaseRequest(
|
|
email=request.email,
|
|
password=request.password,
|
|
username=request.username,
|
|
display_name=request.display_name or request.username,
|
|
)
|
|
)
|
|
if error or response is None:
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=_translate_auth_error(error))
|
|
|
|
return RegisterResponse(
|
|
user_id=response.user_id,
|
|
email=response.email,
|
|
username=response.username,
|
|
display_name=response.display_name,
|
|
message="注册成功!",
|
|
)
|
|
|
|
|
|
@router.post("/login", response_model=LoginResponse)
|
|
async def login(
|
|
request: LoginRequest,
|
|
user_repository: UserRepository = Depends(get_user_repository),
|
|
session_store=Depends(get_auth_session_store),
|
|
):
|
|
use_case = LoginUseCase(
|
|
user_repository=user_repository,
|
|
session_store=session_store,
|
|
jwt_secret_key=settings.JWT_SECRET_KEY,
|
|
)
|
|
response, error = use_case.execute(LoginUseCaseRequest(email=request.email, password=request.password))
|
|
if error or response is None:
|
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="邮箱或密码错误")
|
|
|
|
return LoginResponse(
|
|
access_token=response.access_token,
|
|
user_id=response.user_id,
|
|
email=response.email,
|
|
username=response.username,
|
|
display_name=response.display_name,
|
|
expires_in=response.expires_in,
|
|
)
|
|
|
|
|
|
def _verify_email_token(token: str, user_repository: UserRepository) -> MessageResponse:
|
|
success, error = VerifyEmailUseCase(user_repository=user_repository).execute(VerifyEmailRequest(token=token))
|
|
if not success:
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error or "邮箱验证失败")
|
|
|
|
return MessageResponse(message="邮箱验证成功")
|
|
|
|
|
|
@router.get("/verify-email", response_model=MessageResponse)
|
|
async def verify_email(
|
|
token: str,
|
|
user_repository: UserRepository = Depends(get_user_repository),
|
|
):
|
|
return _verify_email_token(token, user_repository)
|
|
|
|
|
|
@router.post("/verify-email", response_model=MessageResponse)
|
|
async def verify_email_post(
|
|
request: VerifyEmailRequestModel,
|
|
user_repository: UserRepository = Depends(get_user_repository),
|
|
):
|
|
return _verify_email_token(request.token, user_repository)
|
|
|
|
|
|
@router.post("/password/forgot", response_model=MessageResponse, status_code=status.HTTP_202_ACCEPTED)
|
|
@router.post("/forgot-password", response_model=MessageResponse, status_code=status.HTTP_202_ACCEPTED)
|
|
async def forgot_password(
|
|
request: PasswordResetRequestModel,
|
|
user_repository: UserRepository = Depends(get_user_repository),
|
|
email_service=Depends(get_auth_email_service),
|
|
):
|
|
success, error = RequestPasswordResetUseCase(
|
|
user_repository=user_repository,
|
|
base_url="http://localhost:3000",
|
|
email_service=email_service,
|
|
).execute(PasswordResetUseCaseRequest(email=request.email))
|
|
if not success:
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error or "密码重置请求失败")
|
|
|
|
return MessageResponse(message="如果账户存在,密码重置邮件已发送")
|
|
|
|
|
|
@router.post("/password/reset", response_model=MessageResponse)
|
|
@router.post("/reset-password", response_model=MessageResponse)
|
|
async def reset_password(
|
|
request: ResetPasswordModel,
|
|
user_repository: UserRepository = Depends(get_user_repository),
|
|
):
|
|
success, error = ResetPasswordUseCase(user_repository=user_repository).execute(
|
|
ResetPasswordRequest(token=request.token, new_password=request.new_password)
|
|
)
|
|
if not success:
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error or "密码重置失败")
|
|
|
|
return MessageResponse(message="密码重置成功")
|
|
|
|
|
|
@router.get("/me", response_model=CurrentUserResponse)
|
|
async def get_current_user_info(
|
|
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
|
):
|
|
user = authenticated_user.user
|
|
return CurrentUserResponse(
|
|
user_id=user.id,
|
|
email=user.email,
|
|
username=user.username,
|
|
display_name=user.display_name,
|
|
email_verified=user.email_verified,
|
|
)
|
|
|
|
|
|
class _NoopSessionStore(NoopSessionStore):
|
|
pass
|
|
|
|
|
|
class _NoopEmailService(NoopEmailService):
|
|
pass
|
|
|
|
|
|
def _translate_auth_error(error: str | None) -> str:
|
|
translations = {
|
|
"Email already registered": "邮箱已被注册",
|
|
"Username already taken": "用户名已被使用",
|
|
"Username is required": "用户名不能为空",
|
|
"Display name is required": "显示名称不能为空",
|
|
}
|
|
return translations.get(error or "", error or "注册失败")
|