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.
This commit is contained in:
+23
-32
@@ -39,43 +39,34 @@ Goal: Users can log in to the portal via Authentik.
|
||||
- `docker-compose.yml` mounts `infrastructure/docker-compose/certs/mkcert-root.pem` into the portal at `/etc/ssl/mkcert-root.pem` and sets `NODE_EXTRA_CA_CERTS` so 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)
|
||||
## Phase 3: Tenant data model (week 1-2) — done
|
||||
|
||||
Goal: MongoDB schema for tenants, users, subscriptions.
|
||||
- [x] Mongoose schemas in `services/provisioning/src/schemas/` (Tenant, User, Subscription)
|
||||
- [x] Tenant: slug, name, status, plan, domains, authentikGroupId, ocisSpaceId, stalwartDomain, billingInfo
|
||||
- [x] User: authentikSubjectId, tenantIds[], email, name, role, active, lastLoginAt
|
||||
- [x] Subscription: tenantId, plan, status, stripeCustomerId, stripeSubscriptionId, period dates
|
||||
- [x] CRUD endpoints behind `JwtAuthGuard` (validates Authentik JWT via JWKS)
|
||||
- [x] Group-based authorization: users see only tenants whose slug matches one of their Authentik `groups`; `dezky-platform-admins` group has global access
|
||||
- [x] Idempotent seed (`SeedService`) creates the `dezky` tenant + matching subscription on bootstrap
|
||||
- [x] Provisioning exposed at `https://api.dezky.local` (Traefik label, dev only) and via internal `http://provisioning:3001`
|
||||
- [x] Portal Nitro route at `/api/me` forwards the user's encrypted access token to provisioning — verified end-to-end
|
||||
|
||||
- [ ] Define Mongoose schemas in `services/provisioning/src/schemas/`
|
||||
- [ ] Tenant schema: id, name, slug, status, plan, billingInfo, domains
|
||||
- [ ] User schema: id, tenantId, email, name, role, authentikId
|
||||
- [ ] Subscription schema: tenantId, plan, status, stripeCustomerId
|
||||
- [ ] Add CRUD endpoints in NestJS
|
||||
### Endpoints
|
||||
|
||||
Schema example:
|
||||
```typescript
|
||||
// services/provisioning/src/schemas/tenant.schema.ts
|
||||
@Schema({ timestamps: true })
|
||||
export class Tenant {
|
||||
@Prop({ required: true, unique: true })
|
||||
slug: string
|
||||
| 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 |
|
||||
|
||||
@Prop({ required: true })
|
||||
name: string
|
||||
### Dev-mode caveats (clean up before prod)
|
||||
|
||||
@Prop({ enum: ['pending', 'active', 'suspended', 'deleted'], default: 'pending' })
|
||||
status: string
|
||||
|
||||
@Prop({ type: [String], default: [] })
|
||||
domains: string[]
|
||||
|
||||
@Prop({ enum: ['mvp', 'pro', 'enterprise'], default: 'mvp' })
|
||||
plan: string
|
||||
|
||||
@Prop()
|
||||
authentikGroupId?: string
|
||||
|
||||
@Prop()
|
||||
ocisSpaceId?: string
|
||||
}
|
||||
```
|
||||
- `NUXT_OIDC_TOKEN_KEY` must be base64-encoded 32 bytes (`openssl rand -base64 32`) — NOT hex. Module silently fails with "Invalid key length" if wrong
|
||||
- Portal config has `exposeAccessToken: true` so Nitro routes can forward the token; token still never reaches the browser
|
||||
- The `dezky` group in Authentik is the single tenant for dev. New tenants in Phase 4 need to create matching Authentik groups
|
||||
- A `dezky-platform-admins` group doesn't exist yet — for now akadmin's membership in `authentik Admins` does 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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user