清理全局角色残留并修复项目权限判断

This commit is contained in:
Cheng Zhou
2026-05-29 10:20:42 +08:00
parent 63457aab11
commit 44d69c2d7b
63 changed files with 739 additions and 443 deletions
+6 -6
View File
@@ -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="非法操作")
+4 -4
View File
@@ -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),
)
+5 -7
View File
@@ -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="仅系统管理员可修改项目负责人权限",
+9 -14
View File
@@ -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 -3
View File
@@ -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}
+1 -4
View File
@@ -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,
)
+8 -13
View File
@@ -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 -8
View File
@@ -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)
+4 -4
View File
@@ -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),
)
+5 -6
View File
@@ -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),
)
+9 -12
View File
@@ -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),
)
+13 -19
View File
@@ -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),
)
+3 -2
View File
@@ -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
+4 -4
View File
@@ -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),
)
+13 -10
View File
@@ -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(
+2 -8
View File
@@ -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,
+3 -4
View File
@@ -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:
+4 -4
View File
@@ -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),
)
+2 -2
View File
@@ -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 -4
View File
@@ -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
+12 -12
View File
@@ -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),
)
+16 -17
View File
@@ -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),
)
+4 -4
View File
@@ -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),
)
+4 -3
View File
@@ -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 -4
View File
@@ -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 -3
View File
@@ -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="至少保留一个管理员账号")
+5 -5
View File
@@ -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),
)
-13
View File
@@ -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
View File
@@ -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:
+1 -2
View File
@@ -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:
-2
View File
@@ -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()),
+3 -6
View File
@@ -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,
)
+2 -15
View File
@@ -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
-2
View File
@@ -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
+13 -17
View File
@@ -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
+3 -8
View File
@@ -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()