Commit Graph

4 Commits

Author SHA1 Message Date
Ronni Baslund 4bf6a85517 fix(stalwart): wire recovery admin + point portal tile at admin UI
- docker-compose: add STALWART_RECOVERY_ADMIN env so the env-file password
  works as a permanent recovery login. Without this, Stalwart prints a
  one-time bootstrap password to the logs and discards it after first setup
- portal: mail tile now links to /admin/ (the real Stalwart admin SPA),
  not /login (which is the OAuth client authorization UI for IMAP/SMTP
  clients like Thunderbird — confusing and unrelated)

The persistent admin (admin@dezky.local) was created via Stalwart's setup
wizard at /admin/init and lives in the stalwart_data volume. Recovery admin
in env is the "I lost the wizard credentials" escape hatch.
2026-05-23 22:51:25 +02:00
Ronni Baslund e0808bf13e fix(ocis): wire OCIS web SSO + Collabora document editing end to end
OCIS SSO was loading the SPA but never redirecting to Authentik: the default
OCIS CSP only allows connect-src to itself + the awesome-ocis GitHub repo, so
the metadata fetch to auth.dezky.local was blocked. Mount a custom csp.yaml
and point PROXY_CSP_CONFIG_FILE_LOCATION at it (env var lives on the proxy
service, not web — easy mistake). Also added the .html OIDC callback URIs to
the ocis-provider in Authentik (run-time state, not in this commit).

Collabora document editing required adding the OCIS collaboration service —
the WOPI bridge between OCIS storage and Collabora. Key wiring:

- ocis: expose embedded NATS (NATS_NATS_HOST=0.0.0.0) and gateway
  (GATEWAY_GRPC_ADDR=0.0.0.0:9142) so the new container can register and
  reach the rest of OCIS over the Docker network
- collaboration: COLLABORATION_GRPC_ADDR=0.0.0.0:9301 so it registers itself
  in the service registry with a reachable address (default 127.0.0.1 was
  unreachable from cross-container callers)
- collaboration: APP_ADDR uses the public host (office.dezky.local), not
  the internal Docker hostname — this value is sent to the browser as the
  iframe src
- collabora: regenerate proof key on every start (coolconfig generate-proof-key)
  so its public key matches what coolwsd signs with; otherwise collaboration
  rejects WOPI calls with "ProofKeys verification failed"
- collabora: ssl_verification=false (mkcert root not in Collabora's trust
  store), frame_ancestors=files.dezky.local (otherwise the iframe is blocked
  with a Danish "Indhold blokeret"), home_mode.enable=true to drop the
  "Explore The New" welcome popup and feedback prompt
- ocis CSP: extend connect-src + frame-src to include the new hostnames

Result: opening a .docx from OCIS now embeds Collabora in an iframe and the
document opens for editing.

Dev-mode caveats (not for prod): TLS verification disabled on Collabora's
outbound WOPI calls; home_mode caps at 20 concurrent connections / 10 docs.
2026-05-23 22:36:42 +02:00
Ronni Baslund 3d370caa62 feat(provisioning): tenant data model + CRUD with JWT-validated authz
Implements Phase 3 from docs/NEXT-STEPS.md.

Mongoose schemas (services/provisioning/src/schemas/):
- Tenant: slug, name, status, plan, domains, billingInfo, plus handles for
  Authentik group, OCIS space, and Stalwart domain (set in Phase 4)
- User: authentikSubjectId, tenantIds[], email, name, role, platformAdmin flag
- Subscription: tenantId, plan, status, Stripe IDs (unused until Phase 4)

Auth (services/provisioning/src/auth/):
- JwtAuthGuard verifies Authentik access tokens against the provider's JWKS
  with issuer + audience checks. Uses NODE_EXTRA_CA_CERTS to trust the
  mkcert root for the local Authentik cert
- ActorService resolves the verified JWT into a Mongo User document — every
  controller reads tenantIds + platformAdmin from the DB, not the token
- CurrentUser decorator extracts the JWT payload onto controllers

CRUD modules:
- /tenants, /users, /subscriptions with create/read/update/delete
- /users/me upserts the caller's User record on every request, syncing email,
  name, tenantIds, and platformAdmin from the JWT's groups claim — the only
  place we read JWT.groups outside the bootstrap

Why DB-derived authz: putting all group memberships in the JWT doesn't scale
past ~50 tenants per user (header/cookie size limits, no mid-session
revocation, stale data until re-login). JWT now carries identity only; the
DB is the source of truth for who can see what.

Seed (SeedService.OnApplicationBootstrap): idempotent creation of the
default 'dezky' tenant + matching subscription. User records are created on
first /users/me hit.

Infrastructure:
- Traefik label exposes provisioning at https://api.dezky.local (dev only)
- api.dezky.local added to Docker network aliases on Traefik
- mkcert root CA mounted into the provisioning container for JWKS fetch
- Authentik 'groups' scope mapping created + attached to dezky-portal
  provider; portal now requests it as a scope
- nuxt.config.ts portal: exposeAccessToken=true so Nitro forwards token;
  NUXT_OIDC_TOKEN_KEY fixed to base64-encoded 32 bytes (was hex, causing
  "Invalid key length" once exposeAccessToken turned on)

Portal: apps/portal/server/api/me.get.ts is a scaffolding route that
forwards the user's access token to provisioning and returns profile +
tenants + subscriptions — verifies the full chain end to end.
2026-05-23 21:53:53 +02:00
Ronni Baslund adfd9baafe chore: initial scaffold with running local stack and portal auth
Brings up Dezky's local development environment end-to-end:

Infrastructure (docker-compose):
- Traefik v3.7 reverse proxy with mkcert TLS (v3.2 couldn't speak Docker API 1.54)
- Postgres + Mongo + Redis with healthchecks and init script for per-service users
- Authentik 2025.10 (server + worker) as OIDC IdP
- Stalwart v0.16 mail server (image renamed from stalwartlabs/mail-server)
- OCIS 7.0 with PROXY_TLS=false and OCIS_CONFIG_DIR=/etc/ocis so init writes
  where the server reads
- Collabora office, plus the portal + provisioning service stubs
- Docker network aliases on Traefik so containers resolve auth.dezky.local etc.
  through the network (not host /etc/hosts)
- Docker socket mount parameterized for macOS Docker Desktop symlink path

Authentik provisioning (done via API after stack boot):
- ocis-provider (public client) + OCIS Files application
- dezky-portal provider (confidential) + Dezky Portal application
- Admin API token bound to akadmin manually since 2025.10's
  AUTHENTIK_BOOTSTRAP_TOKEN env var doesn't auto-materialize a token row

Portal (apps/portal):
- Nuxt 3 with nuxt-oidc-auth 1.0.0-beta.11 against generic 'oidc' preset
- Global auth middleware; login at /auth/oidc/login redirects to Authentik
- Visual implementation of Claude Design 'Auth' canvas: AuthShell, NodeMark,
  Auth* sub-components, design tokens as CSS custom properties
- Pages: auth/login, auth/expired, auth/disabled, index (post-login landing)
- mkcert root CA mounted into the portal so Node fetch trusts Authentik's
  self-signed cert (NODE_EXTRA_CA_CERTS) — dev only

Docs:
- AUTHENTIK-SETUP.md updated with manual token bind + portal provider scripted
  alternative
- NEXT-STEPS.md: Phase 1 and Phase 2 marked done with file locations and
  dev-mode caveats

Dev-mode shortcuts that need to be revisited before prod:
- skipAccessTokenParsing on the OIDC config
- NODE_EXTRA_CA_CERTS mkcert mount
- Bootstrap password still the generated value in .env
- Authentik admin token (dezky-bootstrap-token) is non-expiring
2026-05-23 21:25:11 +02:00