3d370caa62
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.
7.4 KiB
7.4 KiB
Next Steps — After Local Stack Is Running
Once ./scripts/bootstrap.sh completes successfully and all services are reachable, here's the development roadmap.
Phase 1: Verify everything works (day 1) — done
https://app.dezky.localshows portal landing page (now the new auth design / post-login home)https://auth.dezky.localshows Authentik login- Log into Authentik as admin (still using generated
AUTHENTIK_BOOTSTRAP_PASSWORDfrom.env— rotate before exposing to anyone else) - Follow
docs/AUTHENTIK-SETUP.mdto configure OIDC providers (ocis + dezky-portal) - Test OCIS SSO end-to-end (login from
https://files.dezky.local) - Verify Stalwart admin UI loads at
https://mail.dezky.local/login(root path 404s — admin SPA is at/login)
Phase 2: Build portal authentication (week 1) — done
Goal: Users can log in to the portal via Authentik.
- Add
nuxt-oidc-authtoapps/portal(1.0.0-beta.11) - Configure Authentik as OIDC provider (generic
oidcpreset with explicit URLs + discovery) - Implement login/logout flows (
/auth/oidc/login,/auth/oidc/logoutfrom the module) - Display logged-in user info on the portal home (
pages/index.vueusesuseOidcAuth()) - Add protected routes (
globalMiddlewareEnabled: true; public pages opt out viadefinePageMeta({ auth: false }))
Where things live
| Concern | File |
|---|---|
| OIDC module config | apps/portal/nuxt.config.ts (oidc block) |
| Custom login page | apps/portal/pages/auth/login.vue |
| Error states (expired / disabled) | apps/portal/pages/auth/{expired,disabled}.vue |
| Post-login landing | apps/portal/pages/index.vue |
| Visual shell + tokens | apps/portal/components/auth/*, assets/styles/tokens.css |
| Brand mark | apps/portal/components/NodeMark.vue |
Dev-mode caveats (clean up before prod)
skipAccessTokenParsing: truein the OIDC config — Authentik's access tokens in this setup aren't reliably JWT-parseable; production should re-evaluateopenIdConfigurationis pinned to the discovery URL because the genericoidcpreset doesn't ship a default — required for id_token JWKS validationdocker-compose.ymlmountsinfrastructure/docker-compose/certs/mkcert-root.peminto the portal at/etc/ssl/mkcert-root.pemand setsNODE_EXTRA_CA_CERTSso Node fetch trusts the mkcert root CA. In prod, replace with real CA-signed certs- Traefik has Docker network aliases for
auth.dezky.local,app.dezky.local, etc. so container-to-Authentik fetch resolves inside the network without going through host/etc/hosts
Phase 3: Tenant data model (week 1-2) — done
- Mongoose schemas in
services/provisioning/src/schemas/(Tenant, User, Subscription) - Tenant: slug, name, status, plan, domains, authentikGroupId, ocisSpaceId, stalwartDomain, billingInfo
- User: authentikSubjectId, tenantIds[], email, name, role, active, lastLoginAt
- Subscription: tenantId, plan, status, stripeCustomerId, stripeSubscriptionId, period dates
- CRUD endpoints behind
JwtAuthGuard(validates Authentik JWT via JWKS) - Group-based authorization: users see only tenants whose slug matches one of their Authentik
groups;dezky-platform-adminsgroup has global access - Idempotent seed (
SeedService) creates thedezkytenant + matching subscription on bootstrap - Provisioning exposed at
https://api.dezky.local(Traefik label, dev only) and via internalhttp://provisioning:3001 - Portal Nitro route at
/api/meforwards the user's encrypted access token to provisioning — verified end-to-end
Endpoints
| Method | Path | Notes |
|---|---|---|
| GET | /health |
open |
| POST/GET | /tenants, /tenants/:slug |
platform admin to create/delete; tenant members can read+update their own |
| GET | /users/me |
upserts the user on first call from JWT claims |
| GET/POST/PATCH/DELETE | /users[/:subject] |
platform admin for mutations |
| GET/POST/PATCH | /subscriptions[/:slug] |
platform admin for mutations |
Dev-mode caveats (clean up before prod)
NUXT_OIDC_TOKEN_KEYmust be base64-encoded 32 bytes (openssl rand -base64 32) — NOT hex. Module silently fails with "Invalid key length" if wrong- Portal config has
exposeAccessToken: trueso Nitro routes can forward the token; token still never reaches the browser - The
dezkygroup in Authentik is the single tenant for dev. New tenants in Phase 4 need to create matching Authentik groups - A
dezky-platform-adminsgroup doesn't exist yet — for now akadmin's membership inauthentik Adminsdoes NOT grant platform-admin rights. Create that group if you want admin-only endpoints to work for you
Phase 4: Provisioning automation (week 2-3)
Goal: Sign up creates tenant resources across all services.
- Endpoint:
POST /tenants— creates tenant in MongoDB - Worker: triggers Authentik tenant/group creation via API
- Worker: configures Stalwart domain + DKIM via admin API
- Worker: creates OCIS space
- Worker: emails customer with onboarding info
Authentik API examples:
// Create group (tenant) in Authentik
await authentikClient.coreGroupsCreate({
name: tenant.slug,
attributes: { tenantId: tenant.id, plan: tenant.plan },
})
// Create user
await authentikClient.coreUsersCreate({
username: user.email,
email: user.email,
name: user.name,
groups: [authentikGroupId],
})
Phase 5: Custom webmail (week 3-4)
Goal: Branded webmail client using Stalwart's JMAP API.
- Add JMAP client library to portal
- Build inbox view in Nuxt
- Build compose dialog
- Build message view with thread support
- Style to match Dezky branding
JMAP is a modern JSON-RPC protocol — clean to work with.
Phase 6: Production migration prep (week 4+)
When the local stack is solid and you have 2-3 pilot customers interested:
- Order Hetzner AX41-NVMe
- Order Storage Box BX11 (Falkenstein)
- Enable Hetzner Object Storage (bucket: dezky-ocis-prod)
- Build Terraform module for Hetzner provisioning
- Build Ansible playbook for bare-metal Stalwart deployment
- Set up k3s on the cloud server
- Migrate compose to Helm charts
- Configure Let's Encrypt via cert-manager
- Set up Restic backup jobs to Storage Box + B2
Phase 7: Add Zulip and Jitsi (when chat/video needed)
These were excluded from MVP for simplicity. When ready:
- Create
infrastructure/docker-compose/docker-compose.optional.yml - Add Zulip stack (server + db + worker)
- Add Jitsi stack (web + prosody + jicofo + jvb)
- Configure OIDC integration with Authentik
- Add to portal launcher
Decisions still open
These need to be made before public launch:
- Final pricing tiers (MVP, Pro, Enterprise)
- dezky.com purchase decision ($3,000 via BrandBucket)
- Final logo design (4 directions explored, need to pick one)
- Legal entity structure for the new business
- DPA (databehandleraftale) template
- Customer support process (ticket system choice)
Long-term architecture goals
- Multi-region deployment (Hetzner Falkenstein + Helsinki)
- Disaster recovery: cross-DC Restic copies
- ISO 27001 certification via Vanta
- GDPR Article 30 record of processing activities
- SOC 2 (later, for enterprise customers)
- Customer-facing status page (Uptime Kuma or cstate)
- Public documentation site
- Self-service migration tooling from M365