Files
apf_portal/apps/portal-bff/.env.example
T
Julien Gautier 7a313f627c
CI / commits (pull_request) Successful in 2m22s
CI / scan (pull_request) Successful in 2m33s
CI / check (pull_request) Successful in 4m3s
CI / a11y (pull_request) Successful in 1m50s
CI / perf (pull_request) Successful in 5m33s
feat(portal-bff): redis client foundation per ADR-0010
First step toward Redis-backed sessions. Adds the shared `ioredis`
connection that every downstream consumer (session storage, OBO
token cache, …) will inject via the new `REDIS_CLIENT` DI token.

What lands:

- `ioredis@^5.10.1` as a direct dependency. Chosen by ADR-0010 for
  its mature Sentinel support — single-instance URL today,
  Sentinel-HA configuration plumbing lands with the production
  infrastructure ADR.
- `.env.example` promotes `REDIS_URL` from its previous future-vars
  comment block into an active variable, with a default that
  matches `infra/local/.env` (REDIS_PASSWORD + REDIS_PORT). The
  Sentinel-style keys (`REDIS_SENTINEL_HOSTS`,
  `REDIS_SENTINEL_NAME`, `REDIS_TLS`) stay in the future-vars
  comment until the prod deploy lands.
- `apps/portal-bff/src/config/check-redis-config.ts` — boot-time
  guard mirroring the existing four:
  `assertDatabaseUrl` / `assertEntraConfig` /
  `assertSessionSecret`. Refuses to start if `REDIS_URL` is unset,
  not a valid `redis://` / `rediss://` URL, missing the password
  (the local stack requires one), or still set to the
  `redis_dev_change_me` .env.example placeholder. Returns a typed
  `RedisConfig` with parsed `host` + `port` for downstream
  observability.
- `apps/portal-bff/src/redis/redis.token.ts` — `REDIS_CLIENT`
  string token + `Redis` type alias. Same shape as
  `ENTRA_CONFIG` / `MSAL_CLIENT`.
- `apps/portal-bff/src/redis/redis.module.ts` — `RedisModule`
  exposes a factory provider for `REDIS_CLIENT`. The factory
  builds the `ioredis` client from the parsed config, caps
  `maxRetriesPerRequest` at 3 (so an unreachable Redis surfaces a
  command-time error instead of an infinite reconnect storm), and
  wires `connect` / `ready` / `error` / `close` / `reconnecting`
  events into the Pino stream with the `redis` Pino context.
  Non-global on purpose — modules import it to state "I depend on
  Redis".
- `main.ts` calls `assertRedisConfig()` alongside the other three
  validators. `AppModule` imports `RedisModule`.

Verification:

- `nx run-many -t lint test build --projects=portal-bff` — green.
- 62 / 62 specs (was 52; +10 across the config validator spec and
  the module spec — the latter exercises both the happy path
  against an unreachable URL — `ioredis` constructs lazily so no
  real socket opens — and the missing-env failure mode).
- Boot smoke (with the local Compose stack running): the `redis`
  Pino context shows `redis.connect` → `redis.ready` lines on
  startup; killing the Redis container later produces
  `redis.close` / `redis.reconnecting`.

What this PR explicitly does NOT do:

- Mount `express-session` + `connect-redis` middleware. The next
  PR wires the session cookie (`__Host-portal_session`) + the
  encrypted payload + the lookup middleware that attaches `user` to
  every request.
- Plug the callback into session creation. Auth still ends with a
  Pino log + redirect; the SPA still sees the user anonymous on the
  next request.
2026-05-12 16:36:29 +02:00

121 lines
6.1 KiB
Bash

# BFF environment template
# Copy to .env (which is gitignored) and fill in actual values for local development.
# Production values are managed by the platform's secret manager (see future infrastructure ADR).
# Postgres connection (per ADR-0006)
# Local dev default: dockerised Postgres on port 5432, schema 'public'.
# Username / password / db must match infra/local/.env (POSTGRES_USER /
# POSTGRES_PASSWORD / POSTGRES_DB) — those are the source of truth,
# this is the BFF view of the same connection.
#
# IMPORTANT — URL encoding. The password is part of the URL userinfo
# segment, so any of these characters must be URL-encoded:
# @ → %40 # → %23 : → %3A / → %2F ? → %3F
# % → %25 & → %26 = → %3D + → %2B ; → %3B
# i.e. if your POSTGRES_PASSWORD is "p@ss#1", DATABASE_URL must read
# "postgresql://portal:p%40ss%231@localhost:5432/portal_dev?schema=public"
# The BFF aborts at boot with a clear error if it detects an unencoded
# special character (see apps/portal-bff/src/config/check-database-url.ts).
DATABASE_URL="postgresql://portal:portal_dev_change_me@localhost:5432/portal_dev?schema=public"
# Observability (per ADR-0012)
# All OTEL_* keys are honoured by the OpenTelemetry SDK directly — see
# apps/portal-bff/src/observability/tracing.ts for the bootstrap.
# Pino log level: 'info' in prod, 'debug' in dev (default if unset).
LOG_LEVEL=debug
OTEL_SERVICE_NAME=portal-bff
OTEL_SERVICE_VERSION=dev
# Default endpoint targets the Collector provisioned in
# infra/local/dev.compose.yml. The /v1/traces suffix is required by
# the HTTP/Protobuf transport.
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318/v1/traces
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
# v1 samples 100 % at the app; tail sampling is delegated to the
# Collector (per ADR-0012). Override only for spike investigations.
OTEL_TRACES_SAMPLER=always_on
# Identity / Entra ID app registration (per ADR-0008 / ADR-0009)
# Values come from the project's Entra application registration in the
# Azure Admin Center → App registrations → APF Portal. The four
# *_INSTANCE_URL / *_TENANT_ID / *_CLIENT_ID / *_CLIENT_SECRET keys
# are mandatory; the BFF refuses to boot without them (see
# apps/portal-bff/src/config/check-entra-config.ts).
#
# ENTRA_INSTANCE_URL is the Microsoft login endpoint — usually
# https://login.microsoftonline.com/. The authority used by MSAL is
# `${ENTRA_INSTANCE_URL}${ENTRA_TENANT_ID}` for single-tenant flows,
# or `${ENTRA_INSTANCE_URL}organizations` / `common` for multi-tenant
# (per ADR-0008's dual-audience design). v1 uses the tenant-scoped
# authority; the multi-tenant switch lands when External ID activation
# is needed.
#
# ENTRA_CLIENT_SECRET is the high-value secret of this set. Never
# commit a real value. Production manages it via the deploy platform's
# secret manager (future infrastructure ADR).
ENTRA_INSTANCE_URL=https://login.microsoftonline.com/
ENTRA_TENANT_ID=00000000-0000-0000-0000-000000000000
ENTRA_CLIENT_ID=00000000-0000-0000-0000-000000000000
ENTRA_CLIENT_SECRET=replace_with_real_value
# Redirect URIs registered in Entra alongside the same client id. Both
# `/auth/callback` and `/auth/logout` paths are mounted by the BFF
# once the OIDC routes land in a subsequent PR.
ENTRA_REDIRECT_URI=http://localhost:3000/api/auth/callback
ENTRA_POST_LOGOUT_REDIRECT_URI=http://localhost:4200/
# Cookie signing secret (per ADR-0009 §"Cookies"). Used to sign the
# transient pre-auth cookie that carries the OIDC `state` + PKCE
# verifier between the /auth/login redirect and the /auth/callback
# round-trip, and (once ADR-0010 ships) the session cookie's
# integrity layer. Mandatory at boot — the BFF aborts if missing or
# obviously weak (less than 32 base64-decoded bytes ≈ 256 bits of
# entropy). Generate a fresh value per environment:
#
# node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))"
SESSION_SECRET=replace_with_32_random_bytes_base64url
# Redis connection (per ADR-0010). The BFF uses `ioredis` for session
# storage (today: just the connection; the express-session +
# connect-redis middleware lands in the next PR).
#
# REDIS_URL — full URL form including auth. Must match `infra/local/.env`
# (REDIS_PASSWORD + REDIS_PORT) when running against the local Compose
# stack. Production wiring uses Sentinel (REDIS_SENTINEL_HOSTS +
# REDIS_SENTINEL_NAME — future-vars block below) and TLS; the current
# variable supports the dev single-instance shape only.
REDIS_URL=redis://default:redis_dev_change_me@localhost:6379/0
# Future env vars introduced by upcoming phases / ADRs:
#
# Auth flow (ADR-0009) — additional keys wired as the routes land:
# ENTRA_CLIENT_CERT_PATH (alternative to ENTRA_CLIENT_SECRET)
# ENTRA_ACCEPTED_TENANT_IDS (CSV; restricts which tenants can sign in
# in the multi-tenant phase — empty means
# "only ENTRA_TENANT_ID is accepted")
#
# Sessions (ADR-0010) — additional keys wired as the layers land:
# REDIS_SENTINEL_HOSTS (CSV `host:port,host:port,…`; prod HA)
# REDIS_SENTINEL_NAME (master name in Sentinel; prod HA)
# REDIS_TLS ('true' in prod)
# SESSION_ENCRYPTION_KEY (32-byte base64)
# SESSION_IDLE_TIMEOUT_SECONDS (default 1800)
# SESSION_ABSOLUTE_TIMEOUT_SECONDS (default 43200)
#
# MFA (ADR-0011):
# MFA_FRESHNESS_SECONDS (default 600)
#
# Observability — additional keys to be wired as features land:
# LOG_USER_ID_SALT (per-environment salt for hashing user_id
# in CLS context — needed once auth lands)
#
# Audit trail (ADR-0013):
# AUDIT_DATABASE_URL (separate creds, role 'audit_writer')
# AUDIT_ARCHIVER_DATABASE_URL (role 'audit_archiver', for the retention purge job)
# AUDIT_RETENTION_DAYS (default 365)
#
# Downstream API access (ADR-0014):
# OBO_CACHE_ENCRYPTION_KEY (32-byte base64, distinct from SESSION_ENCRYPTION_KEY)
# BFF_JWKS_PRIVATE_KEY_PATH
# BFF_JWKS_KID
# <SERVICE>_API_BASE_URL (per integrated downstream)
# <SERVICE>_TIMEOUT_MS (optional, defaults to 5000)