发布候选:整合桌面端界面与发布稳定化里程碑
Client Quality Gates / Shared client and Web (push) Has been cancelled
Client Quality Gates / macOS Desktop (push) Has been cancelled
Storage Persistence Guard / storage-persistence-audit (push) Has been cancelled
Client Quality Gates / Shared client and Web (pull_request) Has been cancelled
Client Quality Gates / macOS Desktop (pull_request) Has been cancelled
Storage Persistence Guard / storage-persistence-audit (pull_request) Has been cancelled

This commit is contained in:
Cheng Zhou
2026-07-01 10:53:24 +08:00
parent b283cf1e5c
commit b491b6a146
132 changed files with 17337 additions and 2375 deletions
+1 -4
View File
@@ -346,13 +346,10 @@ async def preview_attachment(
async def _authorize_global(request: Request, db: AsyncSession, study_id: uuid.UUID):
token = None
auth_header = request.headers.get("Authorization")
if auth_header and auth_header.lower().startswith("bearer "):
token = auth_header.split(" ", 1)[1]
if not token:
token = request.query_params.get("token")
if not token:
else:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="未登录")
payload = decode_token(token)
user = await user_crud.get_by_id(db, uuid.UUID(str(payload.get("sub"))))
+3 -1
View File
@@ -1,5 +1,5 @@
from datetime import datetime, timedelta, timezone
from fastapi import APIRouter, Depends, HTTPException, Query, status
from fastapi import APIRouter, Depends, HTTPException, Query, Response, status
from fastapi import File, UploadFile
from pydantic import BaseModel, EmailStr, Field
from sqlalchemy.ext.asyncio import AsyncSession
@@ -130,8 +130,10 @@ def ensure_user_active(db_user) -> None:
@router.get("/email-domains", response_model=EmailDomainsResponse)
async def read_email_domains(
response: Response,
db: AsyncSession = Depends(get_db_session),
) -> EmailDomainsResponse:
response.headers["Cache-Control"] = "no-store"
rows = await email_service.list_email_settings(db)
return EmailDomainsResponse(items=[row.register_domain for row in rows])
@@ -0,0 +1,86 @@
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
from app.schemas.notification import (
DesktopNotificationAckRequest,
DesktopNotificationClaimRequest,
DesktopNotificationClaimResponse,
DesktopNotificationSubscriptionRead,
DesktopNotificationSubscriptionUpdate,
)
from app.services import desktop_notification_service
router = APIRouter()
@router.get("/subscription", response_model=DesktopNotificationSubscriptionRead)
async def read_subscription(
db: AsyncSession = Depends(get_db_session),
current_user=Depends(get_current_user),
) -> DesktopNotificationSubscriptionRead:
subscription = await desktop_notification_service.get_subscription(db, current_user.id)
return DesktopNotificationSubscriptionRead(
enabled=bool(subscription and subscription.enabled),
enabled_at=subscription.enabled_at if subscription else None,
)
@router.put("/subscription", response_model=DesktopNotificationSubscriptionRead)
async def update_subscription(
payload: DesktopNotificationSubscriptionUpdate,
db: AsyncSession = Depends(get_db_session),
current_user=Depends(get_current_user),
) -> DesktopNotificationSubscriptionRead:
subscription = await desktop_notification_service.set_subscription(
db, current_user.id, payload.enabled
)
return DesktopNotificationSubscriptionRead(
enabled=subscription.enabled,
enabled_at=subscription.enabled_at,
)
@router.post("/claim", response_model=DesktopNotificationClaimResponse)
async def claim_notifications(
payload: DesktopNotificationClaimRequest,
db: AsyncSession = Depends(get_db_session),
current_user=Depends(get_current_user),
) -> DesktopNotificationClaimResponse:
if payload.limit < 1 or payload.limit > 50:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="limit 必须在 1 到 50 之间")
claim_token, lease_expires_at, items = await desktop_notification_service.claim_notifications(
db, current_user.id, payload.limit
)
return DesktopNotificationClaimResponse(
claim_token=claim_token,
lease_expires_at=lease_expires_at,
items=items,
)
@router.post("/ack", status_code=status.HTTP_204_NO_CONTENT)
async def acknowledge_notifications(
payload: DesktopNotificationAckRequest,
db: AsyncSession = Depends(get_db_session),
current_user=Depends(get_current_user),
) -> None:
await desktop_notification_service.acknowledge_notifications(
db,
current_user.id,
payload.claim_token,
payload.delivered_ids,
)
@router.post("/{distribution_id}/read", status_code=status.HTTP_204_NO_CONTENT)
async def mark_notification_read(
distribution_id: uuid.UUID,
db: AsyncSession = Depends(get_db_session),
current_user=Depends(get_current_user),
) -> None:
await desktop_notification_service.mark_notification_read(
db, current_user.id, distribution_id
)
+19 -1
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, is_system_admin
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 (
CACHE_HIT_RATE_HEALTH_MIN_ACCESSES,
CACHE_METRICS_WINDOW_SECONDS,
@@ -43,6 +43,11 @@ class MonitoringScope:
async def resolve_monitoring_scope(db: AsyncSession, current_user) -> MonitoringScope:
if is_system_admin(current_user):
return MonitoringScope(is_admin=True, study_ids=set())
user_id = getattr(current_user, "id", None)
if user_id:
study_ids = await list_active_pm_study_ids(db, user_id)
if study_ids:
return MonitoringScope(is_admin=False, study_ids=study_ids)
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足")
@@ -479,6 +484,8 @@ async def get_security_access_logs(
_=Depends(get_current_user),
status_min: Optional[int] = Query(None, ge=100, le=599),
auth_status: Optional[str] = Query(None),
client_type: Optional[str] = Query(None),
client_version: Optional[str] = Query(None),
page: int = Query(1, ge=1),
page_size: int = Query(50, ge=1, le=200),
) -> dict:
@@ -486,11 +493,17 @@ async def get_security_access_logs(
scope = await resolve_monitoring_scope(db, _)
if not scope.is_admin:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足")
client_type = client_type if isinstance(client_type, str) else None
client_version = client_version if isinstance(client_version, str) else None
conditions = []
if status_min is not None:
conditions.append(SecurityAccessLog.status_code >= status_min)
if auth_status:
conditions.append(SecurityAccessLog.auth_status == auth_status)
if client_type:
conditions.append(SecurityAccessLog.client_type == client_type)
if client_version:
conditions.append(SecurityAccessLog.client_version == client_version)
query = select(SecurityAccessLog)
count_query = select(func.count()).select_from(SecurityAccessLog)
@@ -545,6 +558,11 @@ async def get_security_access_logs(
"ip_city": ip_location.city,
"ip_isp": ip_location.isp,
"user_agent": log.user_agent,
"client_type": log.client_type,
"client_version": log.client_version,
"client_platform": log.client_platform,
"build_channel": log.build_channel,
"build_commit": log.build_commit,
"auth_status": log.auth_status,
"user_identifier": log.user_identifier,
"account_label": _security_account_label(log.auth_status, log.user_identifier, user_names),
+2 -1
View File
@@ -1,6 +1,6 @@
from fastapi import APIRouter
from app.api.v1 import auth, users, admin_email_settings, studies, sites, members, attachments, audit_logs, dashboard, subjects, visits, aes, finance_dashboard, fees_contracts, drug_shipments, material_equipments, project_milestones, startup, precautions, subject_histories, subject_pds, study_subject_pds, faq_categories, faqs, documents, etmf, overview, notifications, monitoring_visit_issues, api_permissions, permission_monitoring, permission_templates, system_permissions, study_active_roles
from app.api.v1 import auth, users, admin_email_settings, studies, sites, members, attachments, audit_logs, dashboard, subjects, visits, aes, finance_dashboard, fees_contracts, drug_shipments, material_equipments, project_milestones, startup, precautions, subject_histories, subject_pds, study_subject_pds, faq_categories, faqs, documents, etmf, overview, notifications, desktop_notifications, monitoring_visit_issues, api_permissions, permission_monitoring, permission_templates, system_permissions, study_active_roles
api_router = APIRouter()
@@ -10,6 +10,7 @@ api_router.include_router(users.router, prefix="/users", tags=["users"])
api_router.include_router(studies.router, prefix="/studies", tags=["studies"])
api_router.include_router(overview.router, prefix="/studies/{study_id}", tags=["overview"])
api_router.include_router(notifications.router, prefix="/studies/{study_id}", tags=["notifications"])
api_router.include_router(desktop_notifications.router, prefix="/desktop-notifications", tags=["desktop-notifications"])
api_router.include_router(sites.router, prefix="/studies/{study_id}/sites", tags=["sites"])
api_router.include_router(members.router, prefix="/studies/{study_id}/members", tags=["study-members"])
api_router.include_router(api_permissions.router, tags=["api-permissions"])