Files
Pre_Seg_Server/backend/routers/auth.py
admin b5413066a0 添加Docker自包含部署分支
- 新增 Seg_Server_Docker 自包含部署内容,包含前后端、FastAPI、Celery、PostgreSQL、Redis、MinIO、演示视频和 DICOM 数据。

- 保留 demo 数据以支持恢复演示出厂设置,排除 SAM 2.1 .pt 权重并在 README 中补充下载命令。

- 补充 GPU 部署、backend/worker 镜像复用、frpc/frps + NPM 公网域名反代部署说明。

- 在 .env/.env.example 中用 # XXXX 标注局域网和公网域名部署需要修改的配置项。

- 添加部署分支 .gitignore,忽略本地模型权重、构建产物、缓存和日志。
2026-05-07 19:06:07 +08:00

223 lines
7.1 KiB
Python

"""Authentication endpoints and dependencies."""
from datetime import datetime, timedelta, timezone
from typing import Any
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from jose import JWTError, jwt
from passlib.context import CryptContext
from pydantic import BaseModel
from sqlalchemy.orm import Session
from config import settings
from database import get_db
from models import AuditLog, User
from schemas import LoginResponse, UserOut
router = APIRouter(prefix="/api/auth", tags=["Auth"])
password_context = CryptContext(schemes=["pbkdf2_sha256"], deprecated="auto")
bearer_scheme = HTTPBearer(auto_error=False)
SUPPORTED_ROLES = {"admin", "annotator"}
class LoginRequest(BaseModel):
username: str
password: str
def hash_password(password: str) -> str:
"""Hash a plain password for storage."""
return password_context.hash(password)
def verify_password(password: str, password_hash: str) -> bool:
"""Verify a plain password against a stored hash."""
return password_context.verify(password, password_hash)
def create_access_token(user: User, expires_delta: timedelta | None = None) -> str:
"""Create a signed JWT access token for a user."""
expire = datetime.now(timezone.utc) + (
expires_delta or timedelta(minutes=settings.access_token_expire_minutes)
)
payload: dict[str, Any] = {
"sub": str(user.id),
"username": user.username,
"role": user.role,
"exp": expire,
}
return jwt.encode(payload, settings.jwt_secret_key, algorithm=settings.jwt_algorithm)
def ensure_default_admin(db: Session) -> User:
"""Create and enforce the single default administrator account."""
existing = db.query(User).filter(User.username == settings.default_admin_username).first()
if existing:
changed = False
if existing.role != "admin":
existing.role = "admin"
changed = True
if not existing.is_active:
existing.is_active = 1
changed = True
extra_admins = db.query(User).filter(
User.role == "admin",
User.id != existing.id,
).all()
for user in extra_admins:
user.role = "annotator"
changed = True
if changed:
db.commit()
db.refresh(existing)
return existing
user = User(
username=settings.default_admin_username,
password_hash=hash_password(settings.default_admin_password),
role="admin",
is_active=1,
)
db.add(user)
db.commit()
db.refresh(user)
extra_admins = db.query(User).filter(
User.role == "admin",
User.id != user.id,
).all()
if extra_admins:
for extra_user in extra_admins:
extra_user.role = "annotator"
db.commit()
db.refresh(user)
return user
def normalize_user_role(db: Session, user: User) -> User:
"""Keep legacy accounts within the current two-role policy."""
desired_role = "admin" if user.username == settings.default_admin_username else "annotator"
changed = False
if user.role != desired_role:
user.role = desired_role
changed = True
if user.username == settings.default_admin_username and not user.is_active:
user.is_active = 1
changed = True
if changed:
db.commit()
db.refresh(user)
return user
def get_current_user(
credentials: HTTPAuthorizationCredentials | None = Depends(bearer_scheme),
db: Session = Depends(get_db),
) -> User:
"""Resolve and validate the current user from the Bearer token."""
if credentials is None or credentials.scheme.lower() != "bearer":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(
credentials.credentials,
settings.jwt_secret_key,
algorithms=[settings.jwt_algorithm],
)
user_id = int(payload.get("sub"))
except (JWTError, TypeError, ValueError) as exc:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or expired token",
headers={"WWW-Authenticate": "Bearer"},
) from exc
user = db.query(User).filter(User.id == user_id).first()
if user:
user = normalize_user_role(db, user)
if not user or not user.is_active:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Inactive or missing user",
headers={"WWW-Authenticate": "Bearer"},
)
return user
def require_admin(current_user: User = Depends(get_current_user)) -> User:
"""Require the current user to have the administrator role."""
if current_user.role != "admin":
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin permission required")
return current_user
def require_editor(current_user: User = Depends(get_current_user)) -> User:
"""Require a user role that can modify segmentation data."""
if current_user.role not in SUPPORTED_ROLES:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Edit permission required")
return current_user
def write_audit_log(
db: Session,
*,
actor: User | None,
action: str,
target_type: str | None = None,
target_id: str | int | None = None,
detail: dict[str, Any] | None = None,
) -> AuditLog:
"""Persist a compact audit event."""
log = AuditLog(
actor_user_id=actor.id if actor else None,
action=action,
target_type=target_type,
target_id=str(target_id) if target_id is not None else None,
detail=detail or {},
)
db.add(log)
db.commit()
db.refresh(log)
return log
@router.post("/login", response_model=LoginResponse)
def login(payload: LoginRequest, db: Session = Depends(get_db)) -> dict:
"""Authenticate a user and return a signed JWT."""
ensure_default_admin(db)
user = db.query(User).filter(User.username == payload.username).first()
if user:
user = normalize_user_role(db, user)
if not user or not user.is_active or not verify_password(payload.password, user.password_hash):
write_audit_log(
db,
actor=None,
action="auth.login_failed",
target_type="user",
target_id=payload.username,
detail={"username": payload.username},
)
raise HTTPException(status_code=401, detail="Invalid credentials")
write_audit_log(
db,
actor=user,
action="auth.login_success",
target_type="user",
target_id=user.id,
detail={"username": user.username},
)
return {
"token": create_access_token(user),
"token_type": "bearer",
"username": user.username,
"user": user,
}
@router.get("/me", response_model=UserOut)
def read_current_user(current_user: User = Depends(get_current_user)) -> User:
"""Return the authenticated user profile."""
return current_user