refactor(auth): route simple auth through use cases

This commit is contained in:
Xiaoxia AI
2026-06-21 08:22:49 +08:00
parent 0f7cc8f12a
commit 93271f7dda
6 changed files with 295 additions and 168 deletions
+69 -114
View File
@@ -1,20 +1,20 @@
"""
认证 APISQLAlchemy ORM
认证 API compatibility routes.
The route layer is intentionally thin: repository construction lives in
app.dependencies and authentication behavior lives in application use cases.
"""
import hashlib
import secrets
from datetime import datetime, timedelta, timezone
import jwt
from app.config import settings
from app.dependencies import get_db_session
from app.dependencies import get_user_repository
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, EmailStr
from sqlalchemy.orm import Session
from packages.adapters.sqlalchemy_impl.models import UserModel
from packages.domain.auth import password_hasher, password_validator
from packages.application.auth.login_use_case import LoginRequest as LoginUseCaseRequest
from packages.application.auth.login_use_case import LoginUseCase
from packages.application.auth.register_user_use_case import RegisterUserRequest as RegisterUseCaseRequest
from packages.application.auth.register_user_use_case import RegisterUserUseCase
from packages.ports.user_repository import UserRepository
router = APIRouter(prefix="/auth", tags=["认证"])
@@ -49,122 +49,57 @@ class LoginResponse(BaseModel):
expires_in: int
ACCESS_TOKEN_EXPIRE_MINUTES = 30
JWT_ALGORITHM = "HS256"
LEGACY_SHA256_HEX_LENGTH = 64
def _normalize_email(email: str) -> str:
return email.strip().lower()
def _normalize_username(username: str) -> str:
return username.strip()
def _is_legacy_sha256_hash(password_hash: str) -> bool:
return len(password_hash) == LEGACY_SHA256_HEX_LENGTH and all(
char in "0123456789abcdef" for char in password_hash.lower()
)
def _legacy_sha256(password: str) -> str:
return hashlib.sha256(password.encode()).hexdigest()
def _verify_password_with_legacy_upgrade(password: str, user: UserModel, db: Session) -> bool:
stored_hash = user.password_hash or ""
if password_hasher.verify_password(password, stored_hash):
return True
if _is_legacy_sha256_hash(stored_hash) and secrets.compare_digest(_legacy_sha256(password), stored_hash):
user.password_hash = password_hasher.hash_password(password)
db.add(user)
db.commit()
db.refresh(user)
return True
return False
def _create_access_token(user: UserModel) -> tuple[str, int]:
expires_delta = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
now = datetime.now(timezone.utc)
payload = {
"sub": user.id,
"email": user.email,
"type": "user_auth",
"iat": now,
"exp": now + expires_delta,
}
token = jwt.encode(payload, settings.JWT_SECRET_KEY, algorithm=JWT_ALGORITHM)
return token, int(expires_delta.total_seconds())
@router.post("/register", response_model=RegisterResponse, status_code=status.HTTP_201_CREATED)
async def register(request: RegisterRequest, db: Session = Depends(get_db_session)):
email = _normalize_email(request.email)
username = _normalize_username(request.username)
display_name = request.display_name.strip()
if not username:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="用户名不能为空")
if not display_name:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="显示名称不能为空")
password_valid, password_error = password_validator.validate(request.password)
if not password_valid:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=password_error)
existing_user = db.query(UserModel).filter(UserModel.email == email).first()
if existing_user:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="邮箱已被注册")
existing_username = db.query(UserModel).filter(UserModel.username == username).first()
if existing_username:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="用户名已被使用")
new_user = UserModel(
id=f"user_{secrets.token_hex(8)}",
email=email,
username=username,
display_name=display_name,
password_hash=password_hasher.hash_password(request.password),
email_verified=False,
created_at=datetime.now(timezone.utc),
async def register(
request: RegisterRequest,
user_repository: UserRepository = Depends(get_user_repository),
):
use_case = RegisterUserUseCase(
user_repository=user_repository,
base_url="http://localhost:3000",
email_service=_NoopEmailService(),
)
db.add(new_user)
db.commit()
db.refresh(new_user)
response, error = use_case.execute(
RegisterUseCaseRequest(
email=request.email,
password=request.password,
username=request.username,
display_name=request.display_name,
)
)
if error or response is None:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=_translate_auth_error(error))
return RegisterResponse(
user_id=new_user.id,
email=new_user.email,
username=new_user.username or "",
display_name=new_user.display_name,
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, db: Session = Depends(get_db_session)):
email = _normalize_email(request.email)
user = db.query(UserModel).filter(UserModel.email == email).first()
if not user or not _verify_password_with_legacy_upgrade(request.password, user, db):
async def login(
request: LoginRequest,
user_repository: UserRepository = Depends(get_user_repository),
):
use_case = LoginUseCase(
user_repository=user_repository,
session_store=_NoopSessionStore(),
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="邮箱或密码错误")
access_token, expires_in = _create_access_token(user)
return LoginResponse(
access_token=access_token,
user_id=user.id,
email=user.email,
username=user.username or "",
display_name=user.display_name,
expires_in=expires_in,
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,
)
@@ -174,3 +109,23 @@ async def get_current_user_info():
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="/auth/me requires bearer-token dependency integration",
)
class _NoopSessionStore:
def save_session(self, **kwargs):
return None
class _NoopEmailService:
def send_verification_email(self, **kwargs):
return False, "Email delivery is disabled for compatibility auth routes"
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 "注册失败")