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:
@@ -0,0 +1,41 @@
|
||||
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'
|
||||
import { HydratedDocument, Types } from 'mongoose'
|
||||
|
||||
export type UserDocument = HydratedDocument<User>
|
||||
|
||||
export type UserRole = 'owner' | 'admin' | 'member'
|
||||
|
||||
@Schema({ collection: 'users', timestamps: true })
|
||||
export class User {
|
||||
// Authentik subject claim — stable identity across login sessions.
|
||||
@Prop({ required: true, unique: true, index: true })
|
||||
authentikSubjectId!: string
|
||||
|
||||
// Tenants this user belongs to. A user can belong to multiple tenants (e.g. partner staff).
|
||||
@Prop({ type: [Types.ObjectId], ref: 'Tenant', default: [], index: true })
|
||||
tenantIds!: Types.ObjectId[]
|
||||
|
||||
@Prop({ required: true, lowercase: true, trim: true, index: true })
|
||||
email!: string
|
||||
|
||||
@Prop({ required: true, trim: true })
|
||||
name!: string
|
||||
|
||||
// Role is per-user globally for the MVP. Refine to per-tenant later if needed.
|
||||
@Prop({ enum: ['owner', 'admin', 'member'], default: 'member' })
|
||||
role!: UserRole
|
||||
|
||||
@Prop({ default: true })
|
||||
active!: boolean
|
||||
|
||||
// Cross-tenant admin flag — independent of per-tenant role above.
|
||||
// Set at upsert time based on Authentik group membership; once set, the DB is the
|
||||
// source of truth and a future revocation requires explicit setUserAdmin().
|
||||
@Prop({ default: false, index: true })
|
||||
platformAdmin!: boolean
|
||||
|
||||
@Prop()
|
||||
lastLoginAt?: Date
|
||||
}
|
||||
|
||||
export const UserSchema = SchemaFactory.createForClass(User)
|
||||
Reference in New Issue
Block a user