feat(admin/users): editable member drawer + mailbox & ownership management
ci / typecheck (map[dir:apps/booking name:booking]) (push) Has been cancelled
ci / typecheck (map[dir:apps/portal name:portal]) (push) Has been cancelled
ci / typecheck (map[dir:apps/website name:website]) (push) Has been cancelled
ci / typecheck (map[dir:services/platform-api name:platform-api]) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / typecheck (map[dir:apps/booking name:booking]) (push) Has been cancelled
ci / typecheck (map[dir:apps/portal name:portal]) (push) Has been cancelled
ci / typecheck (map[dir:apps/website name:website]) (push) Has been cancelled
ci / typecheck (map[dir:services/platform-api name:platform-api]) (push) Has been cancelled
ci / test (push) Has been cancelled
Rebuild the /admin/users detail drawer from a read-only profile into an editable, Office 365-style panel with four sections: - Username & mail: read-only primary for mailbox users; editable sign-in (Authentik-only) for mailbox-less identities; "Create mailbox" provisions a Stalwart inbox for an external-login admin - Aliases: list/add/remove mailbox aliases (Stalwart), domain-scoped - Role: member/admin toggle with a primary-account lock (owner, mailbox-less bootstrap admin, self) and a last-admin guard - Contact information: display name, first/last name, phone, alternative email — mirrored best-effort to Authentik attributes + mailbox name Ownership transfer: "Make owner" (row menu + drawer) plus an owner-side "Transfer ownership" picker, gated to tenant admins / platform admins so a departed owner can be replaced; promotes the target and demotes the prior owner to admin. Backend (platform-api): contact fields on User; AuthentikClient.updateUser; StalwartClient.setMailboxName; UsersService updateTenantMember, changeMemberPrimaryEmail, list/add/removeMemberAlias, createMailboxForMember, transferOwnership; new DTOs and tenant-member routes. All mutations audited. Portal: Nuxt proxies for the new endpoints + extended TenantUserDoc.
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
import { IsEmail, MaxLength } from 'class-validator'
|
||||
|
||||
// Change a mailbox-less member's primary email (their Authentik sign-in
|
||||
// identity + our User.email). Refused server-side for members who have a
|
||||
// Stalwart mailbox — there the primary address IS the inbox.
|
||||
export class ChangePrimaryEmailDto {
|
||||
@IsEmail()
|
||||
@MaxLength(200)
|
||||
email!: string
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { IsOptional, IsString, Matches, MaxLength } from 'class-validator'
|
||||
|
||||
// Provision a Stalwart mailbox for an existing member who doesn't have one yet
|
||||
// (e.g. the bootstrap admin who signs in with an external email). The mailbox
|
||||
// is `localPart@domain` on one of the tenant's provisioned domains; the
|
||||
// member's sign-in identity is left untouched.
|
||||
export class CreateMailboxDto {
|
||||
@IsString()
|
||||
@MaxLength(64)
|
||||
@Matches(/^[a-zA-Z0-9._-]+$/, {
|
||||
message: 'address prefix may only contain letters, numbers, dots, hyphens and underscores',
|
||||
})
|
||||
localPart!: string
|
||||
|
||||
// Optional explicit domain (must belong to the tenant); omitted = primary.
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
domain?: string
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { IsEmail, IsString, Matches, MaxLength } from 'class-validator'
|
||||
|
||||
// Add an alias address (localPart@domain) that delivers to the member's
|
||||
// mailbox. The domain must be one of this tenant's provisioned mail domains.
|
||||
export class AddAliasDto {
|
||||
@IsString()
|
||||
@MaxLength(64)
|
||||
@Matches(/^[a-zA-Z0-9._-]+$/, {
|
||||
message: 'alias prefix may only contain letters, numbers, dots, hyphens and underscores',
|
||||
})
|
||||
localPart!: string
|
||||
|
||||
@IsString()
|
||||
@MaxLength(255)
|
||||
domain!: string
|
||||
}
|
||||
|
||||
// Remove an alias by its full address.
|
||||
export class RemoveAliasDto {
|
||||
@IsEmail()
|
||||
@MaxLength(320)
|
||||
address!: string
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { IsEmail, IsIn, IsOptional, IsString, MaxLength, MinLength, ValidateIf } from 'class-validator'
|
||||
|
||||
// Patch a workspace member's directory profile + in-tenant role. Every field is
|
||||
// optional — the drawer saves contact info and role independently, so a request
|
||||
// may carry just one section's worth. Empty strings are allowed (and clear the
|
||||
// field) for the free-text fields; alternativeEmail must be a valid email when
|
||||
// non-empty.
|
||||
export class UpdateTenantMemberDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
@MaxLength(120)
|
||||
name?: string
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(80)
|
||||
firstName?: string
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(80)
|
||||
lastName?: string
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(40)
|
||||
phone?: string
|
||||
|
||||
// '' clears it; any other value must look like an email.
|
||||
@IsOptional()
|
||||
@ValidateIf((o) => o.alternativeEmail !== '')
|
||||
@IsEmail()
|
||||
@MaxLength(200)
|
||||
alternativeEmail?: string
|
||||
|
||||
// In-tenant role only. Owner is intentionally excluded — ownership transfer
|
||||
// is a separate, guarded flow; this section never promotes to / demotes from
|
||||
// owner.
|
||||
@IsOptional()
|
||||
@IsIn(['admin', 'member'])
|
||||
role?: 'admin' | 'member'
|
||||
}
|
||||
Reference in New Issue
Block a user