收敛用户角色并共享项目库
- 后端限制系统只保留默认 admin 管理员,新建用户固定为标注员,并拒绝观察员或额外管理员角色。 - 将项目、帧、媒体解析、AI 标注、任务、Dashboard 和导出接口改为共享项目库访问,标注员具备同等项目管理和标注能力。 - 前端用户管理移除角色选择和观察员入口,只展示唯一管理员与标注员状态。 - 更新后端/前端测试,覆盖唯一 admin、旧 viewer 归一为标注员、用户删除和共享项目库访问。 - 同步更新 AGENTS 与 doc 文档中的角色权限、共享项目库和测试计划说明。
This commit is contained in:
@@ -10,7 +10,7 @@ from sqlalchemy.orm import Session
|
||||
from config import settings
|
||||
from database import get_db
|
||||
from models import Annotation, AuditLog, Frame, Mask, ProcessingTask, Project, Template, User
|
||||
from routers.auth import ensure_default_admin, hash_password, require_admin, write_audit_log
|
||||
from routers.auth import SUPPORTED_ROLES, ensure_default_admin, hash_password, normalize_user_role, require_admin, write_audit_log
|
||||
from schemas import (
|
||||
AdminUserCreate,
|
||||
AdminUserUpdate,
|
||||
@@ -30,18 +30,22 @@ from services.default_templates import restore_default_templates
|
||||
|
||||
router = APIRouter(prefix="/api/admin", tags=["Admin"])
|
||||
|
||||
VALID_ROLES = {"admin", "annotator", "viewer"}
|
||||
DEMO_RESET_CONFIRMATION = "RESET_DEMO_FACTORY"
|
||||
DEMO_PROJECT_NAME = DEMO_DICOM_PROJECT_NAME
|
||||
|
||||
|
||||
def _normalize_role(role: str | None) -> str:
|
||||
normalized = (role or "annotator").strip().lower()
|
||||
if normalized not in VALID_ROLES:
|
||||
if normalized not in SUPPORTED_ROLES:
|
||||
raise HTTPException(status_code=400, detail=f"Unsupported role: {role}")
|
||||
return normalized
|
||||
|
||||
|
||||
def _assert_non_admin_role(role: str) -> None:
|
||||
if role == "admin":
|
||||
raise HTTPException(status_code=400, detail="Only the default admin account can have admin role")
|
||||
|
||||
|
||||
@router.get("/users", response_model=List[UserOut], summary="List users")
|
||||
def list_users(
|
||||
db: Session = Depends(get_db),
|
||||
@@ -49,7 +53,8 @@ def list_users(
|
||||
) -> List[User]:
|
||||
"""Return all users for the administrator console."""
|
||||
_ = admin_user
|
||||
return db.query(User).order_by(User.id).all()
|
||||
users = db.query(User).order_by(User.id).all()
|
||||
return [normalize_user_role(db, user) for user in users]
|
||||
|
||||
|
||||
@router.post(
|
||||
@@ -69,10 +74,12 @@ def create_user(
|
||||
raise HTTPException(status_code=400, detail="Username is required")
|
||||
if len(payload.password) < 6:
|
||||
raise HTTPException(status_code=400, detail="Password must be at least 6 characters")
|
||||
role = _normalize_role(payload.role)
|
||||
_assert_non_admin_role(role)
|
||||
user = User(
|
||||
username=username,
|
||||
password_hash=hash_password(payload.password),
|
||||
role=_normalize_role(payload.role),
|
||||
role=role,
|
||||
is_active=1 if payload.is_active else 0,
|
||||
)
|
||||
db.add(user)
|
||||
@@ -104,6 +111,7 @@ def update_user(
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
user = normalize_user_role(db, user)
|
||||
|
||||
updates = payload.model_dump(exclude_unset=True)
|
||||
audit_detail: dict = {"before": {"username": user.username, "role": user.role, "is_active": bool(user.is_active)}}
|
||||
@@ -111,6 +119,8 @@ def update_user(
|
||||
username = (updates["username"] or "").strip()
|
||||
if not username:
|
||||
raise HTTPException(status_code=400, detail="Username is required")
|
||||
if user.role == "admin" and username != settings.default_admin_username:
|
||||
raise HTTPException(status_code=400, detail="Default admin username cannot be changed")
|
||||
user.username = username
|
||||
if "password" in updates:
|
||||
password = updates["password"] or ""
|
||||
@@ -119,8 +129,11 @@ def update_user(
|
||||
user.password_hash = hash_password(password)
|
||||
if "role" in updates:
|
||||
next_role = _normalize_role(updates["role"])
|
||||
if user.id == admin_user.id and next_role != "admin":
|
||||
raise HTTPException(status_code=400, detail="Cannot remove your own admin role")
|
||||
if user.username == settings.default_admin_username:
|
||||
if next_role != "admin":
|
||||
raise HTTPException(status_code=400, detail="Cannot remove the default admin role")
|
||||
else:
|
||||
_assert_non_admin_role(next_role)
|
||||
user.role = next_role
|
||||
if "is_active" in updates:
|
||||
if user.id == admin_user.id and not updates["is_active"]:
|
||||
@@ -158,9 +171,9 @@ def delete_user(
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
owned_project_count = db.query(Project).filter(Project.owner_user_id == user_id).count()
|
||||
if owned_project_count:
|
||||
raise HTTPException(status_code=409, detail="User owns projects; deactivate or migrate projects first")
|
||||
user = normalize_user_role(db, user)
|
||||
if user.role == "admin":
|
||||
raise HTTPException(status_code=400, detail="Cannot delete the default admin account")
|
||||
username = user.username
|
||||
db.delete(user)
|
||||
db.commit()
|
||||
|
||||
Reference in New Issue
Block a user