diff --git a/apps/self-service/Dockerfile b/apps/self-service/Dockerfile new file mode 100644 index 0000000..6f72a08 --- /dev/null +++ b/apps/self-service/Dockerfile @@ -0,0 +1,23 @@ +# Multi-stage build for the self-service portal (Angular → nginx). +# Build context is the repo root (the app needs the pnpm workspace + libs). See infra/docker-compose.yml. +FROM node:24-slim AS build +WORKDIR /src +RUN corepack enable && corepack prepare pnpm@11.5.2 --activate + +# Restore first (cached unless the manifests change). +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml nx.json tsconfig.base.json eslint.config.mjs ./ +RUN pnpm install --frozen-lockfile + +# Sources (only what the app + its libs need). +COPY apps/self-service apps/self-service +COPY libs libs +RUN pnpm nx build self-service + +FROM nginx:1.27-alpine AS runtime +COPY apps/self-service/nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build /src/dist/apps/self-service/browser /usr/share/nginx/html +# Compose-time OIDC config: the browser (Playwright, on the compose network) reaches Keycloak by +# service name, so the token issuer matches the BFF's authority (host-consistent, ADR-0010). +RUN printf '{ "authority": "http://keycloak:8080/realms/digid" }\n' > /usr/share/nginx/html/config.json + +EXPOSE 80 diff --git a/apps/self-service/nginx.conf b/apps/self-service/nginx.conf new file mode 100644 index 0000000..d399eee --- /dev/null +++ b/apps/self-service/nginx.conf @@ -0,0 +1,29 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + # Resolve the BFF via Docker's embedded DNS at request time (variable proxy_pass), so nginx starts + # even before the BFF is up and picks up restarts — instead of failing to load the config. + resolver 127.0.0.11 ipv6=off valid=30s; + + # Same-origin API: proxy the BFF endpoint groups to the bff service. The api-client uses relative + # URLs, so the browser calls this origin and nginx forwards to the BFF — no CORS, and the DigiD + # token (same-origin) is attached by the app's interceptor (S-08d/ADR-0010). + location /self-service/ { + set $bff http://bff:8080; + proxy_pass $bff; + proxy_set_header Host $host; + } + location /openbaar/ { + set $bff http://bff:8080; + proxy_pass $bff; + proxy_set_header Host $host; + } + + # SPA fallback — Angular client-side routing. + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/apps/self-service/public/config.json b/apps/self-service/public/config.json new file mode 100644 index 0000000..7642cac --- /dev/null +++ b/apps/self-service/public/config.json @@ -0,0 +1,3 @@ +{ + "authority": "http://localhost:8180/realms/digid" +} diff --git a/apps/self-service/src/app/app.config.ts b/apps/self-service/src/app/app.config.ts index d7a7072..a4b7577 100644 --- a/apps/self-service/src/app/app.config.ts +++ b/apps/self-service/src/app/app.config.ts @@ -7,16 +7,29 @@ import { provideRouter } from '@angular/router'; import { authInterceptor, provideDigiadAuth } from 'auth'; import { appRoutes } from './app.routes'; -export const appConfig: ApplicationConfig = { - providers: [ - provideBrowserGlobalErrorListeners(), - provideRouter(appRoutes), - provideHttpClient(withInterceptors([authInterceptor()])), - // Dev defaults (host ports). The compose-served app overrides these for the stack (S-08d). - provideDigiadAuth({ - authority: 'http://localhost:8180/realms/digid', - redirectUrl: typeof window !== 'undefined' ? window.location.origin : '/', - secureApiOrigin: 'http://localhost:8080', - }), - ], -}; +/** Environment-specific settings fetched from /config.json at startup (see main.ts). */ +export interface RuntimeConfig { + /** The Keycloak `digid` realm issuer as the browser reaches it (dev: localhost; compose: keycloak:8080). */ + authority: string; +} + +/** + * Build the app providers from runtime config. `redirectUrl` and `secureApiOrigin` are the app's own + * origin: the app is served same-origin as the BFF (nginx proxies /self-service + /openbaar), so the + * api-client's relative calls stay same-origin and the token interceptor attaches to them. + */ +export function appConfig(runtime: RuntimeConfig): ApplicationConfig { + const origin = typeof window !== 'undefined' ? window.location.origin : '/'; + return { + providers: [ + provideBrowserGlobalErrorListeners(), + provideRouter(appRoutes), + provideHttpClient(withInterceptors([authInterceptor()])), + provideDigiadAuth({ + authority: runtime.authority, + redirectUrl: origin, + secureApiOrigin: origin, + }), + ], + }; +} diff --git a/apps/self-service/src/main.ts b/apps/self-service/src/main.ts index 190f341..29b0198 100644 --- a/apps/self-service/src/main.ts +++ b/apps/self-service/src/main.ts @@ -1,5 +1,10 @@ import { bootstrapApplication } from '@angular/platform-browser'; -import { appConfig } from './app/app.config'; import { App } from './app/app'; +import { appConfig, type RuntimeConfig } from './app/app.config'; -bootstrapApplication(App, appConfig).catch((err) => console.error(err)); +// Load environment config before bootstrap so the OIDC authority is set per environment +// (dev: localhost; compose: keycloak:8080) from a single build — 12-factor (S-08d). +fetch('config.json') + .then((response) => response.json() as Promise) + .then((config) => bootstrapApplication(App, appConfig(config))) + .catch((err) => console.error(err));