feat: initialize governed postbox module
This commit is contained in:
@@ -0,0 +1,732 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict
|
||||
from typing import Literal
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from govoplan_core.auth import ApiPrincipal, get_api_principal, has_scope
|
||||
from govoplan_core.core.postbox import (
|
||||
PostboxActorRef,
|
||||
PostboxAttachmentRef,
|
||||
PostboxDeliveryRequest,
|
||||
PostboxParticipantRef,
|
||||
PostboxTargetRef,
|
||||
)
|
||||
from govoplan_core.db.session import get_session
|
||||
from govoplan_postbox.backend.manifest import (
|
||||
ACKNOWLEDGE_SCOPE,
|
||||
BINDING_ADMIN_SCOPE,
|
||||
DELIVERY_SCOPE,
|
||||
READ_SCOPE,
|
||||
SEND_SCOPE,
|
||||
TEMPLATE_ADMIN_SCOPE,
|
||||
)
|
||||
from govoplan_postbox.backend.runtime import get_service
|
||||
from govoplan_postbox.backend.schemas import (
|
||||
PostboxAccessDecisionResponse,
|
||||
PostboxDeliveryCreateRequest,
|
||||
PostboxDeliveryResponse,
|
||||
PostboxDirectoryItem,
|
||||
PostboxDirectoryResponse,
|
||||
PostboxExactCreateRequest,
|
||||
PostboxGroupingItem,
|
||||
PostboxGroupingListResponse,
|
||||
PostboxGroupingPayload,
|
||||
PostboxMaterializeRequest,
|
||||
PostboxMessageItem,
|
||||
PostboxMessageListResponse,
|
||||
PostboxMessageStateRequest,
|
||||
PostboxOrganizationTargetsResponse,
|
||||
PostboxTemplateCreateRequest,
|
||||
PostboxTemplateItem,
|
||||
PostboxTemplateListResponse,
|
||||
PostboxTemplatePublishRequest,
|
||||
PostboxTemplateRevisionPayload,
|
||||
)
|
||||
from govoplan_postbox.backend.service import PostboxError
|
||||
|
||||
|
||||
router = APIRouter(prefix="/postbox", tags=["postbox"])
|
||||
|
||||
|
||||
def _require(principal: ApiPrincipal, scope: str) -> None:
|
||||
if not has_scope(principal, scope):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=f"Missing scope: {scope}",
|
||||
)
|
||||
|
||||
|
||||
def _require_any(principal: ApiPrincipal, *scopes: str) -> None:
|
||||
if any(has_scope(principal, scope) for scope in scopes):
|
||||
return
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=f"Requires one of: {', '.join(scopes)}",
|
||||
)
|
||||
|
||||
|
||||
def _actor(
|
||||
principal: ApiPrincipal,
|
||||
*,
|
||||
assignment_context_id: str | None = None,
|
||||
) -> PostboxActorRef:
|
||||
actions: set[str] = set()
|
||||
if has_scope(principal, READ_SCOPE):
|
||||
actions.update(("discover", "read"))
|
||||
if has_scope(principal, SEND_SCOPE):
|
||||
actions.add("send")
|
||||
if has_scope(principal, ACKNOWLEDGE_SCOPE):
|
||||
actions.add("acknowledge")
|
||||
if has_scope(principal, BINDING_ADMIN_SCOPE) or has_scope(
|
||||
principal,
|
||||
TEMPLATE_ADMIN_SCOPE,
|
||||
):
|
||||
actions.add("administer")
|
||||
selected = assignment_context_id
|
||||
if selected is None and len(principal.function_assignment_ids) == 1:
|
||||
selected = next(iter(principal.function_assignment_ids))
|
||||
return PostboxActorRef(
|
||||
account_id=principal.account_id,
|
||||
identity_id=principal.identity_id,
|
||||
selected_assignment_id=selected,
|
||||
acting_for_account_id=principal.acting_for_account_id,
|
||||
authorized_actions=frozenset(actions), # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
|
||||
def _http_error(exc: PostboxError) -> HTTPException:
|
||||
if exc.code.endswith("_not_found") or exc.code in {
|
||||
"message_not_found",
|
||||
"postbox_not_found",
|
||||
"template_not_found",
|
||||
"revision_not_found",
|
||||
"grouping_not_found",
|
||||
"target_not_found",
|
||||
}:
|
||||
code = status.HTTP_404_NOT_FOUND
|
||||
elif exc.code in {"access_denied", "grouping_source_denied"}:
|
||||
code = status.HTTP_403_FORBIDDEN
|
||||
elif exc.code in {
|
||||
"template_slug_exists",
|
||||
"address_collision",
|
||||
"idempotency_conflict",
|
||||
}:
|
||||
code = status.HTTP_409_CONFLICT
|
||||
else:
|
||||
code = status.HTTP_400_BAD_REQUEST
|
||||
return HTTPException(
|
||||
status_code=code,
|
||||
detail={"code": exc.code, "message": str(exc)},
|
||||
)
|
||||
|
||||
|
||||
def _directory_item(value) -> PostboxDirectoryItem:
|
||||
return PostboxDirectoryItem.model_validate(asdict(value))
|
||||
|
||||
|
||||
def _message_item(value) -> PostboxMessageItem:
|
||||
return PostboxMessageItem.model_validate(asdict(value))
|
||||
|
||||
|
||||
def _template_item(template) -> PostboxTemplateItem:
|
||||
return PostboxTemplateItem(
|
||||
id=template.id,
|
||||
tenant_id=template.tenant_id,
|
||||
slug=template.slug,
|
||||
name=template.name,
|
||||
description=template.description,
|
||||
status=template.status,
|
||||
current_revision=template.current_revision,
|
||||
published_revision_id=template.published_revision_id,
|
||||
revisions=[
|
||||
{
|
||||
"id": revision.id,
|
||||
"revision": revision.revision,
|
||||
"function_type_id": revision.function_type_id,
|
||||
"scope_kind": revision.scope_kind,
|
||||
"scope_id": revision.scope_id,
|
||||
"name_pattern": revision.name_pattern,
|
||||
"address_pattern": revision.address_pattern,
|
||||
"classification": revision.classification,
|
||||
"allow_vacant_delivery": revision.allow_vacant_delivery,
|
||||
"encryption_profile": revision.encryption_profile,
|
||||
"history_policy": dict(revision.history_policy or {}),
|
||||
"routing_policy": dict(revision.routing_policy or {}),
|
||||
"retention_policy": dict(revision.retention_policy or {}),
|
||||
"published_at": revision.published_at,
|
||||
"created_at": revision.created_at,
|
||||
}
|
||||
for revision in template.revisions
|
||||
],
|
||||
created_at=template.created_at,
|
||||
updated_at=template.updated_at,
|
||||
)
|
||||
|
||||
|
||||
def _grouping_item(grouping, *, visible_ids: set[str]) -> PostboxGroupingItem:
|
||||
return PostboxGroupingItem(
|
||||
id=grouping.id,
|
||||
name=grouping.name,
|
||||
is_default=grouping.is_default,
|
||||
postbox_ids=[
|
||||
source.postbox_id
|
||||
for source in grouping.sources
|
||||
if source.postbox_id in visible_ids
|
||||
],
|
||||
created_at=grouping.created_at,
|
||||
updated_at=grouping.updated_at,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/directory", response_model=PostboxDirectoryResponse)
|
||||
def api_postbox_directory(
|
||||
assignment_context_id: str | None = None,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> PostboxDirectoryResponse:
|
||||
_require(principal, READ_SCOPE)
|
||||
entries = get_service().list_visible_postboxes(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
actor=_actor(
|
||||
principal,
|
||||
assignment_context_id=assignment_context_id,
|
||||
),
|
||||
)
|
||||
return PostboxDirectoryResponse(
|
||||
postboxes=[_directory_item(entry) for entry in entries]
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/directory/{postbox_id}/access",
|
||||
response_model=PostboxAccessDecisionResponse,
|
||||
)
|
||||
def api_postbox_access(
|
||||
postbox_id: str,
|
||||
action: Literal[
|
||||
"discover",
|
||||
"read",
|
||||
"send",
|
||||
"acknowledge",
|
||||
"administer",
|
||||
] = "read",
|
||||
assignment_context_id: str | None = None,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> PostboxAccessDecisionResponse:
|
||||
_require(principal, READ_SCOPE)
|
||||
try:
|
||||
decision = get_service().explain_access(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
postbox_id=postbox_id,
|
||||
actor=_actor(
|
||||
principal,
|
||||
assignment_context_id=assignment_context_id,
|
||||
),
|
||||
action=action,
|
||||
)
|
||||
except PostboxError as exc:
|
||||
raise _http_error(exc) from exc
|
||||
return PostboxAccessDecisionResponse.model_validate(asdict(decision))
|
||||
|
||||
|
||||
@router.get("/messages", response_model=PostboxMessageListResponse)
|
||||
def api_list_postbox_messages(
|
||||
postbox_id: list[str] = Query(default=[]),
|
||||
assignment_context_id: str | None = None,
|
||||
q: str | None = Query(default=None, max_length=200),
|
||||
state_filter: Literal[
|
||||
"all",
|
||||
"unread",
|
||||
"read",
|
||||
"acknowledged",
|
||||
] = Query(default="all", alias="state"),
|
||||
limit: int = Query(default=100, ge=1, le=500),
|
||||
offset: int = Query(default=0, ge=0),
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> PostboxMessageListResponse:
|
||||
_require(principal, READ_SCOPE)
|
||||
actor = _actor(
|
||||
principal,
|
||||
assignment_context_id=assignment_context_id,
|
||||
)
|
||||
requested = tuple(postbox_id)
|
||||
if not requested:
|
||||
requested = tuple(
|
||||
entry.id
|
||||
for entry in get_service().list_visible_postboxes(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
actor=actor,
|
||||
)
|
||||
)
|
||||
messages = get_service().list_messages(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
postbox_ids=requested,
|
||||
actor=actor,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
query=q,
|
||||
state=state_filter,
|
||||
)
|
||||
total = get_service().count_messages(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
postbox_ids=requested,
|
||||
actor=actor,
|
||||
query=q,
|
||||
state=state_filter,
|
||||
)
|
||||
return PostboxMessageListResponse(
|
||||
messages=[_message_item(message) for message in messages],
|
||||
total=total,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/messages/{message_id}", response_model=PostboxMessageItem)
|
||||
def api_get_postbox_message(
|
||||
message_id: str,
|
||||
assignment_context_id: str | None = None,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> PostboxMessageItem:
|
||||
_require(principal, READ_SCOPE)
|
||||
try:
|
||||
message = get_service().get_message(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
message_id=message_id,
|
||||
actor=_actor(
|
||||
principal,
|
||||
assignment_context_id=assignment_context_id,
|
||||
),
|
||||
)
|
||||
except PostboxError as exc:
|
||||
raise _http_error(exc) from exc
|
||||
if message is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Postbox message not found.",
|
||||
)
|
||||
session.commit()
|
||||
return _message_item(message)
|
||||
|
||||
|
||||
@router.patch(
|
||||
"/messages/{message_id}/state",
|
||||
response_model=PostboxMessageItem,
|
||||
)
|
||||
def api_mark_postbox_message(
|
||||
message_id: str,
|
||||
payload: PostboxMessageStateRequest,
|
||||
assignment_context_id: str | None = None,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> PostboxMessageItem:
|
||||
_require(
|
||||
principal,
|
||||
ACKNOWLEDGE_SCOPE if payload.state == "acknowledged" else READ_SCOPE,
|
||||
)
|
||||
try:
|
||||
message = get_service().mark_message(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
message_id=message_id,
|
||||
actor=_actor(
|
||||
principal,
|
||||
assignment_context_id=assignment_context_id,
|
||||
),
|
||||
state=payload.state,
|
||||
)
|
||||
except PostboxError as exc:
|
||||
raise _http_error(exc) from exc
|
||||
session.commit()
|
||||
return _message_item(message)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/deliveries",
|
||||
response_model=PostboxDeliveryResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
def api_deliver_to_postbox(
|
||||
payload: PostboxDeliveryCreateRequest,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> PostboxDeliveryResponse:
|
||||
_require(principal, DELIVERY_SCOPE)
|
||||
request = PostboxDeliveryRequest(
|
||||
tenant_id=principal.tenant_id,
|
||||
target=PostboxTargetRef(**payload.target.model_dump()),
|
||||
producer_module=payload.producer_module,
|
||||
producer_resource_type=payload.producer_resource_type,
|
||||
producer_resource_id=payload.producer_resource_id,
|
||||
idempotency_key=payload.idempotency_key,
|
||||
subject=payload.subject,
|
||||
body_text=payload.body_text,
|
||||
sender_label=payload.sender_label,
|
||||
classification=payload.classification,
|
||||
participants=tuple(
|
||||
PostboxParticipantRef(**participant.model_dump())
|
||||
for participant in payload.participants
|
||||
),
|
||||
attachments=tuple(
|
||||
PostboxAttachmentRef(**attachment.model_dump())
|
||||
for attachment in payload.attachments
|
||||
),
|
||||
expires_at=payload.expires_at,
|
||||
metadata=payload.metadata,
|
||||
)
|
||||
try:
|
||||
result = get_service().deliver(session, request)
|
||||
except PostboxError as exc:
|
||||
session.rollback()
|
||||
raise _http_error(exc) from exc
|
||||
session.commit()
|
||||
return PostboxDeliveryResponse.model_validate(asdict(result))
|
||||
|
||||
|
||||
@router.get("/groupings", response_model=PostboxGroupingListResponse)
|
||||
def api_list_postbox_groupings(
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> PostboxGroupingListResponse:
|
||||
_require(principal, READ_SCOPE)
|
||||
actor = _actor(principal)
|
||||
visible_ids = {
|
||||
item.id
|
||||
for item in get_service().list_visible_postboxes(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
actor=actor,
|
||||
)
|
||||
}
|
||||
groupings = get_service().list_groupings(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
actor=actor,
|
||||
)
|
||||
return PostboxGroupingListResponse(
|
||||
groupings=[
|
||||
_grouping_item(grouping, visible_ids=visible_ids)
|
||||
for grouping in groupings
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/groupings",
|
||||
response_model=PostboxGroupingItem,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
def api_create_postbox_grouping(
|
||||
payload: PostboxGroupingPayload,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> PostboxGroupingItem:
|
||||
_require(principal, READ_SCOPE)
|
||||
actor = _actor(principal)
|
||||
try:
|
||||
grouping = get_service().save_grouping(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
actor=actor,
|
||||
grouping_id=None,
|
||||
**payload.model_dump(),
|
||||
)
|
||||
except PostboxError as exc:
|
||||
raise _http_error(exc) from exc
|
||||
session.commit()
|
||||
return _grouping_item(grouping, visible_ids=set(payload.postbox_ids))
|
||||
|
||||
|
||||
@router.put("/groupings/{grouping_id}", response_model=PostboxGroupingItem)
|
||||
def api_update_postbox_grouping(
|
||||
grouping_id: str,
|
||||
payload: PostboxGroupingPayload,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> PostboxGroupingItem:
|
||||
_require(principal, READ_SCOPE)
|
||||
actor = _actor(principal)
|
||||
try:
|
||||
grouping = get_service().save_grouping(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
actor=actor,
|
||||
grouping_id=grouping_id,
|
||||
**payload.model_dump(),
|
||||
)
|
||||
except PostboxError as exc:
|
||||
raise _http_error(exc) from exc
|
||||
session.commit()
|
||||
return _grouping_item(grouping, visible_ids=set(payload.postbox_ids))
|
||||
|
||||
|
||||
@router.delete("/groupings/{grouping_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def api_delete_postbox_grouping(
|
||||
grouping_id: str,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> None:
|
||||
_require(principal, READ_SCOPE)
|
||||
try:
|
||||
get_service().delete_grouping(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
actor=_actor(principal),
|
||||
grouping_id=grouping_id,
|
||||
)
|
||||
except PostboxError as exc:
|
||||
raise _http_error(exc) from exc
|
||||
session.commit()
|
||||
|
||||
|
||||
@router.get(
|
||||
"/admin/organization-targets",
|
||||
response_model=PostboxOrganizationTargetsResponse,
|
||||
)
|
||||
def api_postbox_organization_targets(
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> PostboxOrganizationTargetsResponse:
|
||||
_require_any(principal, BINDING_ADMIN_SCOPE, TEMPLATE_ADMIN_SCOPE)
|
||||
return PostboxOrganizationTargetsResponse(
|
||||
units=list(
|
||||
get_service().organization_targets(tenant_id=principal.tenant_id)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@router.get("/admin/postboxes", response_model=PostboxDirectoryResponse)
|
||||
def api_admin_postboxes(
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> PostboxDirectoryResponse:
|
||||
_require(principal, BINDING_ADMIN_SCOPE)
|
||||
return PostboxDirectoryResponse(
|
||||
postboxes=[
|
||||
_directory_item(item)
|
||||
for item in get_service().list_admin_postboxes(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/admin/postboxes",
|
||||
response_model=PostboxDirectoryItem,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
def api_create_exact_postbox(
|
||||
payload: PostboxExactCreateRequest,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> PostboxDirectoryItem:
|
||||
_require(principal, BINDING_ADMIN_SCOPE)
|
||||
try:
|
||||
postbox = get_service().create_exact_postbox(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
actor_id=principal.account_id,
|
||||
**payload.model_dump(),
|
||||
)
|
||||
except PostboxError as exc:
|
||||
raise _http_error(exc) from exc
|
||||
session.commit()
|
||||
return _directory_item(
|
||||
get_service().resolve_postbox(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
target=PostboxTargetRef(postbox_id=postbox.id),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/admin/postboxes/{postbox_id}",
|
||||
response_model=PostboxDirectoryItem,
|
||||
)
|
||||
def api_archive_postbox(
|
||||
postbox_id: str,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> PostboxDirectoryItem:
|
||||
_require(principal, BINDING_ADMIN_SCOPE)
|
||||
try:
|
||||
postbox = get_service().archive_postbox(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
postbox_id=postbox_id,
|
||||
actor_id=principal.account_id,
|
||||
)
|
||||
except PostboxError as exc:
|
||||
raise _http_error(exc) from exc
|
||||
session.commit()
|
||||
return _directory_item(
|
||||
get_service().resolve_postbox(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
target=PostboxTargetRef(postbox_id=postbox.id),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@router.get("/admin/templates", response_model=PostboxTemplateListResponse)
|
||||
def api_postbox_templates(
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> PostboxTemplateListResponse:
|
||||
_require(principal, TEMPLATE_ADMIN_SCOPE)
|
||||
return PostboxTemplateListResponse(
|
||||
templates=[
|
||||
_template_item(template)
|
||||
for template in get_service().list_templates(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/admin/templates",
|
||||
response_model=PostboxTemplateItem,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
def api_create_postbox_template(
|
||||
payload: PostboxTemplateCreateRequest,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> PostboxTemplateItem:
|
||||
_require(principal, TEMPLATE_ADMIN_SCOPE)
|
||||
try:
|
||||
template = get_service().create_template(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
actor_id=principal.account_id,
|
||||
**payload.model_dump(),
|
||||
)
|
||||
except PostboxError as exc:
|
||||
raise _http_error(exc) from exc
|
||||
session.commit()
|
||||
return _template_item(template)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/admin/templates/{template_id}/revisions",
|
||||
response_model=PostboxTemplateItem,
|
||||
)
|
||||
def api_revise_postbox_template(
|
||||
template_id: str,
|
||||
payload: PostboxTemplateRevisionPayload,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> PostboxTemplateItem:
|
||||
_require(principal, TEMPLATE_ADMIN_SCOPE)
|
||||
try:
|
||||
template = get_service().revise_template(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
template_id=template_id,
|
||||
actor_id=principal.account_id,
|
||||
**payload.model_dump(),
|
||||
)
|
||||
except PostboxError as exc:
|
||||
raise _http_error(exc) from exc
|
||||
session.commit()
|
||||
return _template_item(template)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/admin/templates/{template_id}/publish",
|
||||
response_model=PostboxTemplateItem,
|
||||
)
|
||||
def api_publish_postbox_template(
|
||||
template_id: str,
|
||||
payload: PostboxTemplatePublishRequest,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> PostboxTemplateItem:
|
||||
_require(principal, TEMPLATE_ADMIN_SCOPE)
|
||||
try:
|
||||
template = get_service().publish_template(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
template_id=template_id,
|
||||
revision_number=payload.revision,
|
||||
actor_id=principal.account_id,
|
||||
)
|
||||
except PostboxError as exc:
|
||||
raise _http_error(exc) from exc
|
||||
session.commit()
|
||||
return _template_item(template)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/admin/templates/{template_id}/retire",
|
||||
response_model=PostboxTemplateItem,
|
||||
)
|
||||
def api_retire_postbox_template(
|
||||
template_id: str,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> PostboxTemplateItem:
|
||||
_require(principal, TEMPLATE_ADMIN_SCOPE)
|
||||
try:
|
||||
template = get_service().retire_template(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
template_id=template_id,
|
||||
actor_id=principal.account_id,
|
||||
)
|
||||
except PostboxError as exc:
|
||||
raise _http_error(exc) from exc
|
||||
session.commit()
|
||||
return _template_item(template)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/admin/templates/{template_id}/materialize",
|
||||
response_model=PostboxDirectoryItem,
|
||||
)
|
||||
def api_materialize_postbox_template(
|
||||
template_id: str,
|
||||
payload: PostboxMaterializeRequest,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> PostboxDirectoryItem:
|
||||
_require(principal, BINDING_ADMIN_SCOPE)
|
||||
_require(principal, TEMPLATE_ADMIN_SCOPE)
|
||||
try:
|
||||
postbox = get_service().materialize_template(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
template_id=template_id,
|
||||
actor_id=principal.account_id,
|
||||
**payload.model_dump(),
|
||||
)
|
||||
except PostboxError as exc:
|
||||
raise _http_error(exc) from exc
|
||||
session.commit()
|
||||
entry = get_service().resolve_postbox(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
target=PostboxTargetRef(postbox_id=postbox.id),
|
||||
)
|
||||
if entry is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Materialized Postbox could not be reloaded.",
|
||||
)
|
||||
return _directory_item(entry)
|
||||
Reference in New Issue
Block a user