diff --git a/apps/portal-admin/project.json b/apps/portal-admin/project.json index 37b54e3..4fd72a7 100644 --- a/apps/portal-admin/project.json +++ b/apps/portal-admin/project.json @@ -84,7 +84,8 @@ "continuous": true, "executor": "@angular/build:dev-server", "options": { - "port": 4300 + "port": 4300, + "proxyConfig": "apps/portal-admin/proxy.conf.js" }, "configurations": { "production": { diff --git a/apps/portal-admin/proxy.conf.js b/apps/portal-admin/proxy.conf.js new file mode 100644 index 0000000..220f7a6 --- /dev/null +++ b/apps/portal-admin/proxy.conf.js @@ -0,0 +1,21 @@ +// Angular dev-server proxy for portal-admin. +// +// Mirrors apps/portal-shell/proxy.conf.js — same rationale, same +// `/api → ${BFF_TARGET:-http://localhost:3000}` rule. The admin app +// talks to the same BFF (ADR-0020 §"Where does the admin app live"), +// just at admin-specific paths under `/api/admin/...`; the proxy +// match on `/api` covers both surfaces. +// +// JS form deliberate — only this form can read `process.env` so the +// Docker / native target swap (BFF_TARGET in dev.compose.yml) works +// without a rebuild. + +const target = process.env['BFF_TARGET'] ?? 'http://localhost:3000'; + +module.exports = { + '/api': { + target, + secure: false, + changeOrigin: true, + }, +}; diff --git a/apps/portal-admin/src/environments/environment.ts b/apps/portal-admin/src/environments/environment.ts index 904d22d..74029f3 100644 --- a/apps/portal-admin/src/environments/environment.ts +++ b/apps/portal-admin/src/environments/environment.ts @@ -13,13 +13,17 @@ */ export const environment = { /** - * Origin + prefix of the BFF HTTP API. Same value as portal-shell - * — both SPAs talk to the same BFF (per ADR-0020 §"Where does - * the admin app live"). The admin-specific routing happens via - * the `AUTH_PATH_PREFIX` token (`/admin/auth`) provided in + * Prefix of the BFF HTTP API. Same value as portal-shell — both + * SPAs talk to the same BFF (per ADR-0020 §"Where does the admin + * app live"). The admin-specific routing happens via the + * `AUTH_PATH_PREFIX` token (`/admin/auth`) provided in * `app.config.ts`, not by talking to a different host. + * + * Relative path: see portal-shell `environment.ts` for the full + * rationale. Both SPAs use `proxy.conf.js` to proxy `/api/*` to + * the BFF, keeping every call same-origin in the browser. */ - bffApiBaseUrl: 'http://localhost:3000/api', + bffApiBaseUrl: '/api', /** * Name of the BFF's CSRF cookie. v1 reuses `portal_csrf` diff --git a/apps/portal-shell/project.json b/apps/portal-shell/project.json index d368a58..a719e41 100644 --- a/apps/portal-shell/project.json +++ b/apps/portal-shell/project.json @@ -83,6 +83,9 @@ "serve": { "continuous": true, "executor": "@angular/build:dev-server", + "options": { + "proxyConfig": "apps/portal-shell/proxy.conf.js" + }, "configurations": { "production": { "buildTarget": "portal-shell:build:production" diff --git a/apps/portal-shell/proxy.conf.js b/apps/portal-shell/proxy.conf.js new file mode 100644 index 0000000..091e65e --- /dev/null +++ b/apps/portal-shell/proxy.conf.js @@ -0,0 +1,32 @@ +// Angular dev-server proxy for portal-shell. +// +// Lets the SPA call `/api/...` as a SAME-ORIGIN request — the dev +// server intercepts it and proxies to the BFF. Two wins: +// - the browser no longer pins the BFF to `localhost:3000`, so the +// SPA works when accessed from a different host (e.g. `http:// +// :4200/` in the ADR-0030 dockerised dev mode, where the +// browser may not be on the same machine as the BFF); +// - CORS is bypassed entirely in dev (same origin), so the BFF's +// `CORS_ALLOWED_ORIGINS` allowlist no longer has to enumerate the +// workstation/VM hostnames a developer might use. +// +// Target resolution: +// - native `nx serve` → defaults to http://localhost:3000 +// (the BFF on the same machine). +// - Compose `apps` profile → BFF_TARGET=http://portal-bff:3000 is +// set in dev.compose.yml so the proxy +// hits the BFF container by name. +// +// JS form (not JSON) is deliberate: it is the only Angular-supported +// proxy-config form that can read `process.env` at dev-server startup, +// which is what makes the Docker / native swap work without rebuilds. + +const target = process.env['BFF_TARGET'] ?? 'http://localhost:3000'; + +module.exports = { + '/api': { + target, + secure: false, + changeOrigin: true, + }, +}; diff --git a/apps/portal-shell/src/environments/environment.ts b/apps/portal-shell/src/environments/environment.ts index 20234c6..e5d1d44 100644 --- a/apps/portal-shell/src/environments/environment.ts +++ b/apps/portal-shell/src/environments/environment.ts @@ -18,12 +18,24 @@ */ export const environment = { /** - * Origin + prefix of the BFF HTTP API. The SPA prepends this to - * every backend call (`${bffApiBaseUrl}/health`, etc.) and derives - * the OTel trace-header propagation pattern from its origin (see - * `observability/tracing.ts`). + * Prefix of the BFF HTTP API. The SPA prepends this to every + * backend call (`${bffApiBaseUrl}/health`, etc.) and derives the + * OTel trace-header propagation pattern from its resolved origin + * (see `observability/tracing.ts`). + * + * Relative path: the Angular dev-server proxies `/api/*` to the + * BFF (see `proxy.conf.js`, BFF target overridable via the + * `BFF_TARGET` env var — set by the ADR-0030 `apps` Compose + * profile to `http://portal-bff:3000`). This keeps every BFF call + * same-origin in the browser, so the SPA works whether it is + * accessed via `localhost:4200`, the VM IP, or any other host — + * and the BFF's `CORS_ALLOWED_ORIGINS` no longer has to enumerate + * every developer-side hostname. Production siblings + * (`environment.prod.ts`, etc.) may set an absolute origin when + * the SPA and BFF live on different hosts; `tracing.ts` resolves + * either form against `window.location.origin`. */ - bffApiBaseUrl: 'http://localhost:3000/api', + bffApiBaseUrl: '/api', /** * Name of the BFF's CSRF cookie. Mirrors the BFF's diff --git a/apps/portal-shell/src/observability/tracing.ts b/apps/portal-shell/src/observability/tracing.ts index 8eacebe..36e1a55 100644 --- a/apps/portal-shell/src/observability/tracing.ts +++ b/apps/portal-shell/src/observability/tracing.ts @@ -55,7 +55,13 @@ const SERVICE_VERSION = 'dev'; // so a deploy-time change to `bffApiBaseUrl` automatically propagates // `traceparent` to the right origin. RegExp special chars are escaped // before going into the source. -const bffOrigin = new URL(environment.bffApiBaseUrl).origin; +// +// Resolved against `window.location.origin` so a relative +// `bffApiBaseUrl` (e.g. `/api` for the dev-server proxy in +// `proxy.conf.js`) yields the current origin; an absolute +// `bffApiBaseUrl` (e.g. cross-origin production) keeps its own origin +// (the second `URL` arg is ignored when the first is absolute). +const bffOrigin = new URL(environment.bffApiBaseUrl, window.location.origin).origin; const bffOriginRegex = new RegExp(`^${bffOrigin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/.*`); const provider = new WebTracerProvider({ diff --git a/infra/local/dev.compose.yml b/infra/local/dev.compose.yml index 37d53c6..dcb4cde 100644 --- a/infra/local/dev.compose.yml +++ b/infra/local/dev.compose.yml @@ -261,6 +261,11 @@ services: portal-shell: <<: *app-base container_name: apf-portal-shell-dev + environment: + # Read by apps/portal-shell/proxy.conf.js — points the dev-server + # /api proxy at the BFF container by name (Compose DNS). Native + # `nx serve` leaves BFF_TARGET unset and falls back to localhost. + BFF_TARGET: http://portal-bff:3000 command: ['pnpm', 'exec', 'nx', 'serve', 'portal-shell', '--host', '0.0.0.0', '--port', '4200'] ports: - '${SHELL_PORT:-4200}:4200' @@ -271,6 +276,9 @@ services: portal-admin: <<: *app-base container_name: apf-portal-admin-dev + environment: + # See portal-shell — same proxy target for the admin SPA. + BFF_TARGET: http://portal-bff:3000 command: ['pnpm', 'exec', 'nx', 'serve', 'portal-admin', '--host', '0.0.0.0', '--port', '4300'] ports: - '${ADMIN_PORT:-4300}:4300'