refactor(auth): promote canonical auth routes
Deploy / Deploy Production (push) Failing after 2093h39m31s
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 2093h40m36s
Deploy / Deploy Staging (push) Has been cancelled

This commit is contained in:
Xiaoxia AI
2026-06-21 09:50:10 +08:00
parent c5321f4f12
commit 93ba337538
4 changed files with 115 additions and 367 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
from app.api.routes.asset_libraries import router as asset_libraries_router
from app.api.routes.assets import router as assets_router
from app.api.routes.auth_simple import router as auth_router
from app.api.routes.auth import router as auth_router
from app.api.routes.classification_jobs import router as classification_jobs_router
from app.api.routes.generated_videos import router as generated_videos_router
from app.api.routes.generation_tasks import router as generation_tasks_router
+81 -205
View File
@@ -1,62 +1,47 @@
"""
Legacy full-auth API route skeleton.
Canonical authentication API routes.
This module is intentionally disabled until the DI container/auth ports are rebuilt.
Do not mount it directly; use `auth_simple.py` only as the current compatibility route.
The route layer is intentionally thin: repository construction lives in
app.dependencies and authentication behavior lives in application use cases.
"""
raise RuntimeError("apps.api.app.api.routes.auth is disabled: rebuild DI container before mounting full auth routes")
from fastapi import APIRouter, Depends, HTTPException, Request, status
from app.auth import AuthenticatedUser, get_current_user
from app.config import settings
from app.dependencies import get_user_repository
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, EmailStr
from apps.api.app.dependencies import get_container
from apps.api.app.middleware.auth import get_current_user
from packages.application.auth import (
LoginRequest,
LoginUseCase,
LogoutRequest,
LogoutUseCase,
RegisterUserRequest,
RegisterUserUseCase,
RequestPasswordResetRequest,
RequestPasswordResetUseCase,
ResetPasswordRequest,
ResetPasswordUseCase,
VerifyEmailRequest,
VerifyEmailUseCase,
)
from packages.domain.entities import User
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=["Authentication"])
router = APIRouter(prefix="/auth", tags=["认证"])
# ==================== Request/Response Models ====================
class RegisterRequestModel(BaseModel):
class RegisterRequest(BaseModel):
email: EmailStr
password: str
username: str
display_name: str
class RegisterResponseModel(BaseModel):
class RegisterResponse(BaseModel):
user_id: str
email: str
username: str
display_name: str
email_verification_sent: bool
message: str
class LoginRequestModel(BaseModel):
class LoginRequest(BaseModel):
email: EmailStr
password: str
class LoginResponseModel(BaseModel):
class LoginResponse(BaseModel):
access_token: str
refresh_token: str
token_type: str = "bearer"
user_id: str
email: str
@@ -65,88 +50,60 @@ class LoginResponseModel(BaseModel):
expires_in: int
class PasswordResetRequestModel(BaseModel):
email: EmailStr
class CurrentUserResponse(BaseModel):
user_id: str
email: str
username: str
display_name: str
email_verified: bool
class ResetPasswordModel(BaseModel):
token: str
new_password: str
# ==================== API Endpoints ====================
@router.post(
"/register",
response_model=RegisterResponseModel,
status_code=status.HTTP_201_CREATED,
)
async def register(request: RegisterRequestModel):
"""
用户注册
- 邮箱必须唯一
- 用户名必须唯一
- 密码至少 8 位,包含大小写字母和数字
- 注册后发送邮箱验证邮件
"""
container = get_container()
use_case = container.get_register_user_use_case()
req = RegisterUserRequest(
email=request.email,
password=request.password,
username=request.username,
display_name=request.display_name,
@router.post("/register", response_model=RegisterResponse, status_code=status.HTTP_201_CREATED)
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(),
)
response, error = use_case.execute(req)
if error:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=error,
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 RegisterResponseModel(
return RegisterResponse(
user_id=response.user_id,
email=response.email,
username=response.username,
display_name=response.display_name,
email_verification_sent=response.email_verification_sent,
message="注册成功!",
)
@router.post("/login", response_model=LoginResponseModel)
async def login(request: LoginRequestModel):
"""
用户登录
- 使用邮箱和密码登录
- 返回 access_token 和 refresh_token
- access_token 有效期 30 分钟
- refresh_token 有效期 30 天
"""
container = get_container()
use_case = container.get_login_use_case()
req = LoginRequest(
email=request.email,
password=request.password,
@router.post("/login", response_model=LoginResponse)
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="邮箱或密码错误")
response, error = use_case.execute(req)
if error:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=error,
)
return LoginResponseModel(
return LoginResponse(
access_token=response.access_token,
refresh_token=response.refresh_token,
user_id=response.user_id,
email=response.email,
username=response.username,
@@ -155,116 +112,35 @@ async def login(request: LoginRequestModel):
)
@router.post("/logout", status_code=status.HTTP_204_NO_CONTENT)
async def logout(
request: Request,
logout_all_devices: bool = False,
current_user: User = Depends(get_current_user),
@router.get("/me", response_model=CurrentUserResponse)
async def get_current_user_info(
authenticated_user: AuthenticatedUser = Depends(get_current_user),
):
"""
用户登出
- 默认只登出当前设备
- 设置 logout_all_devices=true 可登出所有设备
"""
container = get_container()
use_case = container.get_logout_use_case()
# 从 JWT token 中提取 session_id
from packages.domain.auth import jwt_service
# 从 request 中获取 token
auth_header = request.headers.get("Authorization")
session_id = None
if auth_header and auth_header.startswith("Bearer "):
token = auth_header[7:] # 移除 "Bearer " 前缀
try:
payload = jwt_service.verify_token(token)
session_id = payload.get("sid") # 从 payload 提取 session_id
except:
pass # token 无效或没有 session_id,继续使用 None
req = LogoutRequest(
user_id=current_user.id,
session_id=session_id,
logout_all_devices=logout_all_devices,
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,
)
success, error = use_case.execute(req)
if not success:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=error,
)
class _NoopSessionStore:
def save_session(self, **kwargs):
return None
@router.get("/verify-email")
async def verify_email(token: str):
"""
邮箱验证
- 通过邮件中的链接访问此接口
- 验证成功后标记邮箱为已验证
"""
container = get_container()
use_case = container.get_verify_email_use_case()
req = VerifyEmailRequest(token=token)
success, error = use_case.execute(req)
if not success:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=error,
)
return {"message": "Email verified successfully"}
class _NoopEmailService:
def send_verification_email(self, **kwargs):
return False, "Email delivery is disabled for compatibility auth routes"
@router.post("/password/forgot", status_code=status.HTTP_202_ACCEPTED)
async def forgot_password(request: PasswordResetRequestModel):
"""
请求密码重置
- 发送密码重置邮件
- 邮件中包含重置链接(有效期 1 小时)
- 即使邮箱不存在也返回成功(安全考虑)
"""
container = get_container()
use_case = container.get_request_password_reset_use_case()
req = RequestPasswordResetRequest(email=request.email)
success, error = use_case.execute(req)
# 不论成功失败都返回 202(安全考虑)
return {"message": "Password reset email sent if account exists"}
@router.post("/password/reset", status_code=status.HTTP_200_OK)
async def reset_password(request: ResetPasswordModel):
"""
重置密码
- 使用邮件中的 token 重置密码
- 新密码必须符合密码强度要求
"""
container = get_container()
use_case = container.get_reset_password_use_case()
req = ResetPasswordRequest(
token=request.token,
new_password=request.new_password,
)
success, error = use_case.execute(req)
if not success:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=error,
)
return {"message": "Password reset successfully"}
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 "注册失败")
+14 -142
View File
@@ -1,146 +1,18 @@
"""
认证 API compatibility routes.
Backward-compatible import shim for the canonical auth routes.
The route layer is intentionally thin: repository construction lives in
app.dependencies and authentication behavior lives in application use cases.
New code should import `app.api.routes.auth`; this module remains so older tests
or clients that import `auth_simple` continue to resolve the same router.
"""
from app.auth import AuthenticatedUser, get_current_user
from app.config import settings
from app.dependencies import get_user_repository
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, EmailStr
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=["认证"])
class RegisterRequest(BaseModel):
email: EmailStr
password: str
username: str
display_name: str
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
@router.post("/register", response_model=RegisterResponse, status_code=status.HTTP_201_CREATED)
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(),
)
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=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),
):
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="邮箱或密码错误")
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,
)
@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:
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 "注册失败")
from app.api.routes.auth import ( # noqa: F401
CurrentUserResponse,
LoginRequest,
LoginResponse,
RegisterRequest,
RegisterResponse,
get_current_user_info,
login,
register,
router,
)
+19 -19
View File
@@ -278,25 +278,25 @@ python -m pytest tests/unit/test_login_use_case.py tests/unit/test_register_user
## 四、仍需继续治理的问题
### P1:认证体系双轨
### P1:认证体系双轨(已收敛)
当前真实路由仍使用 `auth_simple.py`,完整认证实现位于:
当前认证入口已收敛到 canonical route:
- `apps/api/app/api/routes/auth.py`
- `apps/api/app/auth.py`
- `packages/application/auth/*`
- `packages/domain/auth/session_store.py`
- `packages/domain/auth/email_service.py`
- `packages/adapters/sqlalchemy_impl/user_repository.py`
问题:
- 完整认证路由未接入 canonical router。
- `auth.py` 依赖缺失的 `get_container()`。
- `auth_simple.py` 仍承担生产入口。
已完成:
- `auth.py` 不再是 disabled skeleton,也不依赖缺失的 `get_container()`。
- `api/router.py` 直接挂载 `app.api.routes.auth`。
- `auth_simple.py` 降级为兼容 import shim,避免旧测试或外部导入立即失败。
- 登录/注册/当前用户均通过 `UserRepository + UseCase + app.auth`,route 层不再承载认证业务逻辑。
建议:
1. 先补完整 DI container。
2. 将 session/email 外部服务移出 domain。
3. 用完整 auth 替换 auth_simple。
4. 删除 auth_simple 或改名为测试 fixture。
剩余建议:
1. 后续确认无外部 import 后删除 `auth_simple.py` shim。
2. 根据产品需要补齐 password reset / verify email 的正式 route。
3. 接入真实邮件 adapter 前继续保持 no-op email delivery。
### P1:Domain 层存在外部基础设施依赖
@@ -334,15 +334,15 @@ python -m pytest tests/unit/test_login_use_case.py tests/unit/test_register_user
### P2:临时代码仍在主线
发现:
- `auth_simple.py` 名称和历史注释仍体现临时方案。
- `auth_simple.py` 已降级为兼容 shim,但文件名仍体现历史临时方案。
- 部分测试和文档仍引用旧 MinIO/OSS 混合术语。
- `__pycache__` 文件出现在工作树扫描中,需确认 `.gitignore` 和仓库状态。
## 五、下一步建议修复顺序
1. 完整 DI container:恢复 `get_container()`,但不要回到全局硬编码。
2. 认证体系收敛:完整 auth 替换 auth_simple。
3. 外部服务出 domain:Redis session / SMTP email 移到 adapters。
4. Repository 统一:优先 User/Workspace 迁移 SQLAlchemy。
5. 删除死代码:清理旧 routes、旧 postgres adapters、重复文档入口。
1. 删除死代码:确认无外部依赖后移除 `auth_simple.py` shim 和旧 postgres adapters。
2. 补齐认证扩展:password reset / verify email 正式 route。
3. 外部服务 adapter 化收尾:接入真实 Redis session / SMTP email 生产配置。
4. Repository 统一收尾:继续清理 remaining legacy imports。
5. 生产 Alembic 准备:备份、回滚、首次生产 stamp/upgrade 演练。
6. 全量测试和 CI:后端 unit/integration + 前端 type-check/build + staging smoke。