diff --git a/.env.example b/.env.example index 56ff850..b514cde 100644 --- a/.env.example +++ b/.env.example @@ -20,6 +20,13 @@ META_GRAPH_BASE=https://graph.facebook.com/v18.0 # Generate with: openssl rand -hex 32 WHATSAPP_WEBHOOK_SECRET= +# ── WhatsApp demo path (optional) ─────────────────────── +# Expected sender/recipient number (E.164) for the WhatsApp demo round trip +# (webhook -> ticket -> mock-log). Set the real number ONLY on the deploy +# host's .env — keep this template an empty placeholder, never a live number. +# Empty (default): the demo payload builder fails closed (no fabricated sender). +WHATSAPP_DEMO_TO= + # ── Login rate limiting (P0) ──────────────────────────── # ~5 failed login attempts per 15 minutes per IP+email → HTTP 429 LOGIN_RATE_LIMIT_MAX_ATTEMPTS=5 diff --git a/AGENTS.md b/AGENTS.md index 08dac52..270f375 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -73,6 +73,15 @@ read/write 500s. `ensure_legacy_schema` (app/main.py) adds the missing columns and backfills+drops the obsolete NOT NULL `command` column idempotently at startup — do not hand-edit legacy DBs, ship a self-heal there instead. +Demo WhatsApp round trip: `WHATSAPP_DEMO_TO` (E.164, .env-only — never commit +a real number; `.env.example` keeps an empty placeholder) is the expected +sender/recipient for the demo path. +`app/routers/whatsapp.py::build_demo_webhook_payload` builds a Meta webhook +payload from it (fails closed when unset), so posting it to +`POST /api/whatsapp/webhook` with the secret logs `from_number` = demo number +(visible via `GET /api/whatsapp/mock-log`) and the auto-reply targets the same +number. Covered by `tests/test_whatsapp_demo_number.py`. + ### Pages (Sprint 3) — Jinja2 templates at `app/templates/` | Method | Path | Auth | Description | |--------|------|------|-------------| @@ -108,6 +117,17 @@ Frontend: Alpine.js + Tailwind CSS vendored same-origin (no CDN) — see `test_csp_script_src_allows_unsafe_eval_for_alpine` in `tests/test_frontend_vendoring.py`. +### Branding (logo) +Official Denya Developers logo derivatives are committed under +`app/static/branding/` (Gitea release `logo-assets-v1`, sha256-verified +monochrome forest-green lockup; sourced from the Gitea release, never from +kagentz). Placement: login header uses `denya-logo-h96.png` (full lockup); the +logged-in topbar (base.html nav) uses `denya-logo-h48.png` on a light chip +(logo ink is only ~2:1 against the dark `#0d2b18` nav — keep a light chip +there); favicons 32x32+16x16 declared in `base.html
`. +`img-src 'self' data: blob:` already covers `/static/branding/*` — no CSP +change. Regression coverage: `tests/test_branding_assets.py`. + ### Tickets (Sprint 2) | Method | Path | Auth | Description | |--------|------|------|-------------| diff --git a/app/core/config.py b/app/core/config.py index 75b530d..bceb4c3 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -39,6 +39,14 @@ class Settings(BaseSettings): # Shared secret for inbound webhook POSTs (header ``X-Webhook-Secret``). # Fail-closed: when unset/empty the webhook rejects every message. WHATSAPP_WEBHOOK_SECRET: str = "" + # Expected sender/recipient number (E.164) for the WhatsApp demo round + # trip (webhook -> ticket -> mock-log). Set per deployment in .env only — + # never commit a real number. When set, the demo payload builder + # (``app/routers/whatsapp.py::build_demo_webhook_payload``) originates + # messages from it, the auto-reply targets it, and ``/api/whatsapp/mock-log`` + # surfaces it. Empty (default) means the demo payload cannot be built: + # the helper fails closed rather than fabricating a sender. + WHATSAPP_DEMO_TO: str = "" # ── Login rate limiting ────────────────────────────────────────── LOGIN_RATE_LIMIT_MAX_ATTEMPTS: int = 5 diff --git a/app/routers/whatsapp.py b/app/routers/whatsapp.py index b24d422..29d58ec 100644 --- a/app/routers/whatsapp.py +++ b/app/routers/whatsapp.py @@ -46,6 +46,47 @@ REPLY_TEMPLATE = ( ) +# ── WhatsApp demo round trip (WHATSAPP_DEMO_TO) ───────────────────── +def build_demo_webhook_payload( + text: str = "Demo message — Denya OneCare WhatsApp round trip", + wa_message_id: str = "wamid.demo.000001", +) -> dict: + """Build a Meta webhook payload for the WhatsApp demo round trip. + + The message ``from`` is ``settings.WHATSAPP_DEMO_TO`` (E.164), so the demo + surfaces the expected number end-to-end: the webhook logs it in + ``whatsapp_log`` (visible via ``GET /api/whatsapp/mock-log``) and the + auto-reply is sent back to the same number. The demo number is configured + per deployment in .env (never committed); when it is unset this raises + rather than fabricating a sender (same fail-closed posture as the webhook + secret). + """ + demo_to = (settings.WHATSAPP_DEMO_TO or "").strip() + if not demo_to: + raise RuntimeError( + "WHATSAPP_DEMO_TO is not configured — set the demo sender number " + "in .env to run the WhatsApp demo round trip." + ) + return { + "object": "whatsapp_business_account", + "entry": [ + { + "id": "1", + "changes": [ + { + "id": wa_message_id, + "message": { + "from": demo_to, + "id": wa_message_id, + "text": {"text": text}, + }, + } + ], + } + ], + } + + # ── Meta Graph API helpers ────────────────────────────────────────── async def send_whatsapp_reply( to_phone: str, diff --git a/app/static/branding/denya-logo-16x16.png b/app/static/branding/denya-logo-16x16.png new file mode 100644 index 0000000..44a4f64 Binary files /dev/null and b/app/static/branding/denya-logo-16x16.png differ diff --git a/app/static/branding/denya-logo-32x32.png b/app/static/branding/denya-logo-32x32.png new file mode 100644 index 0000000..251385c Binary files /dev/null and b/app/static/branding/denya-logo-32x32.png differ diff --git a/app/static/branding/denya-logo-64x64.png b/app/static/branding/denya-logo-64x64.png new file mode 100644 index 0000000..1ea60e9 Binary files /dev/null and b/app/static/branding/denya-logo-64x64.png differ diff --git a/app/static/branding/denya-logo-h48.png b/app/static/branding/denya-logo-h48.png new file mode 100644 index 0000000..b5526ca Binary files /dev/null and b/app/static/branding/denya-logo-h48.png differ diff --git a/app/static/branding/denya-logo-h96.png b/app/static/branding/denya-logo-h96.png new file mode 100644 index 0000000..dd4b544 Binary files /dev/null and b/app/static/branding/denya-logo-h96.png differ diff --git a/app/static/branding/denya-logo-trimmed.png b/app/static/branding/denya-logo-trimmed.png new file mode 100644 index 0000000..680deec Binary files /dev/null and b/app/static/branding/denya-logo-trimmed.png differ diff --git a/app/static/branding/denya-logo.png b/app/static/branding/denya-logo.png new file mode 100644 index 0000000..d4aacfd Binary files /dev/null and b/app/static/branding/denya-logo.png differ diff --git a/app/templates/base.html b/app/templates/base.html index 76e131a..7feac9c 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -4,6 +4,9 @@
Sign in to your dashboard