发布候选:整合桌面端界面与发布稳定化里程碑
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
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:
@@ -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"))))
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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),
|
||||
|
||||
@@ -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"])
|
||||
|
||||
Reference in New Issue
Block a user