Files
geMoldInsight/src/shared/services/auth_routes.py
T
cjw 0e6b3b1811 后端设计治理:批次 0-4 全部完成(安全/部署/一致性/结构/架构)
按 ROADMAP §3.1 治理批次推进的后端设计审查整改:

- 批次 0(安全):/api/status/{task_id} 补 JWT 鉴权与任务归属校验;
  pythonocc_available 真实探测;bcrypt 超 72 字节显式拒绝;
  SECRET_KEY/RUSTFS_* 惰性校验,代码侧弱默认移除
- 批次 1(部署正确性):主处理链路改走 RustFS(分派入参 stp_file_id 化,
  worker 按 object_key 下载);AUTO_MIGRATE 开关 + 迁移目录 alembic/→migrations/
  修复包遮蔽(自动迁移此前从未真正生效);OCC 镜像改 conda 原生执行 +
  基础镜像 tag 锁定;compose 关键项改 ${VAR:?} 强制显式配置
- 批次 2(任务一致性):删除 Redis 进程内存回退,PG 为任务状态单一事实源;
  批量元数据入库(processing_tasks.batch_id,迁移 a3f8c2d91e47);
  型腔失败任务标 failed 不再静默 completed;事务边界收口
  (数据本体写 flush-only、失败先回滚再置 failed、进度更新保留即时 commit)
- 批次 3(API 与代码结构):592 行 advanced_router 拆为 design/cost/machining/
  export 四子路由,请求体全量 Pydantic 化;ROUTE_MODULES + route_registry
  (/api/health 呈现 degraded,DEBUG fail fast);纯计算端点统一 to_thread;
  StorageIntegrationService 按职责三拆;MAX_FILE_SIZE 接线生效、
  celery 复用 Settings.redis_url;管理员重置密码改 JSON body(端到端断裂修复);
  openapi.json 重导出(76 paths)+ 前端 gen:api
- 批次 4(架构演进):共享 ORM 按模块拆分(shared/models/base.py + identity.py、
  moldinsight/models/、inventory/models/,删除三条无使用方的跨模块
  relationship,跨模块桥接收敛为裸 FK 硬规则,无兼容 facade);
  OCC executor 重建补 cancel_futures=True(消除旧队列被慢恢复线程
  并行消化的数据竞争);OCC 吞吐方案设计先行
  (docs/topics/performance/OCC_THROUGHPUT.md);顺手清偿 D15
  (vite.config.ts 未用参数致 npm run build 失败)

测试基线:125 passed, 2 skipped(pytest + sqlite+aiosqlite;归属边界、
路由契约、配置治理、鉴权回归等随批新增)
文档同步:STATUS / TECH_DEBT / ROADMAP / ARCHITECTURE / API_CONTRACT /
OPERATIONS / AGENTS

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-17 16:15:49 +08:00

537 lines
16 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.
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.ext.asyncio import AsyncSession
from pydantic import BaseModel, Field
from typing import Optional, List
from datetime import timedelta
from sqlalchemy import select
from sqlalchemy.orm import selectinload
from shared.database.database import get_db_session
from shared.services.auth_service import (
authenticate_user,
create_access_token,
get_current_active_user,
get_password_hash
)
from shared.models.identity import User, Role, Permission, UserRole, RolePermission
from shared.config.settings import settings
from shared.utils.logger import get_logger
logger = get_logger(__name__)
router = APIRouter(prefix="/api/auth", tags=["认证"])
class UserResponse(BaseModel):
id: int
username: str
email: str
full_name: Optional[str]
is_active: bool
is_superuser: bool = False
roles: List[str]
class Config:
from_attributes = True
class Token(BaseModel):
access_token: str
token_type: str
user: UserResponse
class LoginRequest(BaseModel):
username: str
password: str
class RoleCreate(BaseModel):
code: str
name: str
description: Optional[str] = None
class RoleResponse(BaseModel):
id: int
code: str
name: str
description: Optional[str]
is_system: bool
permissions: List[str]
class Config:
from_attributes = True
class PermissionCreate(BaseModel):
code: str
name: str
module: Optional[str] = None
description: Optional[str] = None
class PermissionResponse(BaseModel):
id: int
code: str
name: str
module: Optional[str]
description: Optional[str]
class Config:
from_attributes = True
class UserCreate(BaseModel):
username: str
email: str
password: str
full_name: Optional[str] = None
role_ids: List[int] = []
class UserUpdate(BaseModel):
email: Optional[str] = None
full_name: Optional[str] = None
is_active: Optional[bool] = None
role_ids: Optional[List[int]] = None
class ResetPasswordRequest(BaseModel):
# 新密码走 JSON body(与前端 api-client.ts 的 { new_password } 结构一致)。
# 此前声明为裸 str 参数被 FastAPI 解析为 query param,前端发 body 必然 422,
# 重置密码功能端到端断裂;最短 6 位对齐 UsersView 前端校验。
new_password: str = Field(min_length=6)
def check_admin(user: User) -> bool:
if not user.is_superuser:
raise HTTPException(status_code=403, detail="需要管理员权限")
return True
def _build_user_response(user: User) -> UserResponse:
"""统一构造用户响应,确保 is_superuser 等字段一致"""
return UserResponse(
id=user.id,
username=user.username,
email=user.email,
full_name=user.full_name,
is_active=user.is_active,
is_superuser=user.is_superuser,
roles=[r.code for r in user.roles],
)
@router.post("/login", response_model=Token)
async def login(
form_data: OAuth2PasswordRequestForm = Depends(),
db_session: AsyncSession = Depends(get_db_session)
):
user = await authenticate_user(db_session, form_data.username, form_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="用户名或密码错误",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user.username}, expires_delta=access_token_expires
)
return Token(
access_token=access_token,
token_type="bearer",
user=_build_user_response(user)
)
@router.post("/login/json", response_model=Token)
async def login_json(
login_data: LoginRequest,
db_session: AsyncSession = Depends(get_db_session)
):
user = await authenticate_user(db_session, login_data.username, login_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="用户名或密码错误",
)
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user.username}, expires_delta=access_token_expires
)
return Token(
access_token=access_token,
token_type="bearer",
user=_build_user_response(user)
)
@router.get("/me", response_model=UserResponse)
async def get_current_user_info(
current_user: User = Depends(get_current_active_user)
):
return _build_user_response(current_user)
@router.post("/logout")
async def logout():
return {"message": "已登出"}
@router.get("/users", response_model=List[UserResponse])
async def list_users(
db_session: AsyncSession = Depends(get_db_session),
current_user: User = Depends(get_current_active_user)
):
check_admin(current_user)
result = await db_session.execute(
select(User).options(selectinload(User.user_roles).selectinload(UserRole.role))
)
users = result.scalars().all()
return [
_build_user_response(u) for u in users
]
@router.post("/users", response_model=UserResponse, status_code=201)
async def create_user(
user_data: UserCreate,
db_session: AsyncSession = Depends(get_db_session),
current_user: User = Depends(get_current_active_user)
):
check_admin(current_user)
existing = await db_session.execute(
select(User).where(User.username == user_data.username)
)
if existing.scalar_one_or_none():
raise HTTPException(status_code=400, detail="用户名已存在")
existing_email = await db_session.execute(
select(User).where(User.email == user_data.email)
)
if existing_email.scalar_one_or_none():
raise HTTPException(status_code=400, detail="邮箱已存在")
try:
hashed_password = get_password_hash(user_data.password)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
user = User(
username=user_data.username,
email=user_data.email,
hashed_password=hashed_password,
full_name=user_data.full_name,
is_active=True
)
db_session.add(user)
await db_session.flush()
for role_id in user_data.role_ids:
user_role = UserRole(user_id=user.id, role_id=role_id)
db_session.add(user_role)
await db_session.commit()
await db_session.refresh(user)
logger.info(f"管理员 {current_user.username} 创建了用户 {user.username}")
return _build_user_response(user)
@router.put("/users/{user_id}", response_model=UserResponse)
async def update_user(
user_id: int,
user_data: UserUpdate,
db_session: AsyncSession = Depends(get_db_session),
current_user: User = Depends(get_current_active_user)
):
check_admin(current_user)
result = await db_session.execute(select(User).where(User.id == user_id))
user = result.scalar_one_or_none()
if not user:
raise HTTPException(status_code=404, detail="用户不存在")
if user_data.email is not None:
user.email = user_data.email
if user_data.full_name is not None:
user.full_name = user_data.full_name
if user_data.is_active is not None:
user.is_active = user_data.is_active
if user_data.role_ids is not None:
await db_session.execute(
select(UserRole).where(UserRole.user_id == user_id)
)
for ur in (await db_session.execute(select(UserRole).where(UserRole.user_id == user_id))).scalars().all():
await db_session.delete(ur)
for role_id in user_data.role_ids:
user_role = UserRole(user_id=user.id, role_id=role_id)
db_session.add(user_role)
await db_session.commit()
await db_session.refresh(user)
logger.info(f"管理员 {current_user.username} 更新了用户 {user.username}")
return _build_user_response(user)
@router.delete("/users/{user_id}")
async def delete_user(
user_id: int,
db_session: AsyncSession = Depends(get_db_session),
current_user: User = Depends(get_current_active_user)
):
check_admin(current_user)
result = await db_session.execute(select(User).where(User.id == user_id))
user = result.scalar_one_or_none()
if not user:
raise HTTPException(status_code=404, detail="用户不存在")
if user.id == current_user.id:
raise HTTPException(status_code=400, detail="不能删除自己的账户")
username = user.username
await db_session.delete(user)
await db_session.commit()
logger.info(f"管理员 {current_user.username} 删除了用户 {username}")
return {"message": "用户已删除"}
@router.put("/users/{user_id}/reset-password")
async def reset_user_password(
user_id: int,
body: ResetPasswordRequest,
db_session: AsyncSession = Depends(get_db_session),
current_user: User = Depends(get_current_active_user)
):
check_admin(current_user)
result = await db_session.execute(select(User).where(User.id == user_id))
user = result.scalar_one_or_none()
if not user:
raise HTTPException(status_code=404, detail="用户不存在")
try:
user.hashed_password = get_password_hash(body.new_password)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
await db_session.commit()
logger.info(f"管理员 {current_user.username} 重置了用户 {user.username} 的密码")
return {"message": "密码已重置"}
@router.get("/roles", response_model=List[RoleResponse])
async def list_roles(
db_session: AsyncSession = Depends(get_db_session),
current_user: User = Depends(get_current_active_user)
):
check_admin(current_user)
result = await db_session.execute(select(Role))
roles = result.scalars().all()
return [
RoleResponse(
id=r.id,
code=r.code,
name=r.name,
description=r.description,
is_system=r.is_system,
permissions=[p.code for p in r.permissions]
) for r in roles
]
@router.post("/roles", response_model=RoleResponse, status_code=201)
async def create_role(
role_data: RoleCreate,
db_session: AsyncSession = Depends(get_db_session),
current_user: User = Depends(get_current_active_user)
):
check_admin(current_user)
existing = await db_session.execute(
select(Role).where(Role.code == role_data.code)
)
if existing.scalar_one_or_none():
raise HTTPException(status_code=400, detail="角色编码已存在")
role = Role(
code=role_data.code,
name=role_data.name,
description=role_data.description
)
db_session.add(role)
await db_session.commit()
await db_session.refresh(role)
logger.info(f"管理员 {current_user.username} 创建了角色 {role.code}")
return RoleResponse(
id=role.id,
code=role.code,
name=role.name,
description=role.description,
is_system=role.is_system,
permissions=[]
)
@router.put("/roles/{role_id}", response_model=RoleResponse)
async def update_role(
role_id: int,
role_data: RoleCreate,
db_session: AsyncSession = Depends(get_db_session),
current_user: User = Depends(get_current_active_user)
):
check_admin(current_user)
result = await db_session.execute(select(Role).where(Role.id == role_id))
role = result.scalar_one_or_none()
if not role:
raise HTTPException(status_code=404, detail="角色不存在")
if role.is_system:
raise HTTPException(status_code=400, detail="系统角色不能修改")
role.name = role_data.name
role.description = role_data.description
await db_session.commit()
await db_session.refresh(role)
return RoleResponse(
id=role.id,
code=role.code,
name=role.name,
description=role.description,
is_system=role.is_system,
permissions=[p.code for p in role.permissions]
)
@router.delete("/roles/{role_id}")
async def delete_role(
role_id: int,
db_session: AsyncSession = Depends(get_db_session),
current_user: User = Depends(get_current_active_user)
):
check_admin(current_user)
result = await db_session.execute(select(Role).where(Role.id == role_id))
role = result.scalar_one_or_none()
if not role:
raise HTTPException(status_code=404, detail="角色不存在")
if role.is_system:
raise HTTPException(status_code=400, detail="系统角色不能删除")
await db_session.delete(role)
await db_session.commit()
logger.info(f"管理员 {current_user.username} 删除了角色 {role.code}")
return {"message": "角色已删除"}
@router.put("/roles/{role_id}/permissions")
async def set_role_permissions(
role_id: int,
permission_ids: List[int],
db_session: AsyncSession = Depends(get_db_session),
current_user: User = Depends(get_current_active_user)
):
check_admin(current_user)
result = await db_session.execute(select(Role).where(Role.id == role_id))
role = result.scalar_one_or_none()
if not role:
raise HTTPException(status_code=404, detail="角色不存在")
for rp in (await db_session.execute(select(RolePermission).where(RolePermission.role_id == role_id))).scalars().all():
await db_session.delete(rp)
for perm_id in permission_ids:
rp = RolePermission(role_id=role_id, permission_id=perm_id)
db_session.add(rp)
await db_session.commit()
logger.info(f"管理员 {current_user.username} 更新了角色 {role.code} 的权限")
return {"message": "权限已更新"}
@router.get("/permissions", response_model=List[PermissionResponse])
async def list_permissions(
db_session: AsyncSession = Depends(get_db_session),
current_user: User = Depends(get_current_active_user)
):
check_admin(current_user)
result = await db_session.execute(select(Permission))
permissions = result.scalars().all()
return [PermissionResponse.from_orm(p) for p in permissions]
@router.post("/permissions", response_model=PermissionResponse, status_code=201)
async def create_permission(
perm_data: PermissionCreate,
db_session: AsyncSession = Depends(get_db_session),
current_user: User = Depends(get_current_active_user)
):
check_admin(current_user)
existing = await db_session.execute(
select(Permission).where(Permission.code == perm_data.code)
)
if existing.scalar_one_or_none():
raise HTTPException(status_code=400, detail="权限编码已存在")
permission = Permission(
code=perm_data.code,
name=perm_data.name,
module=perm_data.module,
description=perm_data.description
)
db_session.add(permission)
await db_session.commit()
await db_session.refresh(permission)
logger.info(f"管理员 {current_user.username} 创建了权限 {permission.code}")
return PermissionResponse.from_orm(permission)
@router.delete("/permissions/{permission_id}")
async def delete_permission(
permission_id: int,
db_session: AsyncSession = Depends(get_db_session),
current_user: User = Depends(get_current_active_user)
):
check_admin(current_user)
result = await db_session.execute(select(Permission).where(Permission.id == permission_id))
permission = result.scalar_one_or_none()
if not permission:
raise HTTPException(status_code=404, detail="权限不存在")
await db_session.delete(permission)
await db_session.commit()
logger.info(f"管理员 {current_user.username} 删除了权限 {permission.code}")
return {"message": "权限已删除"}