559348f6bc
Security & audit (admin) - Audit log: real, tenant-scoped — widened GET /tenants/:slug/audit with q/action/outcome/actorEmail/since/before; UI gains search, outcome + time filters, action chips, cursor pagination, and client-side CSV export. - Security policy: new tenant.securityPolicy (mfaMode, session idle/absolute, allowedCountries, ipAllowlist) + PATCH /tenants/:slug/security-policy (membership-gated, audited). Editable, labelled by enforcement status. - MFA: live enrollment overview via GET /tenants/:slug/mfa-status (Authentik countAuthenticators per member). - SSO apps (Dezky as IdP): real Authentik OIDC provider + application CRUD, scoped to the tenant group. New AuthentikClient methods (provider/app/binding + flow/key/scope discovery), TenantSsoApp schema, TenantSsoService (rollback on partial failure; client secret never stored), GET/POST/DELETE /tenants/:slug/sso-apps. Validated end-to-end against live Authentik. - Deferred: shared-flow MFA/geo/session enforcement (global auth-flow blast radius) — to be done as its own reviewed change. Bundled in-progress work that shares the same files (kept together so the tree stays green): - Storage page: StorageService + GET /tenants/:slug/storage (OCIS-backed), storage.get proxy, storage.vue. - Per-tenant roles: User.tenantRoles + MeProfile.tenantRoles plumbing.
130 lines
4.5 KiB
TypeScript
130 lines
4.5 KiB
TypeScript
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'
|
|
import { HydratedDocument, Types } from 'mongoose'
|
|
|
|
export type TenantDocument = HydratedDocument<Tenant>
|
|
|
|
export type TenantStatus = 'pending' | 'active' | 'suspended' | 'deleted'
|
|
export type TenantPlan = 'mvp' | 'pro' | 'enterprise'
|
|
|
|
// One field per external integration. 'pending' = not yet tried; 'ok' = synced;
|
|
// 'error' = last attempt failed (see provisioningErrors for detail).
|
|
export type IntegrationState = 'pending' | 'ok' | 'error' | 'skipped'
|
|
|
|
@Schema({ collection: 'tenants', timestamps: true })
|
|
export class Tenant {
|
|
// URL-safe identifier, also used as Authentik group name. Lowercase, hyphenated.
|
|
@Prop({ required: true, unique: true, index: true, lowercase: true, trim: true })
|
|
slug!: string
|
|
|
|
@Prop({ required: true, trim: true })
|
|
name!: string
|
|
|
|
@Prop({ enum: ['pending', 'active', 'suspended', 'deleted'], default: 'pending', index: true })
|
|
status!: TenantStatus
|
|
|
|
@Prop({ enum: ['mvp', 'pro', 'enterprise'], default: 'mvp' })
|
|
plan!: TenantPlan
|
|
|
|
// Initial seat count from provisioning. Used for portfolio displays and
|
|
// (later) MRR calculations. The "used" count comes from User.tenantIds —
|
|
// not stored here to avoid a denormalized field that drifts on every
|
|
// user-add/remove. Default 0 so older docs without this field render
|
|
// as "0 / N" without throwing.
|
|
@Prop({ type: Number, min: 0, default: 0 })
|
|
seats!: number
|
|
|
|
// Custom domains attached to this tenant. First entry is the primary host.
|
|
@Prop({ type: [String], default: [] })
|
|
domains!: string[]
|
|
|
|
// Partner-editable customer metadata. Display-only — not used for
|
|
// provisioning. `industry` is free text; `brandColor` is a #rrggbb hex
|
|
// (validated at the DTO layer) rendered as the customer's swatch in the
|
|
// partner portal.
|
|
@Prop({ trim: true })
|
|
industry?: string
|
|
|
|
@Prop({ trim: true })
|
|
brandColor?: string
|
|
|
|
// Optional MSP/reseller this tenant belongs to. Sparse — direct tenants have none.
|
|
@Prop({ type: Types.ObjectId, ref: 'Partner', index: true, sparse: true })
|
|
partnerId?: Types.ObjectId
|
|
|
|
// External system handles — filled in by the provisioning worker (Phase 4)
|
|
@Prop({ index: true, sparse: true })
|
|
authentikGroupId?: string
|
|
|
|
@Prop({ sparse: true })
|
|
ocisSpaceId?: string
|
|
|
|
@Prop({ sparse: true })
|
|
stalwartDomain?: string
|
|
|
|
// Free-form billing context. Stripe IDs live on Subscription, not here.
|
|
@Prop({
|
|
type: {
|
|
companyName: String,
|
|
vatId: String,
|
|
country: String,
|
|
contactEmail: String,
|
|
},
|
|
default: {},
|
|
})
|
|
billingInfo!: {
|
|
companyName?: string
|
|
vatId?: string
|
|
country?: string
|
|
contactEmail?: string
|
|
}
|
|
|
|
// Customer-managed security policy. Stored intent; enforcement is wired
|
|
// incrementally (MFA enrollment is read live; MFA/geo/session enforcement
|
|
// via Authentik lands in a later stage). mfaMode: 'all' | 'admins' |
|
|
// 'optional'. Timeouts in minutes/hours. allowedCountries = ISO alpha-2;
|
|
// ipAllowlist = CIDR strings.
|
|
@Prop({
|
|
type: {
|
|
mfaMode: { type: String, enum: ['all', 'admins', 'optional'], default: 'optional' },
|
|
sessionIdleMinutes: { type: Number, min: 0 },
|
|
sessionAbsoluteHours: { type: Number, min: 0 },
|
|
allowedCountries: { type: [String], default: undefined },
|
|
ipAllowlist: { type: [String], default: undefined },
|
|
},
|
|
default: () => ({ mfaMode: 'optional' }),
|
|
})
|
|
securityPolicy!: {
|
|
mfaMode: 'all' | 'admins' | 'optional'
|
|
sessionIdleMinutes?: number
|
|
sessionAbsoluteHours?: number
|
|
allowedCountries?: string[]
|
|
ipAllowlist?: string[]
|
|
}
|
|
|
|
// Per-integration provisioning state. Each one is updated independently when its
|
|
// upstream API call succeeds or fails — orchestration is best-effort, not atomic.
|
|
@Prop({
|
|
type: {
|
|
authentik: { type: String, enum: ['pending', 'ok', 'error', 'skipped'], default: 'pending' },
|
|
stalwart: { type: String, enum: ['pending', 'ok', 'error', 'skipped'], default: 'pending' },
|
|
ocis: { type: String, enum: ['pending', 'ok', 'error', 'skipped'], default: 'pending' },
|
|
},
|
|
default: () => ({ authentik: 'pending', stalwart: 'pending', ocis: 'pending' }),
|
|
})
|
|
provisioningStatus!: {
|
|
authentik: IntegrationState
|
|
stalwart: IntegrationState
|
|
ocis: IntegrationState
|
|
}
|
|
|
|
// Last error message per integration. Cleared when a subsequent attempt succeeds.
|
|
@Prop({ type: Object, default: {} })
|
|
provisioningErrors!: {
|
|
authentik?: string
|
|
stalwart?: string
|
|
ocis?: string
|
|
}
|
|
}
|
|
|
|
export const TenantSchema = SchemaFactory.createForClass(Tenant)
|