feat(platform): real email domains, mailboxes & member lifecycle

Wire the mail/identity stack to real Stalwart/Authentik/OCIS provisioning,
replacing the mocked Domains and Users pages.

Domains (customer-admin):
- StalwartClient: real JMAP management (v0.16 dropped REST) — create/list/delete
  email domains via x:Domain at the internal http://stalwart:8080 listener;
  DKIM auto-generated; the records to publish are read from the domain's
  dnsZoneFile. Gated by STALWART_PROVISIONING_ENABLED.
- New Domain collection + DomainsModule: add/list/recheck/set-DMARC/remove,
  tenant-membership-gated and audited.
- DnsVerifierService: verifies MX/SPF/DKIM/DMARC/ownership against a public
  resolver (1.1.1.1/8.8.8.8) and diffs them against the expected records.
- Remove is guarded: refuses while accounts/aliases/mailing lists still use the
  domain (via Stalwart referential integrity).
- Domains page + add wizard on real data; sidebar badge counts domains needing
  attention.

Users & groups (customer-admin):
- Create a member provisioned across Authentik SSO, a Stalwart mailbox on the
  tenant's primary domain, and OCIS — returning a one-time password.
- Lifecycle: suspend/resume (Authentik is_active + freeze the mailbox via
  account permissions, original password preserved), force-logout (terminate
  sessions, filtered client-side so it can never end other users' sessions),
  reset password (new one-time password on SSO + mailbox), and remove (tear down
  mailbox + SSO identity + OCIS + doc; mailbox-in-use aware for multi-tenant
  users). Self-suspend / self-force-logout are blocked.

Infra: point platform-api at the internal Stalwart listener; document the new
STALWART_/provisioning vars in .env.example.
This commit is contained in:
Ronni Baslund
2026-06-01 21:19:42 +02:00
parent 2a43a7bbf3
commit 47eb9502f8
40 changed files with 3235 additions and 554 deletions
@@ -0,0 +1,26 @@
import { IsIn, IsOptional, IsString, Matches, MaxLength, MinLength } from 'class-validator'
// Create a workspace member. The email is formed as `localPart@<domain>`, where
// domain defaults to the tenant's primary domain. One temp password provisions
// both their SSO login and their mailbox.
export class CreateTenantMemberDto {
@IsString()
@MinLength(1)
@MaxLength(120)
name!: string
@IsString()
@MaxLength(64)
@Matches(/^[a-zA-Z0-9._-]+$/, {
message: 'address prefix may only contain letters, numbers, dots, hyphens and underscores',
})
localPart!: string
@IsIn(['admin', 'member'])
role!: 'admin' | 'member'
// Optional explicit domain (must belong to the tenant); omitted = primary.
@IsOptional()
@IsString()
domain?: string
}