diff --git a/dashboard_api_rev.py b/dashboard_api_rev.py index c764219..8be9e5b 100644 --- a/dashboard_api_rev.py +++ b/dashboard_api_rev.py @@ -69,6 +69,12 @@ _ALLOWED_ORIGINS = [ # uvicorn workers (only one worker refreshes per tick); the work runs in a # thread so the async event loop never blocks on the ~9s REFRESH. _DATABASE_URL = os.environ["DATABASE_URL"] +# The request pool (get_conn / DATABASE_URL) can be a READ-ONLY role +# (dashboard_ro) — least privilege for serving. The v_trips refresher needs write +# perms (it owns the REFRESH), so it connects via a SEPARATE privileged URL: +# REFRESH_DATABASE_URL if set, else DATABASE_URL (single-role / legacy deploys). +# So a prod bridge runs DATABASE_URL=dashboard_ro + REFRESH_DATABASE_URL=. +_REFRESH_DB_URL = os.getenv("REFRESH_DATABASE_URL") or _DATABASE_URL # VTRIPS_REFRESH_INTERVAL_S <= 0 disables the in-process refresher entirely. # Staging sets it to 0: it connects read-only and prod owns the refresh, so a # staging instance must never attempt REFRESH (it would only log permission @@ -82,10 +88,11 @@ def _refresh_v_trips_once() -> str: Uses a dedicated autocommit connection: REFRESH ... CONCURRENTLY cannot run inside a transaction block (so the pooled get_conn, which wraps a txn, won't - do). DATABASE_URL connects as a superuser, which may REFRESH the matview - even though reporting_refresher owns it. + do). Connects via _REFRESH_DB_URL (REFRESH_DATABASE_URL or DATABASE_URL) — a + privileged role that may REFRESH the matview, distinct from a read-only + request pool. """ - conn = psycopg2.connect(_DATABASE_URL, connect_timeout=10) + conn = psycopg2.connect(_REFRESH_DB_URL, connect_timeout=10) try: conn.autocommit = True with conn.cursor() as cur: diff --git a/deploy_dashboard_api.sh b/deploy_dashboard_api.sh new file mode 100755 index 0000000..f815d0e --- /dev/null +++ b/deploy_dashboard_api.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# deploy_dashboard_api.sh — PROD dashboard_api bridge (fleetapi.rahamafresh.com). +# Standalone Traefik-labelled bridge (NOT Coolify-managed): reuses the +# webhook_receiver image + app network, bind-mounts the WIP API file. An env/CORS +# change needs a container RECREATE (this script does that). +# +# Stage-2 least privilege: the request pool connects as the READ-ONLY dashboard_ro +# role (DATABASE_URL), while the v_trips refresher keeps the privileged app role +# (REFRESH_DATABASE_URL) — REFRESH needs write perms that dashboard_ro lacks. +set -euo pipefail + +WH=$(docker ps --filter name=webhook_receiver --format "{{.Names}}" | head -1) +IMG=$(docker inspect "$WH" --format "{{.Image}}") +APPNET=$(docker inspect "$WH" --format '{{range $k,$v := .NetworkSettings.Networks}}{{$k}}{{end}}') +echo "Reusing image $IMG on network $APPNET (from $WH)" + +mkdir -p /home/kianiadee/dashboard_api +# Stage a fresh copy only if one was scp'd to ~; otherwise keep the existing mount. +if [ -f /home/kianiadee/dashboard_api_rev.py ]; then + mv -f /home/kianiadee/dashboard_api_rev.py /home/kianiadee/dashboard_api/dashboard_api_rev.py +fi +test -f /home/kianiadee/dashboard_api/dashboard_api_rev.py \ + || { echo "ERROR: dashboard_api_rev.py missing in mount dir; scp it to ~ first"; exit 1; } + +# Reuse the webhook container's env, stripping runtime noise AND any inherited +# DATABASE_URL + DASHBOARD_CORS_ORIGINS (both set explicitly below). +docker inspect "$WH" --format '{{range .Config.Env}}{{println .}}{{end}}' \ + | grep -vE '^(PATH=|HOSTNAME=|HOME=|PWD=|TERM=|SHLVL=|_=|LANG=|GPG_KEY=|PYTHON_VERSION=|PYTHON_PIP_VERSION=|PYTHONUNBUFFERED=|DATABASE_URL=|DASHBOARD_CORS_ORIGINS=)' \ + > /home/kianiadee/dashboard_api/dapi.env +echo 'DASHBOARD_CORS_ORIGINS=https://liveposition.rahamafresh.com,https://fleetintelligence.rahamafresh.com,https://fleetnow.rahamafresh.com,https://fleetops.rahamafresh.com' \ + >> /home/kianiadee/dashboard_api/dapi.env + +# Split roles: REFRESH_DATABASE_URL = the inherited privileged URL (for the +# refresher); DATABASE_URL = read-only dashboard_ro (for request handling). +SRC_DB_URL=$(docker inspect "$WH" --format '{{range .Config.Env}}{{println .}}{{end}}' | sed -n 's/^DATABASE_URL=//p' | head -1) +RO_PW=$(cat /home/kianiadee/.dashboard_ro.pw 2>/dev/null || true) +[ -n "$SRC_DB_URL" ] || { echo "ERROR: DATABASE_URL not found in $WH env"; exit 1; } +[ -n "$RO_PW" ] || { echo "ERROR: ~/.dashboard_ro.pw missing — run bootstrap_dashboard_ro.sh first"; exit 1; } +HOSTPART="${SRC_DB_URL#*@}" +{ + echo "REFRESH_DATABASE_URL=${SRC_DB_URL}" + echo "DATABASE_URL=postgresql://dashboard_ro:${RO_PW}@${HOSTPART}" +} >> /home/kianiadee/dashboard_api/dapi.env +chmod 600 /home/kianiadee/dashboard_api/dapi.env + +docker rm -f dashboard_api 2>/dev/null || true +docker run -d --name dashboard_api --restart unless-stopped \ + --network "$APPNET" \ + --env-file /home/kianiadee/dashboard_api/dapi.env \ + -v /home/kianiadee/dashboard_api/dashboard_api_rev.py:/app/dashboard_api_rev.py:ro \ + --label 'traefik.enable=true' \ + --label 'traefik.docker.network=coolify' \ + --label 'traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https' \ + --label 'traefik.http.routers.http-0-fleetapi.entryPoints=http' \ + --label 'traefik.http.routers.http-0-fleetapi.middlewares=redirect-to-https' \ + --label 'traefik.http.routers.http-0-fleetapi.rule=Host(`fleetapi.rahamafresh.com`)' \ + --label 'traefik.http.routers.https-0-fleetapi.entryPoints=https' \ + --label 'traefik.http.routers.https-0-fleetapi.rule=Host(`fleetapi.rahamafresh.com`)' \ + --label 'traefik.http.routers.https-0-fleetapi.tls=true' \ + --label 'traefik.http.routers.https-0-fleetapi.tls.certresolver=letsencrypt' \ + --label 'traefik.http.services.fleetapi.loadbalancer.server.port=8890' \ + "$IMG" sh -c 'uvicorn dashboard_api_rev:app --host 0.0.0.0 --port 8890 --workers 2' + +docker network connect coolify dashboard_api 2>/dev/null || true +sleep 8 +echo "== container =="; docker ps --filter name=dashboard_api --format "{{.Names}} | {{.Status}}" +echo "== CORS origins in effect =="; docker exec dashboard_api printenv DASHBOARD_CORS_ORIGINS +echo "== request role (expect dashboard_ro) =="; docker exec dashboard_api sh -lc 'printenv DATABASE_URL | sed -E "s#://([^:]+):[^@]+@#://\1:@#"' +echo "== refresh role set? =="; docker exec dashboard_api sh -lc 'printenv REFRESH_DATABASE_URL | sed -E "s#://([^:]+):[^@]+@#://\1:@#"' +echo "== internal health =="; docker exec dashboard_api sh -lc 'curl -s http://localhost:8890/health' 2>&1 | head || true