refactor(auth): route simple auth through use cases
This commit is contained in:
@@ -1,20 +1,20 @@
|
||||
"""
|
||||
认证 API(SQLAlchemy 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 "注册失败")
|
||||
|
||||
Reference in New Issue
Block a user