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:
@@ -94,6 +94,30 @@ export class AuthentikClient {
|
||||
this.logger.log(`Set Authentik user ${userPk} is_active=${active}`)
|
||||
}
|
||||
|
||||
// Patch a user's identity / profile fields. `username` + `email` are kept
|
||||
// aligned by callers (our convention). `attributesMerge`, when given, is
|
||||
// read-modify-written so we don't clobber unrelated attributes — Authentik
|
||||
// PATCH replaces nested objects wholesale, so a naive `attributes: {...}`
|
||||
// would wipe e.g. the passwordExpired flag. No-op if nothing to change.
|
||||
async updateUser(
|
||||
userPk: number,
|
||||
patch: { username?: string; email?: string; name?: string; attributesMerge?: Record<string, unknown> },
|
||||
): Promise<void> {
|
||||
const body: Record<string, unknown> = {}
|
||||
if (patch.username !== undefined) body.username = patch.username
|
||||
if (patch.email !== undefined) body.email = patch.email
|
||||
if (patch.name !== undefined) body.name = patch.name
|
||||
if (patch.attributesMerge && Object.keys(patch.attributesMerge).length > 0) {
|
||||
const user = await this.request<AuthentikUser & { attributes?: Record<string, unknown> }>(
|
||||
`/core/users/${userPk}/`,
|
||||
)
|
||||
body.attributes = { ...(user.attributes ?? {}), ...patch.attributesMerge }
|
||||
}
|
||||
if (Object.keys(body).length === 0) return
|
||||
await this.request(`/core/users/${userPk}/`, { method: 'PATCH', body: JSON.stringify(body) })
|
||||
this.logger.log(`Updated Authentik user ${userPk} (${Object.keys(body).join(', ')})`)
|
||||
}
|
||||
|
||||
// Force-logout: terminate the user's active sessions so they must sign in
|
||||
// again. Returns how many were terminated. We pass the `?user=` filter AND
|
||||
// re-filter client-side on the session's `user` pk — Authentik's endpoint
|
||||
|
||||
Reference in New Issue
Block a user