移除用户系统级角色,权限完全迁移至项目级管理
- 用户注册/创建不再需要选择角色,账号管理只管资料和状态 - 移除前后端所有系统级 role 字段的暴露,改用 is_admin 标识管理员 - PM(项目负责人)角色自动拥有全部项目权限且不可修改 - 系统管理员在项目中的成员身份不可被删除 - 管理界面全面美化 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -89,8 +89,6 @@ async def register(
|
||||
payload: UserRegisterRequest,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
):
|
||||
if payload.role == UserRole.ADMIN.value:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="不允许注册管理员账号")
|
||||
existing = await user_crud.get_by_email(db, payload.email)
|
||||
if existing:
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="邮箱已注册")
|
||||
|
||||
@@ -274,6 +274,9 @@ async def remove_member(
|
||||
member = await member_crud.get_member_by_id(db, member_id)
|
||||
if not member or member.study_id != study_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="成员不存在")
|
||||
target_user = await user_crud.get_by_id(db, member.user_id)
|
||||
if target_user and _role_value(target_user) == "ADMIN":
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="系统管理员不可从项目中移除")
|
||||
await _ensure_member_mutation_allowed(db, study_id, current_user, target_member=member)
|
||||
before_data = {"role_in_study": member.role_in_study, "is_active": member.is_active}
|
||||
removed = await member_crud.remove_member(db, member)
|
||||
|
||||
@@ -54,21 +54,17 @@ async def update_user(
|
||||
if user_crud.is_protected_admin_user(db_user):
|
||||
if user_in.email is not None and user_in.email != db_user.email:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="系统管理员邮箱不允许修改")
|
||||
if user_in.role is not None and user_in.role != "ADMIN":
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="系统管理员角色不允许修改")
|
||||
requested_status = user_in.status
|
||||
if user_in.is_active is not None:
|
||||
requested_status = "ACTIVE" if user_in.is_active else "DISABLED"
|
||||
if requested_status is not None and requested_status != "ACTIVE":
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="系统管理员不允许停用")
|
||||
if db_user.role.value == "ADMIN":
|
||||
requested_role = user_in.role
|
||||
requested_status = user_in.status
|
||||
if user_in.is_active is not None:
|
||||
requested_status = "ACTIVE" if user_in.is_active else "DISABLED"
|
||||
will_leave_admin = requested_role is not None and requested_role != "ADMIN"
|
||||
will_disable = requested_status is not None and requested_status != "ACTIVE"
|
||||
if (will_leave_admin or will_disable) and db_user.status.value == "ACTIVE":
|
||||
if will_disable and db_user.status.value == "ACTIVE":
|
||||
active_admins = await user_crud.count_active_admins(db)
|
||||
if active_admins <= 1:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="至少保留一个管理员账号")
|
||||
|
||||
@@ -48,7 +48,7 @@ async def role_has_api_permission(
|
||||
check_prerequisites: bool = True,
|
||||
) -> bool:
|
||||
"""检查角色是否有权访问特定接口"""
|
||||
if role == "ADMIN":
|
||||
if role == "ADMIN" or role == "PM":
|
||||
return True
|
||||
|
||||
permissions = await _get_project_permission_overrides(db, study_id)
|
||||
@@ -78,7 +78,7 @@ async def get_missing_prerequisites(
|
||||
endpoint_key: str,
|
||||
) -> list[str]:
|
||||
"""获取缺失的前置权限列表"""
|
||||
if role == "ADMIN":
|
||||
if role == "ADMIN" or role == "PM":
|
||||
return []
|
||||
|
||||
missing = []
|
||||
@@ -111,8 +111,11 @@ async def get_api_endpoint_permissions(
|
||||
for role in roles:
|
||||
matrix[role] = {}
|
||||
for endpoint_key, config in API_ENDPOINT_PERMISSIONS.items():
|
||||
default_allowed = role in config.get("default_roles", [])
|
||||
matrix[role][endpoint_key] = {"allowed": default_allowed}
|
||||
if role == "PM":
|
||||
matrix[role][endpoint_key] = {"allowed": True}
|
||||
else:
|
||||
default_allowed = role in config.get("default_roles", [])
|
||||
matrix[role][endpoint_key] = {"allowed": default_allowed}
|
||||
|
||||
for role, endpoints in overrides.items():
|
||||
if role not in matrix:
|
||||
@@ -136,7 +139,7 @@ async def replace_api_endpoint_permissions(
|
||||
)
|
||||
|
||||
for role, endpoints in payload.items():
|
||||
if role == "ADMIN":
|
||||
if role in ("ADMIN", "PM"):
|
||||
continue
|
||||
for endpoint_key, allowed in endpoints.items():
|
||||
if endpoint_key not in API_ENDPOINT_PERMISSIONS:
|
||||
|
||||
@@ -42,7 +42,7 @@ async def create_user(
|
||||
email=user_in.email,
|
||||
password_hash=hash_password(user_in.password),
|
||||
full_name=user_in.full_name,
|
||||
role=UserRole(user_in.role),
|
||||
role=UserRole.PV,
|
||||
clinical_department=user_in.clinical_department,
|
||||
status=status_value,
|
||||
)
|
||||
@@ -58,8 +58,6 @@ async def create_pending_user(db: AsyncSession, user_in: UserRegisterRequest) ->
|
||||
|
||||
async def update_user(db: AsyncSession, user: User, user_in: UserUpdate) -> User:
|
||||
update_data = {}
|
||||
if user_in.role is not None:
|
||||
update_data["role"] = UserRole(user_in.role)
|
||||
if user_in.status is not None:
|
||||
update_data["status"] = UserStatus(user_in.status)
|
||||
if user_in.is_active is not None:
|
||||
|
||||
@@ -67,6 +67,10 @@ class User(Base):
|
||||
def is_active(self) -> bool: # compatibility helper for existing checks
|
||||
return self.status == UserStatus.ACTIVE
|
||||
|
||||
@property
|
||||
def is_admin(self) -> bool:
|
||||
return self.role == UserRole.ADMIN
|
||||
|
||||
@property
|
||||
def username(self) -> str: # backward compatibility for existing UI copy
|
||||
return self.email
|
||||
|
||||
@@ -6,7 +6,6 @@ from typing import Literal, Optional
|
||||
from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator
|
||||
|
||||
UserRole = Literal["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA", "ADMIN"]
|
||||
RegisterRole = Literal["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"]
|
||||
UserStatus = Literal["PENDING", "ACTIVE", "REJECTED", "DISABLED"]
|
||||
|
||||
PASSWORD_REGEX = re.compile(r"^(?=.*[A-Za-z])(?=.*\d).{8,}$")
|
||||
@@ -16,7 +15,6 @@ class UserDisplay(BaseModel):
|
||||
id: uuid.UUID
|
||||
email: EmailStr
|
||||
full_name: str
|
||||
role: UserRole
|
||||
avatar_url: Optional[str] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
@@ -39,7 +37,6 @@ class UserRegisterRequest(_PasswordValidator):
|
||||
password: str = Field(min_length=8, max_length=72)
|
||||
email: EmailStr
|
||||
full_name: str = Field(min_length=1)
|
||||
role: RegisterRole
|
||||
clinical_department: str = Field(min_length=1)
|
||||
|
||||
|
||||
@@ -47,7 +44,6 @@ class UserCreate(_PasswordValidator):
|
||||
password: str = Field(min_length=8, max_length=72)
|
||||
email: EmailStr
|
||||
full_name: str = Field(min_length=1)
|
||||
role: UserRole
|
||||
clinical_department: str = Field(min_length=1)
|
||||
status: UserStatus = "ACTIVE"
|
||||
|
||||
@@ -57,10 +53,10 @@ class UserRead(BaseModel):
|
||||
email: EmailStr
|
||||
username: str
|
||||
full_name: str
|
||||
role: UserRole
|
||||
clinical_department: str
|
||||
status: UserStatus
|
||||
is_active: bool
|
||||
is_admin: bool = False
|
||||
created_at: datetime
|
||||
approved_at: Optional[datetime] = None
|
||||
approved_by: Optional[uuid.UUID] = None
|
||||
@@ -72,7 +68,6 @@ class UserRead(BaseModel):
|
||||
class UserUpdate(_PasswordValidator):
|
||||
email: Optional[EmailStr] = None
|
||||
full_name: Optional[str] = None
|
||||
role: Optional[UserRole] = None
|
||||
clinical_department: Optional[str] = None
|
||||
status: Optional[UserStatus] = None
|
||||
password: Optional[str] = None
|
||||
|
||||
Reference in New Issue
Block a user