POST /webhooks/postal (unauthenticated — RSA signature is the auth) verifies the X-Postal-Signature-256 header: RSA-SHA256/PKCS1v15 over the raw request body, using the same keypair as DKIM signing. Verified directly against Postal's own source (lib/postal/http.rb, signer.rb) rather than guessed, after the user pointed out the mechanism and that their instance is new enough to use the -256 (SHA256) header over the legacy SHA1 one. The public key is stored as the raw base64 DER blob from the domain's DKIM DNS TXT record (dig TXT postal-eWHeqb._domainkey.vinindkoeb.dk) — no PEM wrapping needed, cryptography.load_der_public_key takes it directly. Events are correlated to MailLog via postal_message_id. MessageSent (actual delivery confirmation, not to be confused with task 7b's synchronous "Postal accepted the request") maps to the DELIVERED status already reserved for it; MessageDeliveryFailed/MessageBounced/ MessageHeld/MessageDelayed map to new terminal/transient statuses. MessageLoaded/MessageLinkClicked set separate opened_at/clicked_at timestamps rather than overwriting status, since engagement can happen after delivery and shouldn't regress it. DomainDNSError and any unrecognized event are acknowledged (200) and ignored — no message to correlate. Discovered along the way: the native_enum=False enum columns are plain length-capped VARCHARs with no IN-list CHECK constraint, so adding "held"/"delayed" needed no constraint migration, just the two new opened_at/clicked_at columns Alembic did autogenerate correctly. Verified: signature logic in isolation against a self-generated RSA keypair (valid data/signature accepted, tampered data and garbage signatures rejected), the real DKIM key parses correctly (1024-bit RSA), the live endpoint rejects missing/invalid signatures with 401, and the event-to-MailLog mapping logic was exercised directly (not over HTTP, since a validly Postal-signed payload can't be forged without their private key) against an isolated throwaway sandbox — all cleaned up afterward, real route/participant data confirmed unaffected throughout. True end-to-end verification (a real Postal-originated webhook call) requires the app to be deployed somewhere Postal can reach, plus configuring the webhook URL in Postal's admin UI — both are deployment steps outside this coding task. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
30 lines
758 B
Python
30 lines
758 B
Python
from fastapi import FastAPI
|
|
from sqlalchemy import text
|
|
|
|
from app.db import SessionDep
|
|
from app.routers import (
|
|
auth,
|
|
mail_logs,
|
|
mail_templates,
|
|
participants,
|
|
purchase_rounds,
|
|
webhooks,
|
|
wine_categories,
|
|
wine_offerings,
|
|
)
|
|
|
|
app = FastAPI(title="Vinindkøb Admin API")
|
|
app.include_router(auth.router)
|
|
app.include_router(participants.router)
|
|
app.include_router(purchase_rounds.router)
|
|
app.include_router(wine_offerings.router)
|
|
app.include_router(wine_categories.router)
|
|
app.include_router(mail_templates.router)
|
|
app.include_router(mail_logs.router)
|
|
app.include_router(webhooks.router)
|
|
|
|
|
|
@app.get("/health")
|
|
def health(session: SessionDep) -> dict:
|
|
session.exec(text("SELECT 1"))
|
|
return {"status": "ok", "database": "ok"}
|