fix(api): hide campaign operational internals

This commit is contained in:
2026-07-21 12:03:56 +02:00
parent 04bc6c430e
commit ce92499333
9 changed files with 593 additions and 48 deletions

View File

@@ -38,6 +38,7 @@ from govoplan_campaign.backend.schemas import (
CampaignJobsResponse,
CampaignJobsDeltaResponse,
CampaignJobDetailResponse,
CampaignJobDiagnosticsResponse,
CampaignRetryJobsRequest,
CampaignSendJobRequest,
CampaignSendUnattemptedRequest,
@@ -98,6 +99,7 @@ from govoplan_campaign.backend.db.models import (
)
from govoplan_core.db.session import get_session
from govoplan_campaign.backend.reports.campaigns import CampaignReportError, generate_campaign_report, generate_jobs_csv
from govoplan_campaign.backend.response_security import public_campaign_payload
from govoplan_campaign.backend.reports.emailing import CampaignReportEmailError, send_campaign_report_email
from govoplan_campaign.backend.persistence.campaigns import (
CampaignPersistenceError,
@@ -1812,7 +1814,7 @@ def validate_version(
},
commit=True,
)
return result
return public_campaign_payload(result)
except HTTPException:
raise
except CampaignPersistenceError as exc:
@@ -1847,7 +1849,7 @@ def build_version(
details={"write_eml": payload.write_eml if payload else True, "built_count": result.get("built_count")},
commit=True,
)
return result
return public_campaign_payload(result)
except CampaignPersistenceError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
except Exception as exc:
@@ -1904,14 +1906,81 @@ def _job_detail_payload(job: CampaignJob) -> dict[str, object]:
return {
**_job_summary_payload(job),
"message_id_header": job.message_id_header,
"eml_local_path": job.eml_local_path,
"eml_storage_key": job.eml_storage_key,
"issues": job.issues_snapshot or [],
"attachments": job.resolved_attachments or [],
"attachments": public_campaign_payload(job.resolved_attachments or []),
"resolved_recipients": job.resolved_recipients or {},
}
def _job_attempts_payload(
send_attempts: list[SendAttempt],
imap_attempts: list[ImapAppendAttempt],
*,
include_diagnostics: bool = False,
) -> dict[str, list[dict[str, object]]]:
smtp_payloads: list[dict[str, object]] = []
for attempt in send_attempts:
payload: dict[str, object] = {
"id": attempt.id,
"attempt_number": attempt.attempt_number,
"status": attempt.status,
"smtp_status_code": attempt.smtp_status_code,
"smtp_response": attempt.smtp_response,
"error_type": attempt.error_type,
"error_message": attempt.error_message,
"started_at": attempt.started_at,
"finished_at": attempt.finished_at,
}
if include_diagnostics:
payload["claim_token"] = attempt.claim_token
smtp_payloads.append(payload)
imap_payloads: list[dict[str, object]] = []
for attempt in imap_attempts:
payload = {
"id": attempt.id,
"attempt_number": attempt.attempt_number,
"status": attempt.status,
"folder": attempt.folder,
"error_message": attempt.error_message,
"created_at": attempt.created_at,
"updated_at": attempt.updated_at,
}
if include_diagnostics:
payload["claim_token"] = attempt.claim_token
imap_payloads.append(payload)
return {"smtp": smtp_payloads, "imap": imap_payloads}
def _job_diagnostics_payload(
job: CampaignJob,
send_attempts: list[SendAttempt],
imap_attempts: list[ImapAppendAttempt],
) -> CampaignJobDiagnosticsResponse:
return CampaignJobDiagnosticsResponse(
job_id=job.id,
campaign_id=job.campaign_id,
campaign_version_id=job.campaign_version_id,
storage={
"eml_local_path": job.eml_local_path,
"eml_storage_key": job.eml_storage_key,
"eml_size_bytes": job.eml_size_bytes,
"eml_sha256": job.eml_sha256,
},
worker_claim={
"claim_token": job.claim_token,
"claimed_at": job.claimed_at,
"smtp_started_at": job.smtp_started_at,
"outcome_unknown_at": job.outcome_unknown_at,
},
attempts=_job_attempts_payload(
send_attempts,
imap_attempts,
include_diagnostics=True,
),
)
def _review_metadata(
session: Session,
version: CampaignVersion | None,
@@ -2462,38 +2531,42 @@ def get_job_detail(
)
return CampaignJobDetailResponse(
job=_job_detail_payload(job),
attempts={
"smtp": [
{
"id": attempt.id,
"attempt_number": attempt.attempt_number,
"status": attempt.status,
"claim_token": attempt.claim_token,
"smtp_status_code": attempt.smtp_status_code,
"smtp_response": attempt.smtp_response,
"error_type": attempt.error_type,
"error_message": attempt.error_message,
"started_at": attempt.started_at,
"finished_at": attempt.finished_at,
}
for attempt in send_attempts
],
"imap": [
{
"id": attempt.id,
"attempt_number": attempt.attempt_number,
"status": attempt.status,
"folder": attempt.folder,
"error_message": attempt.error_message,
"created_at": attempt.created_at,
"updated_at": attempt.updated_at,
}
for attempt in imap_attempts
],
},
attempts=_job_attempts_payload(send_attempts, imap_attempts),
)
@router.get(
"/{campaign_id}/jobs/{job_id}/diagnostics",
response_model=CampaignJobDiagnosticsResponse,
)
def get_job_diagnostics(
campaign_id: str,
job_id: str,
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(require_scope("campaigns:diagnostic:read")),
):
"""Return infrastructure details only to campaign operators/admins."""
_get_campaign_for_principal(session, campaign_id, principal)
campaign = _get_campaign_for_tenant(session, campaign_id, principal.tenant_id)
job = session.get(CampaignJob, job_id)
if not job or job.campaign_id != campaign.id or job.tenant_id != principal.tenant_id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Campaign job not found")
send_attempts = (
session.query(SendAttempt)
.filter(SendAttempt.job_id == job.id)
.order_by(SendAttempt.attempt_number.asc())
.all()
)
imap_attempts = (
session.query(ImapAppendAttempt)
.filter(ImapAppendAttempt.job_id == job.id)
.order_by(ImapAppendAttempt.attempt_number.asc())
.all()
)
return _job_diagnostics_payload(job, send_attempts, imap_attempts)
@router.get("/{campaign_id}/summary")
def campaign_summary(
campaign_id: str,