Initial commit: site monorepo with API, web, and infra.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
|
||||
from app.core.dependencies import require_superuser
|
||||
from app.modules.sync import service
|
||||
from app.modules.sync.schemas import (
|
||||
AckChangesRequest,
|
||||
AckChangesResponse,
|
||||
HeartbeatRequest,
|
||||
HeartbeatResponse,
|
||||
PairConfirmRequest,
|
||||
PairConfirmResponse,
|
||||
PairStartRequest,
|
||||
PullChangesRequest,
|
||||
PullChangesResponse,
|
||||
PushChangesRequest,
|
||||
PushChangesResponse,
|
||||
ResolveConflictRequest,
|
||||
SyncCapabilitiesResponse,
|
||||
)
|
||||
from app.modules.sync.service import SyncServiceError
|
||||
from app.modules.sync.tenant import (
|
||||
HubPrincipal,
|
||||
TenantContext,
|
||||
get_hub_auth,
|
||||
get_tenant_context,
|
||||
require_enterprise_zootech,
|
||||
)
|
||||
from app.modules.users.models import User
|
||||
from app.core.dependencies import get_current_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _map_error(exc: SyncServiceError) -> HTTPException:
|
||||
code = str(exc)
|
||||
status_code = status.HTTP_400_BAD_REQUEST
|
||||
if code in {"ENTERPRISE_ADMIN_ONLY", "ENTERPRISE_FORBIDDEN", "ENTERPRISE_ZOOTECH_ONLY"}:
|
||||
status_code = status.HTTP_403_FORBIDDEN
|
||||
if code in {"INVALID_PAIRING_CODE", "PAIRING_CODE_EXPIRED"}:
|
||||
status_code = status.HTTP_400_BAD_REQUEST
|
||||
if code == "CONFLICT_NOT_FOUND":
|
||||
status_code = status.HTTP_404_NOT_FOUND
|
||||
return HTTPException(status_code=status_code, detail=code)
|
||||
|
||||
|
||||
@router.post("/reports/push")
|
||||
def reports_push(body: PushChangesRequest, hub: HubPrincipal = Depends(get_hub_auth)) -> PushChangesResponse:
|
||||
"""Report domain ingest — same durability path as global push."""
|
||||
return service.push_changes(hub, body)
|
||||
|
||||
|
||||
@router.post("/reports/refresh")
|
||||
def reports_refresh(
|
||||
farm_hub_id: str = Query(...),
|
||||
enterprise_id: str = Query(...),
|
||||
tenant: TenantContext = Depends(require_enterprise_zootech),
|
||||
):
|
||||
if tenant.farm_hub_ids is not None and farm_hub_id not in tenant.farm_hub_ids:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="FARM_ACCESS_FORBIDDEN")
|
||||
return {"status": "queued", "farm_hub_id": farm_hub_id, "enterprise_id": tenant.enterprise_id}
|
||||
|
||||
|
||||
@router.get("/metrics")
|
||||
def sync_metrics(
|
||||
enterprise_id: str = Query(...),
|
||||
tenant: TenantContext = Depends(require_enterprise_zootech),
|
||||
):
|
||||
if tenant.enterprise_id != enterprise_id:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="ENTERPRISE_FORBIDDEN")
|
||||
return service.get_sync_metrics(enterprise_id)
|
||||
|
||||
|
||||
@router.get("/capabilities", response_model=SyncCapabilitiesResponse)
|
||||
def capabilities() -> SyncCapabilitiesResponse:
|
||||
return service.get_capabilities()
|
||||
|
||||
|
||||
@router.post("/changes/push", response_model=PushChangesResponse)
|
||||
def push_changes(body: PushChangesRequest, hub: HubPrincipal = Depends(get_hub_auth)) -> PushChangesResponse:
|
||||
return service.push_changes(hub, body)
|
||||
|
||||
|
||||
@router.post("/changes/pull", response_model=PullChangesResponse)
|
||||
def pull_changes(body: PullChangesRequest, hub: HubPrincipal = Depends(get_hub_auth)) -> PullChangesResponse:
|
||||
return service.pull_changes(hub, body)
|
||||
|
||||
|
||||
@router.post("/changes/ack", response_model=AckChangesResponse)
|
||||
def ack_changes(body: AckChangesRequest, hub: HubPrincipal = Depends(get_hub_auth)) -> AckChangesResponse:
|
||||
return service.ack_changes(hub, body)
|
||||
|
||||
|
||||
@router.post("/hubs/heartbeat", response_model=HeartbeatResponse)
|
||||
def heartbeat(body: HeartbeatRequest, hub: HubPrincipal = Depends(get_hub_auth)) -> HeartbeatResponse:
|
||||
return service.hub_heartbeat(hub, body)
|
||||
|
||||
|
||||
@router.get("/hub/conflicts")
|
||||
def list_hub_conflicts(hub: HubPrincipal = Depends(get_hub_auth)):
|
||||
return service.list_conflicts_for_enterprise(hub.enterprise_id)
|
||||
|
||||
|
||||
@router.get("/hub/conflicts/{conflict_id}")
|
||||
def get_hub_conflict(conflict_id: str, hub: HubPrincipal = Depends(get_hub_auth)):
|
||||
try:
|
||||
return service.get_conflict_detail(conflict_id, hub.enterprise_id)
|
||||
except SyncServiceError as exc:
|
||||
raise _map_error(exc) from exc
|
||||
|
||||
|
||||
@router.post("/hub/conflicts/{conflict_id}/resolve")
|
||||
def resolve_hub_conflict(
|
||||
conflict_id: str,
|
||||
body: ResolveConflictRequest,
|
||||
hub: HubPrincipal = Depends(get_hub_auth),
|
||||
):
|
||||
try:
|
||||
service.resolve_conflict(conflict_id, hub.enterprise_id, f"hub:{hub.hub_site_id}", body)
|
||||
return {"status": "ok"}
|
||||
except SyncServiceError as exc:
|
||||
raise _map_error(exc) from exc
|
||||
|
||||
|
||||
@router.get("/conflicts")
|
||||
def list_conflicts(
|
||||
enterprise_id: str = Query(...),
|
||||
tenant: TenantContext = Depends(require_enterprise_zootech),
|
||||
):
|
||||
if tenant.enterprise_id != enterprise_id and tenant.enterprise_role != "admin":
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="ENTERPRISE_FORBIDDEN")
|
||||
return service.list_conflicts_for_enterprise(enterprise_id)
|
||||
|
||||
|
||||
@router.get("/conflicts/{conflict_id}")
|
||||
def get_conflict(
|
||||
conflict_id: str,
|
||||
enterprise_id: str = Query(...),
|
||||
tenant: TenantContext = Depends(require_enterprise_zootech),
|
||||
):
|
||||
try:
|
||||
return service.get_conflict_detail(conflict_id, enterprise_id)
|
||||
except SyncServiceError as exc:
|
||||
raise _map_error(exc) from exc
|
||||
|
||||
|
||||
@router.post("/conflicts/{conflict_id}/resolve")
|
||||
def resolve_conflict(
|
||||
conflict_id: str,
|
||||
body: ResolveConflictRequest,
|
||||
enterprise_id: str = Query(...),
|
||||
tenant: TenantContext = Depends(require_enterprise_zootech),
|
||||
):
|
||||
try:
|
||||
service.resolve_conflict(conflict_id, enterprise_id, tenant.user_id or "", body)
|
||||
return {"status": "ok"}
|
||||
except SyncServiceError as exc:
|
||||
raise _map_error(exc) from exc
|
||||
|
||||
|
||||
enterprise_router = APIRouter()
|
||||
|
||||
|
||||
@enterprise_router.post("/pair/start")
|
||||
def pair_start(body: PairStartRequest, user: User = Depends(get_current_user)):
|
||||
try:
|
||||
return service.start_pairing(user, body)
|
||||
except SyncServiceError as exc:
|
||||
raise _map_error(exc) from exc
|
||||
|
||||
|
||||
@enterprise_router.post("/pair/confirm", response_model=PairConfirmResponse)
|
||||
def pair_confirm(body: PairConfirmRequest) -> PairConfirmResponse:
|
||||
try:
|
||||
return service.confirm_pairing(body)
|
||||
except SyncServiceError as exc:
|
||||
raise _map_error(exc) from exc
|
||||
Reference in New Issue
Block a user