清理全局角色残留并修复项目权限判断
This commit is contained in:
@@ -3,9 +3,9 @@ import uuid
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.deps import get_db_session, require_roles
|
||||
from app.core.deps import get_db_session, is_system_admin, require_roles
|
||||
from app.crud import user as user_crud
|
||||
from app.models.user import User, UserRole, UserStatus
|
||||
from app.models.user import User, UserStatus
|
||||
from app.schemas.user import AdminUserListResponse, UserAdminReviewRequest, UserResponse
|
||||
|
||||
router = APIRouter(prefix="/users")
|
||||
@@ -15,7 +15,7 @@ router = APIRouter(prefix="/users")
|
||||
async def list_users_for_review(
|
||||
status_filter: UserStatus = Query(default=UserStatus.PENDING, alias="status"),
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(require_roles([UserRole.ADMIN.value])),
|
||||
current_user=Depends(require_roles(["ADMIN"])),
|
||||
) -> AdminUserListResponse:
|
||||
users = await user_crud.list_users_by_status(db, status=status_filter)
|
||||
return AdminUserListResponse(items=list(users), total=len(users))
|
||||
@@ -25,7 +25,7 @@ async def _get_review_user(db: AsyncSession, user_id: uuid.UUID) -> User:
|
||||
user = await user_crud.get_by_id(db, user_id)
|
||||
if not user:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在")
|
||||
if user.role == UserRole.ADMIN:
|
||||
if is_system_admin(user):
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="管理员账号不允许审核")
|
||||
return user
|
||||
|
||||
@@ -35,7 +35,7 @@ async def approve_user(
|
||||
user_id: uuid.UUID,
|
||||
review: UserAdminReviewRequest,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(require_roles([UserRole.ADMIN.value])),
|
||||
current_user=Depends(require_roles(["ADMIN"])),
|
||||
) -> UserResponse:
|
||||
if review.action != "approve":
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="非法操作")
|
||||
@@ -51,7 +51,7 @@ async def reject_user(
|
||||
user_id: uuid.UUID,
|
||||
review: UserAdminReviewRequest,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(require_roles([UserRole.ADMIN.value])),
|
||||
current_user=Depends(require_roles(["ADMIN"])),
|
||||
) -> UserResponse:
|
||||
if review.action not in ("reject", "approve"):
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="非法操作")
|
||||
|
||||
@@ -5,7 +5,7 @@ from datetime import date
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.deps import get_cra_site_scope, get_current_user, get_db_session, require_study_not_locked, require_api_permission
|
||||
from app.core.deps import get_operator_role_label, get_cra_site_scope, get_current_user, get_db_session, require_study_not_locked, require_api_permission
|
||||
from app.crud import ae as ae_crud
|
||||
from app.crud import audit as audit_crud
|
||||
from app.crud import member as member_crud
|
||||
@@ -79,7 +79,7 @@ async def create_ae(
|
||||
action="CREATE_AE",
|
||||
detail=f"AE {ae.id} 已创建",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
data = AERead.model_validate(ae)
|
||||
data.is_overdue = _is_overdue(data)
|
||||
@@ -249,7 +249,7 @@ async def update_ae(
|
||||
action=action,
|
||||
detail=json.dumps({"before": detail_before, "after": detail_after}, ensure_ascii=False, default=str),
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
data = AERead.model_validate(updated)
|
||||
data.is_overdue = _is_overdue(data)
|
||||
@@ -281,5 +281,5 @@ async def delete_ae(
|
||||
action="DELETE_AE",
|
||||
detail=f"AE {ae_id} 已删除",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
|
||||
@@ -9,7 +9,7 @@ from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.deps import get_current_user, get_db_session, require_study_member, require_study_roles
|
||||
from app.core.deps import get_current_user, get_db_session, is_system_admin, require_study_member, require_system_permission
|
||||
from app.core.project_permissions import (
|
||||
get_api_endpoint_permissions,
|
||||
replace_api_endpoint_permissions,
|
||||
@@ -163,8 +163,7 @@ async def get_my_study_api_permissions(
|
||||
db: Annotated[AsyncSession, Depends(get_db_session)] = None,
|
||||
):
|
||||
"""获取当前用户当前项目角色的有效权限。"""
|
||||
role_value = current_user.role.value if hasattr(current_user.role, "value") else str(current_user.role)
|
||||
if role_value == "ADMIN":
|
||||
if is_system_admin(current_user):
|
||||
return {
|
||||
"ADMIN": {
|
||||
endpoint_key: {"allowed": True}
|
||||
@@ -192,7 +191,7 @@ async def get_my_study_api_permissions(
|
||||
)
|
||||
async def get_study_api_permissions(
|
||||
study_id: uuid.UUID,
|
||||
_=Depends(require_study_roles(["PM"])),
|
||||
_=Depends(require_system_permission("system:permissions:project_config")),
|
||||
db: Annotated[AsyncSession, Depends(get_db_session)] = None,
|
||||
):
|
||||
"""获取项目的接口级权限矩阵
|
||||
@@ -233,7 +232,7 @@ async def get_study_api_permissions(
|
||||
async def update_study_api_permissions(
|
||||
study_id: uuid.UUID,
|
||||
payload: dict[str, dict[str, bool]],
|
||||
_=Depends(require_study_roles(["PM"])),
|
||||
_=Depends(require_system_permission("system:permissions:project_config")),
|
||||
current_user=Depends(get_current_user),
|
||||
db: Annotated[AsyncSession, Depends(get_db_session)] = None,
|
||||
):
|
||||
@@ -248,13 +247,12 @@ async def update_study_api_permissions(
|
||||
"""
|
||||
# 验证输入
|
||||
configurable_roles = set(await _get_configurable_roles(db, study_id))
|
||||
current_role = current_user.role.value if hasattr(current_user.role, "value") else str(current_user.role)
|
||||
for role in payload.keys():
|
||||
if role == "ADMIN":
|
||||
continue
|
||||
if role not in configurable_roles:
|
||||
raise ValueError(f"无效的角色: {role}")
|
||||
if role == "PM" and current_role != "ADMIN":
|
||||
if role == "PM" and not is_system_admin(current_user):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="仅系统管理员可修改项目负责人权限",
|
||||
|
||||
@@ -8,7 +8,7 @@ from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status,
|
||||
from fastapi.responses import FileResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.deps import get_current_user, get_db_session, get_study_member, require_study_not_locked
|
||||
from app.core.deps import get_current_user, get_db_session, get_operator_role_label, get_study_member, is_system_admin, require_study_not_locked
|
||||
from app.core.project_permissions import role_has_api_permission
|
||||
from app.crud import attachment as attachment_crud
|
||||
from app.crud import audit as audit_crud
|
||||
@@ -71,10 +71,6 @@ async def _ensure_study_exists(db: AsyncSession, study_id: uuid.UUID):
|
||||
return study
|
||||
|
||||
|
||||
def _role_value(user) -> str:
|
||||
return user.role.value if hasattr(user.role, "value") else user.role
|
||||
|
||||
|
||||
def _attachment_operation(action: str) -> str:
|
||||
if action in {"create", "read", "delete"}:
|
||||
return action
|
||||
@@ -154,8 +150,7 @@ async def _ensure_attachment_permission(
|
||||
membership=None,
|
||||
) -> None:
|
||||
parent_permission = await _resolve_attachment_parent_permission(db, study_id, entity_type, entity_id, action)
|
||||
role_value = _role_value(current_user)
|
||||
if role_value == "ADMIN":
|
||||
if is_system_admin(current_user):
|
||||
return
|
||||
|
||||
if membership is None:
|
||||
@@ -238,7 +233,7 @@ async def upload_attachment(
|
||||
action="UPLOAD_FILE",
|
||||
detail=f"文件已上传:{file.filename}",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
return AttachmentRead(
|
||||
id=attachment.id,
|
||||
@@ -349,7 +344,7 @@ async def _authorize_global(request: Request, db: AsyncSession, study_id: uuid.U
|
||||
user = await user_crud.get_by_id(db, uuid.UUID(str(payload.get("sub"))))
|
||||
if not user or not user.is_active:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="账号不存在或已停用")
|
||||
if _role_value(user) == "ADMIN":
|
||||
if is_system_admin(user):
|
||||
return user, None
|
||||
membership = await member_crud.get_member(db, study_id, user.id)
|
||||
if not membership or not membership.is_active:
|
||||
@@ -429,7 +424,7 @@ async def global_delete_attachment(
|
||||
db, attachment.study_id, attachment.entity_type, attachment.entity_id, "delete", user, membership
|
||||
)
|
||||
can_delete = (
|
||||
_role_value(user) == "ADMIN"
|
||||
is_system_admin(user)
|
||||
or attachment.uploaded_by == user.id
|
||||
or membership is not None
|
||||
)
|
||||
@@ -444,7 +439,7 @@ async def global_delete_attachment(
|
||||
action="DELETE_ATTACHMENT",
|
||||
detail=f"文件已删除:{attachment.filename}",
|
||||
operator_id=user.id,
|
||||
operator_role=user.role,
|
||||
operator_role=await get_operator_role_label(db, attachment.study_id, user),
|
||||
)
|
||||
|
||||
|
||||
@@ -476,11 +471,11 @@ async def delete_attachment(
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="附件不存在")
|
||||
|
||||
membership = None
|
||||
if _role_value(current_user) != "ADMIN":
|
||||
if not is_system_admin(current_user):
|
||||
membership = await get_study_member(study_id, current_user=current_user, db=db)
|
||||
|
||||
can_delete = (
|
||||
_role_value(current_user) == "ADMIN"
|
||||
is_system_admin(current_user)
|
||||
or attachment.uploaded_by == current_user.id
|
||||
or membership is not None
|
||||
)
|
||||
@@ -496,5 +491,5 @@ async def delete_attachment(
|
||||
action="DELETE_ATTACHMENT",
|
||||
detail=f"文件已删除:{attachment.filename}",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@ import uuid
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.deps import get_current_user, get_db_session, require_roles, require_study_member, require_api_permission
|
||||
from app.core.deps import get_operator_role_label, get_current_user, get_db_session, require_roles, require_api_permission
|
||||
from app.crud import audit as audit_crud
|
||||
from app.schemas.audit import AuditEventCreate, AuditLogRead
|
||||
|
||||
@@ -57,11 +57,11 @@ async def delete_audit_log(
|
||||
@router.post(
|
||||
"/events",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
dependencies=[Depends(require_study_member())],
|
||||
)
|
||||
async def create_audit_event(
|
||||
study_id: uuid.UUID,
|
||||
payload: AuditEventCreate,
|
||||
_export_permission=Depends(require_api_permission("audit_logs:export")),
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
):
|
||||
@@ -76,6 +76,6 @@ async def create_audit_event(
|
||||
action=payload.action,
|
||||
detail=payload.detail,
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
return {"ok": True}
|
||||
|
||||
@@ -11,7 +11,7 @@ from app.core.login_crypto import create_login_challenge, decrypt_login_payload,
|
||||
from app.core.security import create_access_token, decode_token_allow_expired, oauth2_scheme, verify_password
|
||||
from app.core.deps import get_current_user, get_db_session
|
||||
from app.crud import user as user_crud
|
||||
from app.models.user import UserRole, UserStatus
|
||||
from app.models.user import UserStatus
|
||||
from app.schemas.user import Token, UserRead, UserRegisterRequest, UserSelfUpdate, UserUpdate
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
@@ -57,7 +57,6 @@ def issue_user_token(db_user) -> Token:
|
||||
session_start = datetime.now(timezone.utc)
|
||||
access_token = create_access_token(
|
||||
user_id=str(db_user.id),
|
||||
role=db_user.role.value if hasattr(db_user.role, "value") else db_user.role,
|
||||
expires_minutes=None,
|
||||
session_start=session_start,
|
||||
)
|
||||
@@ -191,7 +190,6 @@ async def extend_access_token(
|
||||
session_start = datetime.now(timezone.utc)
|
||||
new_token = create_access_token(
|
||||
user_id=str(db_user.id),
|
||||
role=db_user.role.value if hasattr(db_user.role, "value") else db_user.role,
|
||||
expires_minutes=None,
|
||||
session_start=session_start,
|
||||
)
|
||||
@@ -210,7 +208,6 @@ async def unlock_session(
|
||||
session_start = datetime.now(timezone.utc)
|
||||
access_token = create_access_token(
|
||||
user_id=str(db_user.id),
|
||||
role=db_user.role.value if hasattr(db_user.role, "value") else db_user.role,
|
||||
expires_minutes=None,
|
||||
session_start=session_start,
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@ from fastapi import APIRouter, Depends
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.deps import get_cra_site_scope, get_current_user, get_db_session, require_study_member, require_api_permission
|
||||
from app.core.deps import get_cra_site_scope, get_current_user, get_db_session, is_system_admin, require_api_permission
|
||||
from app.core.project_permissions import role_has_api_permission
|
||||
from app.crud import member as member_crud
|
||||
from app.models.milestone import Milestone
|
||||
@@ -16,13 +16,10 @@ from app.crud import overview as overview_crud
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _role_value(user) -> str:
|
||||
return user.role.value if hasattr(user.role, "value") else str(user.role)
|
||||
|
||||
|
||||
@router.get("/progress", response_model=StudyProgressRead, dependencies=[Depends(require_study_member())])
|
||||
@router.get("/progress", response_model=StudyProgressRead)
|
||||
async def get_progress(
|
||||
study_id: uuid.UUID,
|
||||
_=Depends(require_api_permission("project_overview:read")),
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> StudyProgressRead:
|
||||
milestone_total_stmt = select(func.count()).select_from(Milestone).where(Milestone.study_id == study_id)
|
||||
@@ -42,10 +39,11 @@ async def get_progress(
|
||||
)
|
||||
|
||||
|
||||
@router.get("/lost-visits", response_model=list[VisitLostItem], dependencies=[Depends(require_study_member())])
|
||||
@router.get("/lost-visits", response_model=list[VisitLostItem])
|
||||
async def list_lost_visits(
|
||||
study_id: uuid.UUID,
|
||||
limit: int = 20,
|
||||
_=Depends(require_api_permission("project_overview:read")),
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> list[VisitLostItem]:
|
||||
@@ -68,18 +66,15 @@ async def list_lost_visits(
|
||||
return items
|
||||
|
||||
|
||||
@router.get("/center-summary", response_model=list[CenterSummaryItem], dependencies=[
|
||||
Depends(require_study_member()),
|
||||
Depends(require_api_permission("project_overview:read"))
|
||||
])
|
||||
@router.get("/center-summary", response_model=list[CenterSummaryItem])
|
||||
async def get_center_summary(
|
||||
study_id: uuid.UUID,
|
||||
_=Depends(require_api_permission("project_overview:read")),
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> list[CenterSummaryItem]:
|
||||
role_value = _role_value(current_user)
|
||||
membership = None
|
||||
if role_value != "ADMIN":
|
||||
if not is_system_admin(current_user):
|
||||
membership = await member_crud.get_member(db, study_id, current_user.id)
|
||||
if not membership or not membership.is_active:
|
||||
return []
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, File, Form, HTTPException, Query, UploadFile, status
|
||||
from fastapi import APIRouter, Depends, File, Form, Query, UploadFile, status
|
||||
from fastapi.responses import FileResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -11,7 +11,6 @@ from app.schemas.common import PaginatedResponse
|
||||
from app.schemas.distribution import DistributionCreate, DistributionRead
|
||||
from app.schemas.document import DocumentCreate, DocumentDetail, DocumentSummary
|
||||
from app.schemas.document_version import DocumentVersionRead
|
||||
from app.models.user import UserRole
|
||||
from app.services import document_service
|
||||
from app.utils.pagination import paginate
|
||||
|
||||
@@ -73,9 +72,6 @@ async def delete_document(
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> DocumentSummary:
|
||||
role_value = current_user.role.value if hasattr(current_user.role, "value") else current_user.role
|
||||
if role_value != UserRole.ADMIN.value:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="仅管理员可删除文档")
|
||||
doc = await document_service.delete_document(db, document_id, current_user)
|
||||
return DocumentSummary.model_validate(doc)
|
||||
|
||||
@@ -123,9 +119,6 @@ async def delete_version(
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> None:
|
||||
role_value = current_user.role.value if hasattr(current_user.role, "value") else current_user.role
|
||||
if role_value != UserRole.ADMIN.value:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="仅管理员可删除版本")
|
||||
await document_service.delete_version(db, version_id, current_user)
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import uuid
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.deps import get_cra_site_scope, get_current_user, get_db_session, require_study_not_locked, require_api_permission
|
||||
from app.core.deps import get_operator_role_label, get_cra_site_scope, get_current_user, get_db_session, require_study_not_locked, require_api_permission
|
||||
from app.crud import audit as audit_crud
|
||||
from app.crud import drug_shipment as shipment_crud
|
||||
from app.crud import site as site_crud
|
||||
@@ -56,7 +56,7 @@ async def create_shipment(
|
||||
action="CREATE_DRUG_SHIPMENT",
|
||||
detail=f"药品运输 {shipment.id} 已创建",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
return DrugShipmentRead.model_validate(shipment)
|
||||
|
||||
@@ -152,7 +152,7 @@ async def update_shipment(
|
||||
action="UPDATE_DRUG_SHIPMENT",
|
||||
detail=f"药品运输 {shipment_id} 已更新",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
return DrugShipmentRead.model_validate(shipment)
|
||||
|
||||
@@ -185,5 +185,5 @@ async def delete_shipment(
|
||||
action="DELETE_DRUG_SHIPMENT",
|
||||
detail=f"药品运输 {shipment_id} 已删除",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@ import uuid
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.deps import get_current_user, get_db_session, require_study_not_locked, require_api_permission
|
||||
from app.core.deps import get_current_user, get_db_session, get_operator_role_label, is_system_admin, require_study_not_locked, require_api_permission
|
||||
from app.core.project_permissions import role_has_api_permission
|
||||
from app.crud import audit as audit_crud
|
||||
from app.crud import faq_category as category_crud
|
||||
@@ -17,8 +17,7 @@ router = APIRouter()
|
||||
|
||||
|
||||
def _is_system_admin(current_user) -> bool:
|
||||
role = current_user.role.value if hasattr(current_user.role, "value") else current_user.role
|
||||
return role == "ADMIN"
|
||||
return is_system_admin(current_user)
|
||||
|
||||
|
||||
@router.post(
|
||||
@@ -51,7 +50,7 @@ async def create_category(
|
||||
action="CREATE_FAQ_CATEGORY",
|
||||
detail=f"FAQ 分类 {category.name} 已创建",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
return CategoryRead.model_validate(category)
|
||||
|
||||
@@ -113,7 +112,7 @@ async def update_category(
|
||||
action="UPDATE_FAQ_CATEGORY",
|
||||
detail=f"FAQ 分类 {category_id} 已更新",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
return CategoryRead.model_validate(updated)
|
||||
|
||||
@@ -151,5 +150,5 @@ async def delete_category(
|
||||
action="DELETE_FAQ_CATEGORY",
|
||||
detail=f"FAQ 分类 {category_id} 已删除",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@ import uuid
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.deps import get_current_user, get_db_session, require_study_not_locked, require_api_permission
|
||||
from app.core.deps import get_current_user, get_db_session, get_operator_role_label, is_system_admin, require_study_not_locked, require_api_permission
|
||||
from app.core.project_permissions import role_has_api_permission
|
||||
from app.crud import audit as audit_crud
|
||||
from app.crud import faq_category as category_crud
|
||||
@@ -27,8 +27,7 @@ router = APIRouter()
|
||||
|
||||
|
||||
def _is_system_admin(current_user) -> bool:
|
||||
role = current_user.role.value if hasattr(current_user.role, "value") else current_user.role
|
||||
return role == "ADMIN"
|
||||
return is_system_admin(current_user)
|
||||
|
||||
|
||||
@router.post(
|
||||
@@ -75,7 +74,7 @@ async def create_faq(
|
||||
action="CREATE_FAQ_ITEM",
|
||||
detail="FAQ 已创建",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
return FaqRead.model_validate(item)
|
||||
|
||||
@@ -108,8 +107,7 @@ async def list_faqs(
|
||||
if not study_id:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="必须提供项目 ID")
|
||||
|
||||
role_value = current_user.role.value if hasattr(current_user.role, "value") else current_user.role
|
||||
if role_value != "ADMIN":
|
||||
if not is_system_admin(current_user):
|
||||
member = await member_crud.get_member(db, study_id, current_user.id)
|
||||
if not member or not member.is_active:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="不是项目成员")
|
||||
@@ -158,8 +156,7 @@ async def get_faq(
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="FAQ 不存在")
|
||||
if not item.study_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="FAQ 不存在")
|
||||
role_value = current_user.role.value if hasattr(current_user.role, "value") else current_user.role
|
||||
if not item.is_active and role_value != "ADMIN":
|
||||
if not item.is_active and not is_system_admin(current_user):
|
||||
member = await member_crud.get_member(db, item.study_id, current_user.id)
|
||||
if not member or not member.is_active:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="不是项目成员")
|
||||
@@ -203,7 +200,7 @@ async def update_faq(
|
||||
action=action,
|
||||
detail=detail,
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
return FaqRead.model_validate(updated)
|
||||
|
||||
@@ -359,7 +356,7 @@ async def create_reply(
|
||||
action="CREATE_FAQ_REPLY",
|
||||
detail="FAQ 已回复",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
data = FaqReplyRead.model_validate(reply)
|
||||
if quote:
|
||||
@@ -404,7 +401,7 @@ async def delete_faq(
|
||||
action="DELETE_FAQ_ITEM",
|
||||
detail="FAQ 已删除",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
|
||||
|
||||
@@ -457,5 +454,5 @@ async def delete_reply(
|
||||
action="DELETE_FAQ_REPLY",
|
||||
detail="FAQ 回复已删除",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.core.deps import get_cra_site_scope, get_current_user, get_db_session, require_study_not_locked, require_api_permission
|
||||
from app.core.deps import get_cra_site_scope, get_current_user, get_db_session, get_operator_role_label, is_system_admin, require_study_not_locked, require_api_permission
|
||||
from app.core.project_permissions import role_has_api_permission
|
||||
from app.crud import audit as audit_crud
|
||||
from app.crud import contract_fee as contract_fee_crud
|
||||
@@ -22,7 +22,7 @@ from app.schemas.contract_fee_payment import (
|
||||
ContractFeePaymentUpdate,
|
||||
)
|
||||
from app.schemas.fee_common import FeeApiResponse
|
||||
from app.schemas.fee_attachment import FeeAttachmentRead
|
||||
from app.schemas.attachment import AttachmentRead
|
||||
from app.schemas.user import UserDisplay
|
||||
from app.models.attachment import Attachment
|
||||
|
||||
@@ -33,8 +33,7 @@ async def _ensure_project_access(db: AsyncSession, project_id: uuid.UUID, curren
|
||||
study = await study_crud.get(db, project_id)
|
||||
if not study:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="项目不存在")
|
||||
role_value = current_user.role.value if hasattr(current_user.role, "value") else current_user.role
|
||||
if role_value == "ADMIN":
|
||||
if is_system_admin(current_user):
|
||||
return None
|
||||
membership = await member_crud.get_member(db, project_id, current_user.id)
|
||||
if not membership or not membership.is_active:
|
||||
@@ -163,7 +162,7 @@ async def get_contract_fee(
|
||||
user_ids = {a.uploaded_by for a in attachments if a.uploaded_by}
|
||||
users_map = await user_crud.get_users_by_ids(db, user_ids)
|
||||
|
||||
attachments_map: dict[str, list[FeeAttachmentRead]] = {
|
||||
attachments_map: dict[str, list[AttachmentRead]] = {
|
||||
"contract": [],
|
||||
"voucher": [],
|
||||
"invoice": [],
|
||||
@@ -173,16 +172,11 @@ async def get_contract_fee(
|
||||
attachments_map.setdefault(key, [])
|
||||
user = users_map.get(attachment.uploaded_by)
|
||||
attachments_map[key].append(
|
||||
FeeAttachmentRead(
|
||||
AttachmentRead(
|
||||
id=attachment.id,
|
||||
entity_type="contract_fee",
|
||||
entity_id=attachment.entity_id,
|
||||
file_type=key,
|
||||
filename=attachment.filename,
|
||||
mime_type=attachment.content_type,
|
||||
size=attachment.file_size,
|
||||
storage_key=attachment.file_path,
|
||||
url=None,
|
||||
file_size=attachment.file_size,
|
||||
content_type=attachment.content_type,
|
||||
uploaded_by_id=attachment.uploaded_by,
|
||||
uploaded_by=UserDisplay.model_validate(user) if user else None,
|
||||
uploaded_at=attachment.uploaded_at,
|
||||
@@ -244,7 +238,7 @@ async def create_contract_fee(
|
||||
action="CREATE_CONTRACT_FEE",
|
||||
detail="合同费用已创建",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
return FeeApiResponse(data=ContractFeeRead.model_validate(contract))
|
||||
|
||||
@@ -274,7 +268,7 @@ async def update_contract_fee(
|
||||
action="UPDATE_CONTRACT_FEE",
|
||||
detail="合同费用已更新",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
return FeeApiResponse(data=ContractFeeRead.model_validate(contract))
|
||||
|
||||
@@ -303,7 +297,7 @@ async def delete_contract_fee(
|
||||
action="DELETE_CONTRACT_FEE",
|
||||
detail="合同费用已删除",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
|
||||
|
||||
@@ -333,7 +327,7 @@ async def create_contract_payment(
|
||||
action="CREATE_CONTRACT_FEE_PAYMENT",
|
||||
detail="合同费用分期已创建",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
return FeeApiResponse(data=ContractFeePaymentRead.model_validate(payment))
|
||||
|
||||
@@ -374,7 +368,7 @@ async def update_contract_payment(
|
||||
action="UPDATE_CONTRACT_FEE_PAYMENT",
|
||||
detail="合同费用分期已更新",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
return FeeApiResponse(data=ContractFeePaymentRead.model_validate(payment))
|
||||
|
||||
@@ -406,5 +400,5 @@ async def delete_contract_payment(
|
||||
action="DELETE_CONTRACT_FEE_PAYMENT",
|
||||
detail="合同费用分期已删除",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@ from datetime import date
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.deps import get_db_session, require_study_member
|
||||
from app.core.deps import get_db_session, require_api_permission
|
||||
from app.crud import finance as finance_crud
|
||||
from app.crud import study as study_crud
|
||||
from app.schemas.finance import FinanceSummaryRead
|
||||
@@ -12,11 +12,12 @@ from app.schemas.finance import FinanceSummaryRead
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/summary", response_model=FinanceSummaryRead, dependencies=[Depends(require_study_member())])
|
||||
@router.get("/summary", response_model=FinanceSummaryRead)
|
||||
async def finance_summary(
|
||||
study_id: uuid.UUID,
|
||||
date_from: date | None = None,
|
||||
date_to: date | None = None,
|
||||
_=Depends(require_api_permission("fees_contracts:read")),
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> FinanceSummaryRead:
|
||||
# ensure study exists
|
||||
|
||||
@@ -3,7 +3,7 @@ import uuid
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.deps import get_current_user, get_db_session, require_study_not_locked, require_api_permission
|
||||
from app.core.deps import get_operator_role_label, get_current_user, get_db_session, require_study_not_locked, require_api_permission
|
||||
from app.crud import audit as audit_crud
|
||||
from app.crud import material_equipment as equipment_crud
|
||||
from app.crud import study as study_crud
|
||||
@@ -49,7 +49,7 @@ async def create_equipment(
|
||||
action="CREATE_MATERIAL_EQUIPMENT",
|
||||
detail=f"设备 {item.id} 已创建",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
return MaterialEquipmentRead.model_validate(item)
|
||||
|
||||
@@ -120,7 +120,7 @@ async def update_equipment(
|
||||
action="UPDATE_MATERIAL_EQUIPMENT",
|
||||
detail=f"设备 {equipment_id} 已更新",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
return MaterialEquipmentRead.model_validate(item)
|
||||
|
||||
@@ -149,5 +149,5 @@ async def delete_equipment(
|
||||
action="DELETE_MATERIAL_EQUIPMENT",
|
||||
detail=f"设备 {equipment_id} 已删除",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@ import json
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.deps import get_current_user, get_db_session, require_study_member, require_api_permission, require_study_not_locked
|
||||
from app.core.deps import get_current_user, get_db_session, is_system_admin, require_api_permission, require_study_not_locked
|
||||
from app.core.decorators import register_api_endpoint
|
||||
from app.crud import audit as audit_crud
|
||||
from app.crud import member as member_crud
|
||||
@@ -32,8 +32,11 @@ async def _ensure_study_exists(db: AsyncSession, study_id: uuid.UUID):
|
||||
return study
|
||||
|
||||
|
||||
def _role_value(user) -> str:
|
||||
return user.role.value if hasattr(user.role, "value") else str(user.role)
|
||||
async def _operator_project_role(db: AsyncSession, study_id: uuid.UUID, current_user) -> str:
|
||||
if is_system_admin(current_user):
|
||||
return "ADMIN"
|
||||
member = await member_crud.get_member(db, study_id, current_user.id)
|
||||
return member.role_in_study if member and member.is_active else ""
|
||||
|
||||
|
||||
def _role_rank(role: str | None) -> int:
|
||||
@@ -48,7 +51,7 @@ async def _ensure_member_mutation_allowed(
|
||||
target_member=None,
|
||||
target_role: str | None = None,
|
||||
) -> None:
|
||||
if _role_value(current_user) == "ADMIN":
|
||||
if is_system_admin(current_user):
|
||||
return
|
||||
|
||||
actor_member = await member_crud.get_member(db, study_id, current_user.id)
|
||||
@@ -60,7 +63,7 @@ async def _ensure_member_mutation_allowed(
|
||||
|
||||
if target_member:
|
||||
target_user = await user_crud.get_by_id(db, target_member.user_id)
|
||||
if _role_value(target_user) == "ADMIN":
|
||||
if is_system_admin(target_user):
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="不能修改系统管理员账号的项目权限")
|
||||
|
||||
if target_member and _role_rank(target_member.role_in_study) >= actor_rank:
|
||||
@@ -118,7 +121,7 @@ async def add_member(
|
||||
"after": {"is_active": updated.is_active, "role_in_study": updated.role_in_study},
|
||||
}, ensure_ascii=False),
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await _operator_project_role(db, study_id, current_user),
|
||||
)
|
||||
return updated
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="成员已存在")
|
||||
@@ -131,7 +134,7 @@ async def add_member(
|
||||
action="PROJECT_MEMBER_ADDED",
|
||||
detail=json.dumps({"targetName": str(member.user_id), "after": {"role_in_study": member.role_in_study, "is_active": member.is_active}}, ensure_ascii=False),
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await _operator_project_role(db, study_id, current_user),
|
||||
)
|
||||
return member
|
||||
|
||||
@@ -246,7 +249,7 @@ async def update_member(
|
||||
action="PROJECT_MEMBER_UPDATED",
|
||||
detail=json.dumps({"targetName": str(updated.user_id), "before": before_data, "after": after_data}, ensure_ascii=False),
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await _operator_project_role(db, study_id, current_user),
|
||||
)
|
||||
return updated
|
||||
|
||||
@@ -274,7 +277,7 @@ async def remove_member(
|
||||
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":
|
||||
if target_user and is_system_admin(target_user):
|
||||
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}
|
||||
@@ -287,6 +290,6 @@ async def remove_member(
|
||||
action="PROJECT_MEMBER_REMOVED",
|
||||
detail=json.dumps({"targetName": str(removed.user_id), "before": before_data, "after": {"is_active": removed.is_active}}, ensure_ascii=False),
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await _operator_project_role(db, study_id, current_user),
|
||||
)
|
||||
return removed
|
||||
|
||||
@@ -11,7 +11,7 @@ from openpyxl import Workbook, load_workbook
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.deps import get_current_user, get_db_session, require_study_not_locked, require_api_permission
|
||||
from app.core.deps import get_operator_role_label, get_current_user, get_db_session, require_study_not_locked, require_api_permission
|
||||
from app.crud import audit as audit_crud
|
||||
from app.crud import monitoring_visit_issue as issue_crud
|
||||
from app.crud import site as site_crud
|
||||
@@ -413,7 +413,7 @@ async def create_monitoring_visit_issue(
|
||||
action="CREATE_MONITORING_VISIT_ISSUE",
|
||||
detail=f"监查访视问题 {item.issue_no} 已创建",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
return _to_read(item)
|
||||
|
||||
@@ -617,7 +617,7 @@ async def update_monitoring_visit_issue(
|
||||
action="UPDATE_MONITORING_VISIT_ISSUE",
|
||||
detail=json.dumps(detail_payload, ensure_ascii=False),
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
return _to_read(updated)
|
||||
|
||||
@@ -648,7 +648,7 @@ async def delete_monitoring_visit_issue(
|
||||
action="DELETE_MONITORING_VISIT_ISSUE",
|
||||
detail=f"监查访视问题 {issue_no} 已删除",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
|
||||
|
||||
@@ -765,7 +765,7 @@ async def import_monitoring_visit_issues(
|
||||
action="IMPORT_MONITORING_VISIT_ISSUE",
|
||||
detail=f"导入监查访视问题:新增 {created_count} 条,更新 {updated_count} 条,跳过 {len(skipped_rows)} 条",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
|
||||
return MonitoringVisitIssueImportSummary(
|
||||
|
||||
@@ -14,7 +14,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy import func, select, desc
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.deps import get_current_user, get_db_session, list_active_pm_study_ids
|
||||
from app.core.deps import get_current_user, get_db_session, is_system_admin, list_active_pm_study_ids
|
||||
from app.core.permission_monitor import get_permission_monitor
|
||||
from app.models.permission_access_log import PermissionAccessLog
|
||||
from app.models.permission_metric_snapshot import PermissionMetricSnapshot
|
||||
@@ -36,14 +36,8 @@ class MonitoringScope:
|
||||
return study_id is not None and study_id in self.study_ids
|
||||
|
||||
|
||||
def _role_value(user) -> str:
|
||||
if not hasattr(user, "role"):
|
||||
return "ADMIN"
|
||||
return user.role.value if hasattr(user.role, "value") else str(user.role)
|
||||
|
||||
|
||||
async def resolve_monitoring_scope(db: AsyncSession, current_user) -> MonitoringScope:
|
||||
if _role_value(current_user) == "ADMIN":
|
||||
if is_system_admin(current_user):
|
||||
return MonitoringScope(is_admin=True, study_ids=set())
|
||||
return MonitoringScope(
|
||||
is_admin=False,
|
||||
|
||||
@@ -11,7 +11,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.deps import get_current_user, get_db_session, require_roles, require_study_roles
|
||||
from app.models.permission_template import TemplateType
|
||||
from app.models.user import UserRole
|
||||
from app.schemas.permission_template import (
|
||||
ApplyTemplateRequest,
|
||||
ApplyTemplateResponse,
|
||||
@@ -52,7 +51,7 @@ async def list_templates(
|
||||
async def create_template(
|
||||
payload: PermissionTemplateCreate,
|
||||
db: Annotated[AsyncSession, Depends(get_db_session)] = None,
|
||||
current_user=Depends(require_roles([UserRole.ADMIN.value])),
|
||||
current_user=Depends(require_roles(["ADMIN"])),
|
||||
) -> PermissionTemplateRead:
|
||||
"""创建权限模板"""
|
||||
try:
|
||||
@@ -79,7 +78,7 @@ async def update_template(
|
||||
template_id: uuid.UUID,
|
||||
payload: PermissionTemplateUpdate,
|
||||
db: Annotated[AsyncSession, Depends(get_db_session)] = None,
|
||||
current_user=Depends(require_roles([UserRole.ADMIN.value])),
|
||||
current_user=Depends(require_roles(["ADMIN"])),
|
||||
) -> PermissionTemplateRead:
|
||||
"""更新权限模板"""
|
||||
try:
|
||||
@@ -93,7 +92,7 @@ async def update_template(
|
||||
async def delete_template(
|
||||
template_id: uuid.UUID,
|
||||
db: Annotated[AsyncSession, Depends(get_db_session)] = None,
|
||||
current_user=Depends(require_roles([UserRole.ADMIN.value])),
|
||||
current_user=Depends(require_roles(["ADMIN"])),
|
||||
):
|
||||
"""删除权限模板"""
|
||||
try:
|
||||
|
||||
@@ -3,7 +3,7 @@ import uuid
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.deps import get_current_user, get_db_session, require_study_not_locked, require_api_permission
|
||||
from app.core.deps import get_operator_role_label, get_current_user, get_db_session, require_study_not_locked, require_api_permission
|
||||
from app.crud import audit as audit_crud
|
||||
from app.crud import precaution as precaution_crud
|
||||
from app.crud import site as site_crud
|
||||
@@ -51,7 +51,7 @@ async def create_precaution(
|
||||
action="CREATE_PRECAUTION",
|
||||
detail=f"注意事项 {precaution.title} 已创建",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
return PrecautionRead.model_validate(precaution)
|
||||
|
||||
@@ -117,7 +117,7 @@ async def update_precaution(
|
||||
action="UPDATE_PRECAUTION",
|
||||
detail=f"注意事项 {precaution_id} 已更新",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
return PrecautionRead.model_validate(precaution)
|
||||
|
||||
@@ -147,5 +147,5 @@ async def delete_precaution(
|
||||
action="DELETE_PRECAUTION",
|
||||
detail=f"注意事项 {precaution_id} 已删除",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@ import uuid
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.deps import get_current_user, get_db_session, require_study_not_locked, require_api_permission
|
||||
from app.core.deps import get_operator_role_label, get_current_user, get_db_session, require_study_not_locked, require_api_permission
|
||||
from app.crud import audit as audit_crud
|
||||
from app.crud import project_milestone as milestone_crud
|
||||
from app.crud import study as study_crud
|
||||
@@ -69,6 +69,6 @@ async def update_project_milestone(
|
||||
action="UPDATE_PROJECT_MILESTONE",
|
||||
detail=f"项目里程碑 {milestone_id} 已更新",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
return ProjectMilestoneRead.model_validate(item)
|
||||
|
||||
@@ -4,7 +4,7 @@ import json
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.deps import get_cra_site_scope, get_current_user, get_db_session, require_study_member, require_api_permission, require_study_not_locked
|
||||
from app.core.deps import get_operator_role_label, get_cra_site_scope, get_current_user, get_db_session, require_study_member, require_api_permission, require_study_not_locked
|
||||
from app.core.decorators import register_api_endpoint
|
||||
from app.crud import audit as audit_crud
|
||||
from app.crud import site as site_crud
|
||||
@@ -64,7 +64,7 @@ async def create_site(
|
||||
action="SITE_CREATED",
|
||||
detail=json.dumps({"targetName": site.name, "after": {"name": site.name, "is_active": site.is_active}}, ensure_ascii=False),
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
return site
|
||||
|
||||
@@ -152,7 +152,7 @@ async def update_site(
|
||||
action=action,
|
||||
detail=json.dumps({"targetName": updated.name, "before": before_data, "after": after_data}, ensure_ascii=False),
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
return updated
|
||||
|
||||
@@ -196,6 +196,6 @@ async def delete_site(
|
||||
action="SITE_DELETED",
|
||||
detail=json.dumps({"targetName": site.name, "before": before_data, "after": {"deleted": True}}, ensure_ascii=False),
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
return None
|
||||
|
||||
@@ -3,7 +3,7 @@ import uuid
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.deps import get_cra_site_scope, get_current_user, get_db_session, require_api_permission
|
||||
from app.core.deps import get_operator_role_label, get_cra_site_scope, get_current_user, get_db_session, require_api_permission
|
||||
from app.crud import audit as audit_crud
|
||||
from app.crud import site as site_crud
|
||||
from app.crud import startup as startup_crud
|
||||
@@ -75,7 +75,7 @@ async def create_feasibility(
|
||||
action="CREATE_STARTUP_FEASIBILITY",
|
||||
detail="立项记录已创建",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
return StartupFeasibilityRead.model_validate(record)
|
||||
|
||||
@@ -149,7 +149,7 @@ async def update_feasibility(
|
||||
action="UPDATE_STARTUP_FEASIBILITY",
|
||||
detail="立项记录已更新",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
return StartupFeasibilityRead.model_validate(record)
|
||||
|
||||
@@ -182,7 +182,7 @@ async def delete_feasibility(
|
||||
action="DELETE_STARTUP_FEASIBILITY",
|
||||
detail="立项记录已删除",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
|
||||
|
||||
@@ -212,7 +212,7 @@ async def create_ethics(
|
||||
action="CREATE_STARTUP_ETHICS",
|
||||
detail="伦理记录已创建",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
return StartupEthicsRead.model_validate(record)
|
||||
|
||||
@@ -286,7 +286,7 @@ async def update_ethics(
|
||||
action="UPDATE_STARTUP_ETHICS",
|
||||
detail="伦理记录已更新",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
return StartupEthicsRead.model_validate(record)
|
||||
|
||||
@@ -319,7 +319,7 @@ async def delete_ethics(
|
||||
action="DELETE_STARTUP_ETHICS",
|
||||
detail="伦理记录已删除",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
|
||||
|
||||
@@ -349,7 +349,7 @@ async def create_kickoff(
|
||||
action="CREATE_KICKOFF_MEETING",
|
||||
detail="启动会记录已创建",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
return KickoffMeetingRead.model_validate(meeting)
|
||||
|
||||
@@ -423,7 +423,7 @@ async def update_kickoff(
|
||||
action="UPDATE_KICKOFF_MEETING",
|
||||
detail="启动会记录已更新",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
return KickoffMeetingRead.model_validate(meeting)
|
||||
|
||||
@@ -454,7 +454,7 @@ async def create_training_authorization(
|
||||
action="CREATE_TRAINING_AUTH",
|
||||
detail="培训授权人员已创建",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
return TrainingAuthorizationRead.model_validate(record)
|
||||
|
||||
@@ -528,7 +528,7 @@ async def update_training_authorization(
|
||||
action="UPDATE_TRAINING_AUTH",
|
||||
detail="培训授权人员已更新",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
return TrainingAuthorizationRead.model_validate(record)
|
||||
|
||||
@@ -561,5 +561,5 @@ async def delete_training_authorization(
|
||||
action="DELETE_TRAINING_AUTH",
|
||||
detail="培训授权人员已删除",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
|
||||
@@ -10,6 +10,8 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.core.deps import (
|
||||
get_current_user,
|
||||
get_db_session,
|
||||
get_operator_role_label,
|
||||
is_system_admin,
|
||||
require_api_permission,
|
||||
require_roles,
|
||||
require_study_member,
|
||||
@@ -50,9 +52,6 @@ def _study_read_with_role(study, role_in_study: str | None = None) -> StudyRead:
|
||||
return data
|
||||
|
||||
|
||||
def _role_value(user) -> str:
|
||||
return user.role.value if hasattr(user.role, "value") else str(user.role)
|
||||
|
||||
def _raise_validation_error(errors: list[dict[str, str]]) -> None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
@@ -769,7 +768,7 @@ async def create_study(
|
||||
action="CREATE_SETUP_CONFIG",
|
||||
detail="初始化立项配置(创建项目时回填基础信息)",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
|
||||
return study
|
||||
@@ -782,7 +781,7 @@ async def list_studies(
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> PaginatedResponse[StudyRead]:
|
||||
if _role_value(current_user) == "ADMIN":
|
||||
if is_system_admin(current_user):
|
||||
studies = await study_crud.list_studies(db, skip=skip, limit=limit)
|
||||
total = await study_crud.list_studies(db, skip=0, limit=10_000_000)
|
||||
items = [_study_read_with_role(study) for study in studies]
|
||||
@@ -805,7 +804,7 @@ async def get_study(
|
||||
study = await study_crud.get(db, study_id)
|
||||
if not study:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="项目不存在")
|
||||
if _role_value(current_user) == "ADMIN":
|
||||
if is_system_admin(current_user):
|
||||
return _study_read_with_role(study)
|
||||
member = await member_crud.get_member(db, study_id, current_user.id)
|
||||
return _study_read_with_role(study, member.role_in_study if member else None)
|
||||
@@ -893,7 +892,7 @@ async def lock_study(
|
||||
action="LOCK_STUDY",
|
||||
detail=f"项目已锁定:{study.name} ({study.code})",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
|
||||
return locked_study
|
||||
@@ -925,7 +924,7 @@ async def unlock_study(
|
||||
action="UNLOCK_STUDY",
|
||||
detail=f"项目已解锁:{study.name} ({study.code})",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
|
||||
return unlocked_study
|
||||
@@ -964,7 +963,7 @@ async def get_study_setup_config(
|
||||
action="CREATE_SETUP_CONFIG",
|
||||
detail="初始化立项配置",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
|
||||
saved_by_name = None
|
||||
@@ -1047,7 +1046,7 @@ async def upsert_study_setup_config(
|
||||
action="UPDATE_SETUP_CONFIG",
|
||||
detail=_top_level_diff_summary(old_config, record.config),
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
|
||||
saved_by_name = current_user.full_name or current_user.username or current_user.email
|
||||
@@ -1159,7 +1158,7 @@ async def publish_study_setup_config(
|
||||
skipped_items=[{"site_id": item.site_id, "reason": item.reason} for item in projection.skipped_items],
|
||||
),
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
auto_commit=False,
|
||||
)
|
||||
published.config = study_setup_config_crud.empty_draft_payload()
|
||||
@@ -1259,7 +1258,7 @@ async def rollback_study_setup_config(
|
||||
action="ROLLBACK_SETUP_CONFIG",
|
||||
detail=f"立项配置已回滚并替换当前发布为 {target_label},草稿分支已切换到对应分支基线",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
operator_name = current_user.full_name or current_user.username or current_user.email
|
||||
published_by_name = None
|
||||
@@ -1318,7 +1317,7 @@ async def checkout_study_setup_config_branch_draft(
|
||||
action="CHECKOUT_SETUP_CONFIG_BRANCH_DRAFT",
|
||||
detail=f"立项配置草稿已切换到 {target_label} 对应分支",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
operator_name = current_user.full_name or current_user.username or current_user.email
|
||||
published_by_name = None
|
||||
@@ -1370,7 +1369,7 @@ async def clear_study_setup_config_draft(
|
||||
action="CLEAR_SETUP_CONFIG_DRAFT",
|
||||
detail="立项配置草稿已清空",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
operator_name = current_user.full_name or current_user.username or current_user.email
|
||||
published_by_name = None
|
||||
@@ -1424,7 +1423,7 @@ async def refill_study_setup_config_draft(
|
||||
action="REFILL_SETUP_CONFIG_DRAFT",
|
||||
detail="立项配置草稿已从当前发布版本一键回填",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
operator_name = current_user.full_name or current_user.username or current_user.email
|
||||
published_by_name = None
|
||||
@@ -1511,7 +1510,7 @@ async def merge_study_setup_config_to_main(
|
||||
action="MERGE_SETUP_CONFIG_TO_MAIN",
|
||||
detail=f"已将发布版本 {source_label} 合并到主分支并生成新主版本",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
auto_commit=False,
|
||||
)
|
||||
merged.config = study_setup_config_crud.empty_draft_payload()
|
||||
@@ -1578,5 +1577,5 @@ async def delete_study_setup_config_version(
|
||||
action="DELETE_SETUP_CONFIG_VERSION",
|
||||
detail=f"删除发布版本快照 v{target_version}",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@ import uuid
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.deps import get_current_user, get_db_session, require_study_not_locked, require_api_permission
|
||||
from app.core.deps import get_operator_role_label, get_current_user, get_db_session, require_study_not_locked, require_api_permission
|
||||
from app.crud import audit as audit_crud
|
||||
from app.crud import site as site_crud
|
||||
from app.crud import subject as subject_crud
|
||||
@@ -58,7 +58,7 @@ async def create_history(
|
||||
action="CREATE_SUBJECT_HISTORY",
|
||||
detail="病史记录已创建",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
return SubjectHistoryRead.model_validate(history)
|
||||
|
||||
@@ -133,7 +133,7 @@ async def update_history(
|
||||
action="UPDATE_SUBJECT_HISTORY",
|
||||
detail="病史记录已更新",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
return SubjectHistoryRead.model_validate(history)
|
||||
|
||||
@@ -166,5 +166,5 @@ async def delete_history(
|
||||
action="DELETE_SUBJECT_HISTORY",
|
||||
detail="病史记录已删除",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.core.deps import (
|
||||
get_current_user,
|
||||
get_db_session,
|
||||
get_operator_role_label,
|
||||
require_study_not_locked,
|
||||
require_api_permission,
|
||||
)
|
||||
@@ -120,7 +121,7 @@ async def create_subject_pd(
|
||||
action="CREATE_SUBJECT_PD",
|
||||
detail=f"PD记录 {item.pd_no} 已创建",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
return SubjectPdRead.model_validate(item)
|
||||
|
||||
@@ -167,7 +168,7 @@ async def update_subject_pd(
|
||||
action="UPDATE_SUBJECT_PD",
|
||||
detail=f"PD记录 {updated.pd_no} 已更新",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
return SubjectPdRead.model_validate(updated)
|
||||
|
||||
@@ -201,5 +202,5 @@ async def delete_subject_pd(
|
||||
action="DELETE_SUBJECT_PD",
|
||||
detail=f"PD记录 {item.pd_no} 已删除",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@ from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.deps import get_cra_site_scope, get_current_user, get_db_session, require_study_not_locked, require_api_permission
|
||||
from app.core.deps import get_operator_role_label, get_cra_site_scope, get_current_user, get_db_session, require_study_not_locked, require_api_permission
|
||||
from app.crud import audit as audit_crud
|
||||
from app.crud import site as site_crud
|
||||
from app.crud import subject as subject_crud
|
||||
@@ -58,7 +58,7 @@ async def create_subject(
|
||||
action="CREATE_SUBJECT",
|
||||
detail=f"参与者 {subject.subject_no} 已创建",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
return subject
|
||||
|
||||
@@ -188,7 +188,7 @@ async def update_subject(
|
||||
action="SUBJECT_STATUS_CHANGE" if detail else "UPDATE_SUBJECT",
|
||||
detail=detail or "参与者已更新",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
return updated
|
||||
|
||||
@@ -220,5 +220,5 @@ async def delete_subject(
|
||||
action="DELETE_SUBJECT",
|
||||
detail=f"参与者 {subject_id} 已删除",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@ import uuid
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.deps import get_db_session, require_roles
|
||||
from app.core.deps import get_db_session, is_system_admin, require_roles
|
||||
from app.schemas.common import PaginatedResponse
|
||||
from app.crud import user as user_crud
|
||||
from app.crud import member as member_crud
|
||||
@@ -59,7 +59,7 @@ async def update_user(
|
||||
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":
|
||||
if is_system_admin(db_user):
|
||||
requested_status = user_in.status
|
||||
if user_in.is_active is not None:
|
||||
requested_status = "ACTIVE" if user_in.is_active else "DISABLED"
|
||||
@@ -85,7 +85,7 @@ async def delete_user(
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="系统管理员账号不允许删除")
|
||||
if db_user.id == current_user.id:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="不允许删除自己")
|
||||
if db_user.role.value == "ADMIN" and db_user.status.value == "ACTIVE":
|
||||
if is_system_admin(db_user) 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="至少保留一个管理员账号")
|
||||
|
||||
@@ -3,7 +3,7 @@ import uuid
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.deps import get_current_user, get_db_session, require_study_not_locked, require_api_permission
|
||||
from app.core.deps import get_operator_role_label, get_current_user, get_db_session, require_study_not_locked, require_api_permission
|
||||
from app.crud import audit as audit_crud
|
||||
from app.crud import site as site_crud
|
||||
from app.crud import subject as subject_crud
|
||||
@@ -107,7 +107,7 @@ async def create_visit(
|
||||
action="CREATE_VISIT",
|
||||
detail=f"访视 {visit.visit_code} 已创建",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
return visit
|
||||
|
||||
@@ -160,7 +160,7 @@ async def create_early_termination(
|
||||
action="CREATE_EARLY_TERMINATION",
|
||||
detail=f"参与者 {subject.subject_no} 已提前终止",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
return visit
|
||||
|
||||
@@ -204,7 +204,7 @@ async def update_visit(
|
||||
action="VISIT_STATUS_CHANGE" if detail else "UPDATE_VISIT",
|
||||
detail=detail or "访视已更新",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
return updated
|
||||
|
||||
@@ -236,5 +236,5 @@ async def delete_visit(
|
||||
action="DELETE_VISIT",
|
||||
detail=f"访视 {visit_id} 已删除",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
operator_role=await get_operator_role_label(db, study_id, current_user),
|
||||
)
|
||||
|
||||
@@ -269,19 +269,6 @@ API_ENDPOINT_PERMISSIONS = {
|
||||
"description": "删除立项记录",
|
||||
"default_roles": ["PM"],
|
||||
},
|
||||
# 项目权限管理
|
||||
"permissions:read": {
|
||||
"module": "project_members",
|
||||
"action": "read",
|
||||
"description": "查询项目权限矩阵",
|
||||
"default_roles": ["PM"],
|
||||
},
|
||||
"permissions:update": {
|
||||
"module": "project_members",
|
||||
"action": "write",
|
||||
"description": "更新项目权限矩阵",
|
||||
"default_roles": ["PM"],
|
||||
},
|
||||
# 项目概览
|
||||
"project_overview:read": {
|
||||
"module": "project_overview",
|
||||
|
||||
+47
-17
@@ -12,6 +12,7 @@ from app.core.exceptions import AppException
|
||||
from app.core.security import decode_token, oauth2_scheme
|
||||
from app.crud import user as user_crud
|
||||
from app.crud import member as member_crud
|
||||
from app.core.api_permissions import SYSTEM_PERMISSIONS
|
||||
from app.core.project_permissions import role_has_api_permission, get_missing_prerequisites
|
||||
from app.db.session import SessionLocal
|
||||
from app.models.study_member import StudyMember
|
||||
@@ -53,12 +54,15 @@ async def get_current_user(
|
||||
return user
|
||||
|
||||
|
||||
def is_system_admin(user) -> bool:
|
||||
return bool(getattr(user, "is_admin", False))
|
||||
|
||||
|
||||
def require_roles(roles: Iterable[str]) -> Callable:
|
||||
roles_set = set(roles)
|
||||
|
||||
async def dependency(current_user=Depends(get_current_user)):
|
||||
current_role = current_user.role.value if hasattr(current_user.role, "value") else str(current_user.role)
|
||||
if current_role not in roles_set:
|
||||
if "ADMIN" not in roles_set or not is_system_admin(current_user):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="权限不足",
|
||||
@@ -68,10 +72,6 @@ def require_roles(roles: Iterable[str]) -> Callable:
|
||||
return dependency
|
||||
|
||||
|
||||
def _role_value(user) -> str:
|
||||
return user.role.value if hasattr(user.role, "value") else str(user.role)
|
||||
|
||||
|
||||
async def list_active_pm_study_ids(db: AsyncSession, user_id: uuid.UUID) -> set[uuid.UUID]:
|
||||
result = await db.execute(
|
||||
select(StudyMember.study_id).where(
|
||||
@@ -95,12 +95,21 @@ async def is_active_project_pm(db: AsyncSession, user_id: uuid.UUID, study_id: u
|
||||
return result.scalar_one_or_none() is not None
|
||||
|
||||
|
||||
async def get_operator_role_label(db: AsyncSession, study_id: uuid.UUID | None, current_user) -> str:
|
||||
if is_system_admin(current_user):
|
||||
return "ADMIN"
|
||||
if not study_id:
|
||||
return ""
|
||||
membership = await member_crud.get_member(db, study_id, current_user.id)
|
||||
return membership.role_in_study if membership and membership.is_active else ""
|
||||
|
||||
|
||||
def require_admin_or_any_project_pm() -> Callable:
|
||||
async def dependency(
|
||||
current_user=Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
):
|
||||
if _role_value(current_user) == "ADMIN":
|
||||
if is_system_admin(current_user):
|
||||
return current_user
|
||||
if await list_active_pm_study_ids(db, current_user.id):
|
||||
return current_user
|
||||
@@ -112,13 +121,38 @@ def require_admin_or_any_project_pm() -> Callable:
|
||||
return dependency
|
||||
|
||||
|
||||
def require_system_permission(permission_key: str) -> Callable:
|
||||
async def dependency(
|
||||
study_id: uuid.UUID,
|
||||
current_user=Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
):
|
||||
permission = SYSTEM_PERMISSIONS.get(permission_key)
|
||||
if not permission:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="权限不足",
|
||||
)
|
||||
|
||||
allowed_roles = set(permission.get("roles", []))
|
||||
if is_system_admin(current_user) and "ADMIN" in allowed_roles:
|
||||
return current_user
|
||||
if "PM" in allowed_roles and await is_active_project_pm(db, current_user.id, study_id):
|
||||
return current_user
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="权限不足",
|
||||
)
|
||||
|
||||
return dependency
|
||||
|
||||
|
||||
async def get_study_member(
|
||||
study_id: uuid.UUID,
|
||||
current_user=Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
):
|
||||
role_value = _role_value(current_user)
|
||||
if role_value == "ADMIN":
|
||||
if is_system_admin(current_user):
|
||||
return None
|
||||
return await member_crud.get_member(db, study_id, current_user.id)
|
||||
|
||||
@@ -129,8 +163,7 @@ def require_study_member():
|
||||
current_user=Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
):
|
||||
role_value = _role_value(current_user)
|
||||
if role_value == "ADMIN":
|
||||
if is_system_admin(current_user):
|
||||
return current_user
|
||||
membership = await member_crud.get_member(db, study_id, current_user.id)
|
||||
if not membership or not membership.is_active:
|
||||
@@ -152,8 +185,7 @@ def require_study_roles(roles: Iterable[str], *, allow_system_admin: bool = True
|
||||
current_user=Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
):
|
||||
role_value = _role_value(current_user)
|
||||
if allow_system_admin and role_value == "ADMIN":
|
||||
if allow_system_admin and is_system_admin(current_user):
|
||||
return current_user
|
||||
membership = await member_crud.get_member(db, study_id, current_user.id)
|
||||
if not membership or not membership.is_active or membership.role_in_study not in roles_set:
|
||||
@@ -183,8 +215,7 @@ def require_api_permission(endpoint_key: str, *, allow_system_admin: bool = True
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
):
|
||||
from app.core.permission_monitor import get_permission_monitor
|
||||
role_value = _role_value(current_user)
|
||||
if allow_system_admin and role_value == "ADMIN":
|
||||
if allow_system_admin and is_system_admin(current_user):
|
||||
_enqueue_permission_log(
|
||||
study_id, current_user.id, endpoint_key, "ADMIN", True, 0.0, request
|
||||
)
|
||||
@@ -275,8 +306,7 @@ async def get_cra_site_scope(
|
||||
) -> tuple[set[uuid.UUID], set[str]] | None:
|
||||
from app.crud import site as site_crud
|
||||
|
||||
role_value = current_user.role.value if hasattr(current_user.role, "value") else current_user.role
|
||||
if role_value == "ADMIN":
|
||||
if is_system_admin(current_user):
|
||||
return None
|
||||
membership = await member_crud.get_member(db, study_id, current_user.id)
|
||||
if not membership or not membership.is_active:
|
||||
|
||||
@@ -30,8 +30,7 @@ ROLE_ACTIONS: dict[str, set[str]] = {
|
||||
|
||||
|
||||
def is_allowed(action: str, user, membership) -> bool:
|
||||
role_value = user.role.value if hasattr(user.role, "value") else str(user.role)
|
||||
if role_value == "ADMIN":
|
||||
if getattr(user, "is_admin", False):
|
||||
return action in ROLE_ACTIONS["ADMIN"]
|
||||
member_role = getattr(membership, "role_in_study", None)
|
||||
if member_role:
|
||||
|
||||
@@ -17,7 +17,6 @@ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login", scheme_name=
|
||||
def create_access_token(
|
||||
*,
|
||||
user_id: str,
|
||||
role: str,
|
||||
expires_minutes: Optional[int] = None,
|
||||
session_start: Optional[datetime] = None,
|
||||
) -> str:
|
||||
@@ -26,7 +25,6 @@ def create_access_token(
|
||||
session_start_time = session_start or now
|
||||
to_encode: Dict[str, Any] = {
|
||||
"sub": user_id,
|
||||
"role": role,
|
||||
"exp": expire,
|
||||
"iat": int(now.timestamp()),
|
||||
"orig_iat": int(session_start_time.timestamp()),
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from typing import Sequence
|
||||
|
||||
@@ -16,7 +14,7 @@ from app.core.config import (
|
||||
)
|
||||
from app.core.security import hash_password
|
||||
from app.models.study_member import StudyMember
|
||||
from app.models.user import User, UserRole, UserStatus
|
||||
from app.models.user import User, UserStatus
|
||||
from app.schemas.user import UserCreate, UserRegisterRequest, UserUpdate
|
||||
|
||||
|
||||
@@ -46,7 +44,6 @@ async def create_user(
|
||||
email=user_in.email,
|
||||
password_hash=hash_password(user_in.password),
|
||||
full_name=user_in.full_name,
|
||||
role=UserRole.PV,
|
||||
clinical_department=user_in.clinical_department,
|
||||
status=status_value,
|
||||
)
|
||||
@@ -114,7 +111,7 @@ async def count_active_admins(db: AsyncSession) -> int:
|
||||
result = await db.execute(
|
||||
select(func.count())
|
||||
.select_from(User)
|
||||
.where(User.role == UserRole.ADMIN, User.status == UserStatus.ACTIVE)
|
||||
.where(User.is_admin.is_(True), User.status == UserStatus.ACTIVE)
|
||||
)
|
||||
return int(result.scalar_one() or 0)
|
||||
|
||||
@@ -138,7 +135,7 @@ async def ensure_admin_exists(db: AsyncSession, *, default_password: str = PROTE
|
||||
email=PROTECTED_ADMIN_EMAIL,
|
||||
password_hash=hash_password(default_password),
|
||||
full_name=PROTECTED_ADMIN_FULL_NAME,
|
||||
role=UserRole.ADMIN,
|
||||
is_admin=True,
|
||||
clinical_department=PROTECTED_ADMIN_CLINICAL_DEPARTMENT,
|
||||
status=UserStatus.ACTIVE,
|
||||
)
|
||||
|
||||
@@ -5,22 +5,13 @@ import enum
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, Enum, ForeignKey, String, func
|
||||
from sqlalchemy import Boolean, DateTime, Enum, ForeignKey, String, func
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.db.base_class import Base
|
||||
|
||||
|
||||
class UserRole(str, enum.Enum):
|
||||
ADMIN = "ADMIN"
|
||||
PM = "PM"
|
||||
CRA = "CRA"
|
||||
PV = "PV"
|
||||
QA = "QA"
|
||||
CTA = "CTA"
|
||||
|
||||
|
||||
class UserStatus(str, enum.Enum):
|
||||
PENDING = "PENDING"
|
||||
ACTIVE = "ACTIVE"
|
||||
@@ -35,8 +26,8 @@ class User(Base):
|
||||
email: Mapped[str] = mapped_column(String(255), unique=True, index=True, nullable=False)
|
||||
password_hash: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
full_name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
role: Mapped[UserRole] = mapped_column(Enum(UserRole, name="user_role"), nullable=False)
|
||||
clinical_department: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
is_admin: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default="false", default=False)
|
||||
status: Mapped[UserStatus] = mapped_column(
|
||||
Enum(UserStatus, name="user_status"),
|
||||
nullable=False,
|
||||
@@ -66,10 +57,6 @@ 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
|
||||
|
||||
@@ -5,7 +5,6 @@ from typing import Literal, Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator
|
||||
|
||||
UserRole = Literal["PM", "CRA", "PV", "QA", "CTA", "ADMIN"]
|
||||
UserStatus = Literal["PENDING", "ACTIVE", "REJECTED", "DISABLED"]
|
||||
|
||||
PASSWORD_REGEX = re.compile(r"^(?=.*[A-Za-z])(?=.*\d).{8,}$")
|
||||
@@ -103,5 +102,4 @@ class Token(BaseModel):
|
||||
|
||||
class TokenPayload(BaseModel):
|
||||
sub: uuid.UUID
|
||||
role: str
|
||||
exp: Optional[int] = None
|
||||
|
||||
@@ -14,6 +14,7 @@ from sqlalchemy import delete as sa_delete, or_, select, update as sa_update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.deps import get_cra_site_scope
|
||||
from app.core.deps import get_operator_role_label, is_system_admin
|
||||
from app.core.project_permissions import role_has_api_permission
|
||||
from app.crud import acknowledgement as acknowledgement_crud
|
||||
from app.crud import distribution as distribution_crud
|
||||
@@ -43,14 +44,11 @@ DOCUMENT_ACTION_PERMISSIONS = {
|
||||
"ack": "documents:read",
|
||||
"create_document": "documents:create",
|
||||
"create_version": "documents:update",
|
||||
"distribute": "documents:update",
|
||||
"delete_document": "documents:delete",
|
||||
}
|
||||
|
||||
|
||||
def _role_value(user) -> str:
|
||||
return user.role.value if hasattr(user.role, "value") else str(user.role)
|
||||
|
||||
|
||||
def _audit_detail(before: dict | None, after: dict | None) -> str:
|
||||
payload = {"before": before, "after": after}
|
||||
return json.dumps(payload, ensure_ascii=True)
|
||||
@@ -82,7 +80,7 @@ async def _ensure_study_access(db: AsyncSession, trial_id: uuid.UUID, current_us
|
||||
study = await study_crud.get(db, trial_id)
|
||||
if not study:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="项目不存在")
|
||||
if current_user.role == "ADMIN":
|
||||
if is_system_admin(current_user):
|
||||
return None
|
||||
membership = await member_crud.get_member(db, trial_id, current_user.id)
|
||||
if not membership or not membership.is_active:
|
||||
@@ -98,7 +96,7 @@ async def _ensure_study_member(db: AsyncSession, trial_id: uuid.UUID, current_us
|
||||
study = await study_crud.get(db, trial_id)
|
||||
if not study:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="项目不存在")
|
||||
if current_user.role == "ADMIN":
|
||||
if is_system_admin(current_user):
|
||||
return None
|
||||
membership = await member_crud.get_member(db, trial_id, current_user.id)
|
||||
if not membership or not membership.is_active:
|
||||
@@ -154,7 +152,7 @@ async def create_document(
|
||||
action="DOCUMENT_CREATED",
|
||||
detail=_audit_detail(None, _doc_snapshot(doc)),
|
||||
operator_id=current_user.id,
|
||||
operator_role=_role_value(current_user),
|
||||
operator_role=await get_operator_role_label(db, doc.trial_id, current_user),
|
||||
)
|
||||
)
|
||||
await db.commit()
|
||||
@@ -345,7 +343,7 @@ async def create_version(
|
||||
action="VERSION_CREATED",
|
||||
detail=_audit_detail(None, _version_snapshot(version)),
|
||||
operator_id=current_user.id,
|
||||
operator_role=_role_value(current_user),
|
||||
operator_role=await get_operator_role_label(db, doc.trial_id, current_user),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -429,7 +427,7 @@ async def delete_version(
|
||||
action="VERSION_DELETED",
|
||||
detail=_audit_detail(before, None),
|
||||
operator_id=current_user.id,
|
||||
operator_role=_role_value(current_user),
|
||||
operator_role=await get_operator_role_label(db, doc.trial_id, current_user),
|
||||
)
|
||||
)
|
||||
await db.commit()
|
||||
@@ -466,7 +464,7 @@ async def delete_document(
|
||||
action="DOCUMENT_ARCHIVED",
|
||||
detail=_audit_detail(before, _doc_snapshot(doc)),
|
||||
operator_id=current_user.id,
|
||||
operator_role=_role_value(current_user),
|
||||
operator_role=await get_operator_role_label(db, doc.trial_id, current_user),
|
||||
)
|
||||
)
|
||||
await db.commit()
|
||||
@@ -511,7 +509,7 @@ async def create_distributions(
|
||||
doc = await document_crud.get(db, version.document_id)
|
||||
if not doc:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="文档不存在")
|
||||
await _ensure_study_member(db, doc.trial_id, current_user)
|
||||
await _ensure_study_access(db, doc.trial_id, current_user, action="distribute")
|
||||
|
||||
if version.status != DocumentVersionStatus.EFFECTIVE:
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="只有生效版本可分发")
|
||||
@@ -536,7 +534,7 @@ async def create_distributions(
|
||||
action="DISTRIBUTION_CREATED",
|
||||
detail=_audit_detail(None, {"version_id": str(version.id), "target_id": target.target_id}),
|
||||
operator_id=current_user.id,
|
||||
operator_role=_role_value(current_user),
|
||||
operator_role=await get_operator_role_label(db, doc.trial_id, current_user),
|
||||
)
|
||||
)
|
||||
await db.commit()
|
||||
@@ -599,8 +597,7 @@ async def create_acknowledgement(
|
||||
if distribution.target_type == DistributionTargetType.USER and distribution.target_id != str(current_user.id):
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="不在分发范围内")
|
||||
if distribution.target_type == DistributionTargetType.ROLE:
|
||||
role_value = _role_value(current_user)
|
||||
if distribution.target_id not in (role_value, getattr(membership, "role_in_study", "")):
|
||||
if distribution.target_id not in ("ADMIN" if is_system_admin(current_user) else "", getattr(membership, "role_in_study", "")):
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="不在分发范围内")
|
||||
|
||||
if payload.ack_type != AcknowledgementType.RECEIVED:
|
||||
@@ -633,7 +630,7 @@ async def create_acknowledgement(
|
||||
action="ACK_CREATED",
|
||||
detail=_audit_detail(None, {"distribution_id": str(distribution.id), "ack_type": payload.ack_type}),
|
||||
operator_id=current_user.id,
|
||||
operator_role=_role_value(current_user),
|
||||
operator_role=await get_operator_role_label(db, doc.trial_id, current_user),
|
||||
)
|
||||
)
|
||||
await db.commit()
|
||||
@@ -693,9 +690,8 @@ async def list_distribution_notifications(
|
||||
skip: int = 0,
|
||||
limit: int = 20,
|
||||
) -> list[NotificationItem]:
|
||||
role_value = _role_value(current_user)
|
||||
membership = None
|
||||
if role_value != "ADMIN":
|
||||
if not is_system_admin(current_user):
|
||||
membership = await member_crud.get_member(db, study_id, current_user.id)
|
||||
role_in_study = membership.role_in_study if membership else None
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.crud import document as document_crud
|
||||
from app.crud import etmf as etmf_crud
|
||||
from app.core.deps import get_operator_role_label
|
||||
from app.models.document import Document, DocumentStatus
|
||||
from app.models.etmf import EtmfNode
|
||||
from app.models.audit_log import AuditLog
|
||||
@@ -17,12 +18,6 @@ from app.schemas.document import DocumentCreate, DocumentSummary
|
||||
from app.schemas.etmf import EtmfNodeCreate, EtmfNodeRead, EtmfNodeStatus, EtmfTreeNode, EtmfNodeUpdate
|
||||
|
||||
|
||||
def _role_value(user) -> str:
|
||||
if user is None:
|
||||
return "SYSTEM"
|
||||
return user.role.value if hasattr(user.role, "value") else str(user.role)
|
||||
|
||||
|
||||
def calculate_node_status(node: EtmfNode, documents: Iterable[Document]) -> EtmfNodeStatus:
|
||||
docs = list(documents)
|
||||
if not node.is_active:
|
||||
@@ -109,7 +104,7 @@ async def create_node(db: AsyncSession, payload: EtmfNodeCreate, current_user) -
|
||||
action="ETMF_NODE_CREATED",
|
||||
detail=f'{{"code":"{node.code}","name":"{node.name}"}}',
|
||||
operator_id=current_user.id,
|
||||
operator_role=_role_value(current_user),
|
||||
operator_role=await get_operator_role_label(db, payload.study_id, current_user),
|
||||
)
|
||||
)
|
||||
await db.commit()
|
||||
@@ -138,7 +133,7 @@ async def update_node(db: AsyncSession, node_id: uuid.UUID, payload: EtmfNodeUpd
|
||||
action="ETMF_NODE_UPDATED",
|
||||
detail="{}",
|
||||
operator_id=current_user.id,
|
||||
operator_role=_role_value(current_user),
|
||||
operator_role=await get_operator_role_label(db, node.study_id, current_user),
|
||||
)
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
Reference in New Issue
Block a user