new-site/infra/nginx/sites-enabled/pw-api.conf
justin 14357a0223 fix(nginx): unblock public API routes powering lead tools/flows (HC sales killer)
api.performancewest.net uses an explicit per-path allowlist; everything else
falls through to a trusted-IP-only catch-all that returns 403. Six browser-
facing routes had no location block, so they 403'd for every public visitor:

  /api/v1/npi/                 <- THE healthcare sales killer. The 'Free NPI
                                  Compliance Check' tool (top of the HC funnel,
                                  where every HC campaign sends traffic) fetches
                                  /api/v1/npi/lookup. It 403'd -> CORS error in
                                  the browser -> the tool never rendered results
                                  or the upsell CTAs (Revalidation $399 / NPPES
                                  $149 / Bundle $899) -> 0 HC sales despite 17
                                  sessions reaching it in 30d and 0 HC orders
                                  EVER created in the compliance DB.
  /api/v1/cdr/                 telecom CDR profile tool
  /api/v1/icc/                 intrastate/ICC profile tool
  /api/v1/corp/                corporate foreign-qual check
  /api/v1/foreign-qualification/   foreign qualification quote/jurisdictions
  /api/v1/lnpa-regions         LNPA region lookup

Added explicit proxy_pass blocks (mirroring the existing entities/identity
pattern) before the catch-all. Verified live: all six now reach the app with
proper CORS; the NPI tool renders results + order CTAs end-to-end via a real
browser; npi-revalidation order page -> Stripe confirmed.

The live /etc/nginx/sites-enabled/pw-api.conf was hand-edited and untracked;
committing the current state here so it is version-controlled. (Live backup:
/root/pw-api.conf.bak_20260623.)
2026-06-23 15:51:30 -05:00

313 lines
11 KiB
Text

# HTTPS config for api.performancewest.net
# Restricted: only specific paths are publicly accessible.
# Admin/internal paths require trusted IP.
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
geo $api_trusted {
default 0;
127.0.0.1/32 1;
172.16.0.0/12 1;
10.0.0.0/8 1;
207.174.124.71/32 1;
76.228.206.147/32 1;
}
server {
listen 80;
server_name api.performancewest.net;
location /.well-known/acme-challenge/ { root /var/www/certbot; }
location / { return 301 https://api.performancewest.net$request_uri; }
}
server {
listen 443 ssl;
http2 on;
server_name api.performancewest.net;
ssl_certificate /etc/letsencrypt/live/api.performancewest.net/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.performancewest.net/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
client_max_body_size 50m;
# Shared proxy settings
set $api_backend http://127.0.0.1:3001;
# ── Webhooks: fully public (Stripe, PayPal, SHKeeper) ──
location ^~ /api/v1/webhooks/ {
limit_req zone=api_limit burst=10 nodelay;
proxy_pass $api_backend;
proxy_http_version 1.1;
proxy_set_header Host $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;
}
# ── Public API paths (browser frontend needs these) ──
location ^~ /api/v1/fcc/ {
limit_req zone=api_limit burst=20 nodelay;
proxy_pass $api_backend;
proxy_http_version 1.1;
proxy_set_header Host $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_connect_timeout 10s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
location ^~ /api/v1/id-upload/ {
limit_req zone=api_limit burst=10 nodelay;
proxy_pass $api_backend;
proxy_http_version 1.1;
proxy_set_header Host $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;
client_max_body_size 20M;
}
location ^~ /api/v1/dot/ {
limit_req zone=api_limit burst=20 nodelay;
proxy_pass $api_backend;
proxy_http_version 1.1;
proxy_set_header Host $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_connect_timeout 10s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
location ^~ /api/v1/discount/ {
limit_req zone=api_limit burst=10 nodelay;
proxy_pass $api_backend;
proxy_http_version 1.1;
proxy_set_header Host $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;
}
location ^~ /api/v1/auth/ {
limit_req zone=api_limit burst=10 nodelay;
proxy_pass $api_backend;
proxy_http_version 1.1;
proxy_set_header Host $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;
}
location ^~ /api/v1/subscribe {
limit_req zone=api_limit burst=5 nodelay;
proxy_pass $api_backend;
proxy_http_version 1.1;
proxy_set_header Host $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;
}
location ^~ /api/v1/tickets {
limit_req zone=api_limit burst=5 nodelay;
proxy_pass $api_backend;
proxy_http_version 1.1;
proxy_set_header Host $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;
}
location ^~ /api/v1/checkout/ {
limit_req zone=api_limit burst=10 nodelay;
proxy_pass $api_backend;
proxy_http_version 1.1;
proxy_set_header Host $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_send_timeout 60s;
proxy_read_timeout 60s;
}
location ^~ /api/v1/compliance-orders {
limit_req zone=api_limit burst=10 nodelay;
proxy_pass $api_backend;
proxy_http_version 1.1;
proxy_set_header Host $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_send_timeout 60s;
proxy_read_timeout 60s;
}
location ^~ /api/v1/portal {
limit_req zone=api_limit burst=10 nodelay;
proxy_pass $api_backend;
proxy_http_version 1.1;
proxy_set_header Host $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;
}
location ^~ /api/v1/canada-crtc {
limit_req zone=api_limit burst=10 nodelay;
proxy_pass $api_backend;
proxy_http_version 1.1;
proxy_set_header Host $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_send_timeout 60s;
proxy_read_timeout 60s;
}
location ^~ /api/v1/amb/ {
limit_req zone=api_limit burst=10 nodelay;
proxy_pass $api_backend;
proxy_http_version 1.1;
proxy_set_header Host $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;
}
location ^~ /api/v1/formations {
limit_req zone=api_limit burst=10 nodelay;
proxy_pass $api_backend;
proxy_http_version 1.1;
proxy_set_header Host $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_send_timeout 60s;
proxy_read_timeout 60s;
}
location ^~ /api/v1/paypal {
limit_req zone=api_limit burst=10 nodelay;
proxy_pass $api_backend;
proxy_http_version 1.1;
proxy_set_header Host $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;
}
location ^~ /api/v1/entities/ {
limit_req zone=api_limit burst=10 nodelay;
proxy_pass $api_backend;
proxy_http_version 1.1;
proxy_set_header Host $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;
}
location ^~ /api/v1/identity {
limit_req zone=api_limit burst=5 nodelay;
proxy_pass $api_backend;
proxy_http_version 1.1;
proxy_set_header Host $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;
}
# ── Public site routes (browser-facing tools/flows) ──
# Added: these power public lead tools + order flows and MUST be reachable by
# untrusted visitors. Without an explicit block they fell through to the
# trusted-IP-only catch-all and returned 403, silently breaking:
# npi -> Healthcare "Free NPI Compliance Check" (top of the HC funnel; the
# fetch failed CORS/403 so the tool never rendered results -> 0 HC sales)
# cdr/icc/corp/foreign-qualification/lnpa-regions -> telecom/corporate tools.
location ^~ /api/v1/npi/ {
limit_req zone=api_limit burst=20 nodelay;
proxy_pass $api_backend;
proxy_http_version 1.1;
proxy_set_header Host $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;
}
location ^~ /api/v1/cdr/ {
limit_req zone=api_limit burst=10 nodelay;
proxy_pass $api_backend;
proxy_http_version 1.1;
proxy_set_header Host $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;
}
location ^~ /api/v1/icc/ {
limit_req zone=api_limit burst=10 nodelay;
proxy_pass $api_backend;
proxy_http_version 1.1;
proxy_set_header Host $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;
}
location ^~ /api/v1/corp/ {
limit_req zone=api_limit burst=10 nodelay;
proxy_pass $api_backend;
proxy_http_version 1.1;
proxy_set_header Host $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;
}
location ^~ /api/v1/foreign-qualification/ {
limit_req zone=api_limit burst=10 nodelay;
proxy_pass $api_backend;
proxy_http_version 1.1;
proxy_set_header Host $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;
}
location ^~ /api/v1/lnpa-regions {
limit_req zone=api_limit burst=10 nodelay;
proxy_pass $api_backend;
proxy_http_version 1.1;
proxy_set_header Host $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;
}
# ── Everything else: trusted IPs only ──
location / {
if ($api_trusted = 0) {
return 403;
}
limit_req zone=api_limit burst=20 nodelay;
proxy_pass $api_backend;
proxy_http_version 1.1;
proxy_set_header Host $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_connect_timeout 10s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
location /.well-known/acme-challenge/ { root /var/www/certbot; }
}