feat(provisioning): orchestrate Authentik/Stalwart/OCIS on tenant create

Phase 4 from docs/NEXT-STEPS.md. POST /tenants now writes Mongo AND drives
external service provisioning. A new POST /tenants/:slug/reconcile endpoint
retries the orchestration — useful when an upstream was down at create time
or external state drifted out of band.

Integration clients (services/provisioning/src/integrations/):
- AuthentikClient: real implementation. ensureGroup() is idempotent — looks
  up the group by name, creates if missing, returns either way. Group
  attributes record the tenant slug + Mongo id so we can trace back
- StalwartClient: stubbed. v0.16 removed the REST management API in favor
  of JMAP, which is significantly more work to wrap. TODO comment points
  to https://stalw.art/docs/api/management/overview for the follow-up
- OcisClient: stubbed. Needs libregraph /drives endpoint with service-to-
  service auth via OIDC client_credentials

Orchestration (provisioning.service.ts):
- Each step runs independently; one failure doesn't roll back the others
- Per-step state recorded on Tenant.provisioningStatus (ok/skipped/error/
  pending) plus error message on Tenant.provisioningErrors
- Steps return their own terminal state — 'skipped' for stubs, void
  defaults to 'ok' for real integrations
- Mongoose markModified() required for nested subdoc mutations to persist
- Tenant auto-flips status: pending → active when all steps are ok|skipped

Portal proxy routes (apps/portal/server/api/tenants/):
- POST /api/tenants and POST /api/tenants/:slug/reconcile forward the
  signed-in user's access token to the provisioning service. Lets the
  browser drive provisioning without minting tokens by hand. Will be
  replaced by a real "create workspace" flow with UI later

docker-compose: AUTHENTIK_API_URL/STALWART_API_URL/OCIS_API_URL now point
at the public Traefik-routed hostnames (with mkcert CA mounted into the
provisioning container so Node fetch trusts them). Previously these
pointed at internal Docker hostnames which doesn't work for Authentik
because of TLS issuer mismatch against the JWT.
This commit is contained in:
Ronni Baslund
2026-05-24 00:06:40 +02:00
parent 4bf6a85517
commit 28766b80c2
12 changed files with 357 additions and 6 deletions
@@ -401,12 +401,12 @@ services:
NODE_ENV: development
PORT: 3001
MONGODB_URI: mongodb://root:${MONGO_ROOT_PASSWORD}@mongo:27017/dezky?authSource=admin
AUTHENTIK_API_URL: http://authentik-server:9000/api/v3
AUTHENTIK_API_URL: https://auth.dezky.local/api/v3
AUTHENTIK_API_TOKEN: ${AUTHENTIK_BOOTSTRAP_TOKEN}
STALWART_API_URL: http://stalwart:8080
STALWART_API_URL: https://mail.dezky.local
STALWART_ADMIN_USER: admin
STALWART_ADMIN_PASSWORD: ${STALWART_ADMIN_PASSWORD}
OCIS_API_URL: http://ocis:9200
OCIS_API_URL: https://files.dezky.local
# JWT validation against Authentik for portal-issued access tokens
AUTHENTIK_ISSUER: https://auth.dezky.local/application/o/dezky-portal/
AUTHENTIK_AUDIENCE: dezky-portal