From f24e1d33052311dbdfd037a3aa856f7edac17723 Mon Sep 17 00:00:00 2001 From: Ryan Westfall Date: Sat, 10 Oct 2026 06:28:09 -0500 Subject: [PATCH] Proxy Hesychia finance webhooks to chat_backend so Stripe POST is not 405. The static SPA nginx served index.html for /api/finance/webhooks/stripe/ and rejected the method. Closes #41. --- inventory/group_vars/all.yml | 9 ++++++ .../templates/docker-compose.yml.j2 | 3 ++ roles/web-static/templates/nginx.conf.j2 | 28 +++++++++++++++++++ 3 files changed, 40 insertions(+) diff --git a/inventory/group_vars/all.yml b/inventory/group_vars/all.yml index fa5db9b..4f8ebde 100644 --- a/inventory/group_vars/all.yml +++ b/inventory/group_vars/all.yml @@ -119,6 +119,10 @@ app_catalog: # Document root served by nginx and written by `npm run build:`. # {env} is replaced with the entry's env (beta/prod). webroot_pattern: "/var/www/{env}.chat.aimloperations/html" + # hesychia.ai Stripe/RevenueCat webhooks hit this vhost. Proxy the prefix + # to chat_backend so POST is not answered 405 by the SPA index.html. + api_backend: chat_backend + api_proxy_prefix: /api/finance/webhooks/ chat_backend: type: django repo: "{{ git_base_url }}/ai_ml_operations/chat_backend.git" @@ -126,6 +130,11 @@ app_catalog: compose_file: docker-compose.prod.yml web_service: web migrate_cmd: "uv run python manage.py migrate --noinput" + # Host header the static-site webhook proxy sends. Must be in Django + # ALLOWED_HOSTS (see chat_backend DJANGO_ALLOWED_HOSTS). + public_hosts: + prod: chatbackend.aimloperations.com + beta: beta.chatbackend.aimloperations.com monica_site: type: django repo: "{{ git_base_url }}/ai_ml_operations/monica_site.git" diff --git a/roles/web-static/templates/docker-compose.yml.j2 b/roles/web-static/templates/docker-compose.yml.j2 index bb3ad1d..2ff5abb 100644 --- a/roles/web-static/templates/docker-compose.yml.j2 +++ b/roles/web-static/templates/docker-compose.yml.j2 @@ -3,6 +3,9 @@ services: web-static: image: {{ web_static_image }} restart: unless-stopped + # Webhook locations proxy to chat_backend on the host-published port. + extra_hosts: + - "host.docker.internal:host-gateway" logging: driver: json-file options: diff --git a/roles/web-static/templates/nginx.conf.j2 b/roles/web-static/templates/nginx.conf.j2 index 686f33c..a340b0f 100644 --- a/roles/web-static/templates/nginx.conf.j2 +++ b/roles/web-static/templates/nginx.conf.j2 @@ -30,6 +30,34 @@ server { root {{ (app_catalog[a.name].webroot_pattern | default(web_static_root ~ '/{env}_' ~ a.name)) | replace('{env}', a.env) }}; index index.html; +{%- set api_backend_name = app_catalog[a.name].api_backend | default('') %} +{%- if api_backend_name %} +{%- set api_proxy_prefix = app_catalog[a.name].api_proxy_prefix | default('/api/') %} +{%- set api_match = namespace(port=0, ok=false) %} +{%- for item in host_apps | default([]) %} +{%- if item.name == api_backend_name and item.env == a.env and (item.enabled | default(true)) %} +{%- set api_match.port = item.port %} +{%- set api_match.ok = true %} +{%- endif %} +{%- endfor %} +{%- if api_match.ok %} +{%- set api_host = (app_catalog[api_backend_name].public_hosts | default({}))[a.env] | default('') %} + + # Finance webhooks (Stripe) arrive on the public product host. The SPA + # try_files fallback is a static file and returns 405 for POST. + location {{ api_proxy_prefix }} { + proxy_pass http://host.docker.internal:{{ api_match.port }}; + proxy_http_version 1.1; + proxy_set_header Host {{ api_host if api_host else '$host' }}; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 60s; + client_max_body_size 2m; + } +{%- endif %} +{%- endif %} + {% if app_catalog[a.name].error_page_404 | default('') %} error_page 404 {{ app_catalog[a.name].error_page_404 }};