Skip to content

Runtime-configurable frontend API URL + env-driven CORS whitelist - #17

Merged
cfarrell987 merged 2 commits into
masterfrom
fix/avi-42-runtime-config-cors
Aug 25, 2026
Merged

cfarrell987 merged 2 commits into
masterfrom
fix/avi-42-runtime-config-cors

Conversation

@cfarrell987

Copy link
Copy Markdown
Owner

Fixes AVI-42. Live blocker.

Summary

Deploying to Portainer on a LAN IP (http://192.168.1.10:8180) failed. Confirmed via HAR from the actual browser session: the frontend called http://localhost:8000/api/... (response.status: 0 — the request never reached Django at all), because VITE_API_BASE_URL is baked into the JS bundle at build time. A single published crow987/avitrail-frontend:latest image can only ever point at one backend host, which doesn't work for more than one deployment.

Even with that fixed, the next wall: Django's CORS_ALLOWED_ORIGINS/CSRF_TRUSTED_ORIGINS in development.py was a hardcoded Python list (localhost:8000/8080/5173 only) — http://192.168.1.10:8180 would be rejected outright.

Frontend fix:

  • frontend/runtime-config-entrypoint.sh: new container entrypoint that generates runtime-config.js from $API_BASE_URL on every container start (not build) — chains into nginx's own official entrypoint rather than replacing it.
  • constants.js: precedence is now runtime env (window.__RUNTIME_CONFIG__) > build-time Vite env > hardcoded fallback.
  • nginx.conf: exact-match location for runtime-config.js with Cache-Control: no-cache, so a restarted container with a different $API_BASE_URL isn't served stale.
  • index.html: loads runtime-config.js before main.js; onerror fallback for local pnpm dev where the file doesn't exist.

Backend fix:

  • development.py: CORS_ALLOWED_ORIGINS/CSRF_TRUSTED_ORIGINS/ALLOWED_HOSTS now read from comma-separated env vars via a new _env_list() helper, defaulting to the exact same hardcoded values as before — no behavior change for existing local dev.
  • docker-compose.yml: both new env vars added with defaults matching current behavior, documented inline with what to change for a non-localhost deployment.

Test plan

  • Reproduced the exact reported failure: OPTIONS preflight with Origin: http://192.168.1.10:8180 against default config returns no access-control-* headers
  • Confirmed the fix: same request, with CORS_ALLOWED_ORIGINS overridden via a real container env var (matching how Portainer sets env vars, not shell-exported vars before docker compose up, which don't override literal compose values) — now returns the correct access-control-allow-origin header
  • Full round trip: an actual POST /api/auth/register/ with that Origin now returns 201 Created
  • Confirmed default/unset behavior is unchanged
  • Rebuilt and pushed both crow987/avitrail:latest and crow987/avitrail-frontend:latest with the fix — live on Docker Hub now, verified via docker manifest inspect
  • 66 backend tests, 100% coverage; eslint clean

🤖 Generated with Claude Code

cfarrell987 and others added 2 commits August 21, 2026 08:02
Fixes AVI-42.

Live blocker from a real Portainer deployment on a LAN IP
(http://192.168.1.10:8180). Confirmed via HAR: the frontend called
http://localhost:8000/api/... (response.status: 0 — never reached
Django), because VITE_API_BASE_URL is baked into the JS bundle at build
time. One published image can only ever point at one backend host.

- frontend/runtime-config-entrypoint.sh: new entrypoint that generates
  runtime-config.js from $API_BASE_URL on every container *start*, not
  build. Chains into nginx's own official entrypoint (renamed to avoid
  colliding with its conventional /docker-entrypoint.sh path) rather than
  replacing it.
- constants.js: precedence is now runtime env (window.__RUNTIME_CONFIG__)
  > build-time Vite env > hardcoded fallback.
- nginx.conf: exact-match location for runtime-config.js with
  Cache-Control: no-cache, so a restarted container with a different
  $API_BASE_URL isn't served stale.
- index.html: loads runtime-config.js before main.js; onerror fallback
  for local `pnpm dev` where the file doesn't exist.

Second half — even with the URL fixed, CORS_ALLOWED_ORIGINS/
CSRF_TRUSTED_ORIGINS in development.py was a hardcoded Python list
(localhost:8000/8080/5173 only), which would reject the request next:

- development.py: both now read from comma-separated env vars via a new
  _env_list() helper, defaulting to the exact same hardcoded values as
  before — no behavior change for existing local dev.
- docker-compose.yml: both new env vars added with defaults matching
  current behavior, documented inline with what to change for a
  non-localhost deployment.

## Test plan
- [x] Reproduced the exact reported failure: OPTIONS preflight with
      Origin: http://192.168.1.10:8180 against default config returns no
      access-control-* headers
- [x] Confirmed the fix: same request, with CORS_ALLOWED_ORIGINS
      overridden via a real container env var (matching how Portainer
      sets env vars, not shell-exported vars before `docker compose up`,
      which don't override literal compose values) — now returns the
      correct access-control-allow-origin header
- [x] Full round trip: an actual POST /api/auth/register/ with that
      Origin now returns 201 Created
- [x] Confirmed default/unset behavior is unchanged
- [x] Rebuilt and pushed both crow987/avitrail:latest and
      crow987/avitrail-frontend:latest with the fix — live on Docker Hub,
      verified via docker manifest inspect
- [x] 66 backend tests, 100% coverage; eslint clean
@cfarrell987
cfarrell987 merged commit 94e7f4f into master Aug 25, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant