Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/sys-user-detail-header-actions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@objectstack/platform-objects': patch
---

`sys_user` account-management actions (Ban/Unban, Unlock Account, Set Password, Set Platform Role, Impersonate) now also surface on the user record-detail header (`record_header`, overflowing into the ⋯ "More" menu), not just the Users list row menu — so a platform admin can manage an account from an open user record without navigating back to the list.
18 changes: 10 additions & 8 deletions packages/platform-objects/src/identity/sys-user.object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,15 +77,17 @@ export const SysUser = ObjectSchema.create({
// These actions hit /api/v1/auth/admin/* endpoints that are only
// wired when `auth.plugins.admin` is enabled. When the plugin is
// disabled the actions still render (schema is static) but server
// returns 404. UI surfaces them under the row menu so platform
// admins can manage accounts without dropping to SQL or
// returns 404. UI surfaces them under the row menu AND the
// record-detail header (`record_header`, overflowing into the ⋯
// "More" menu) so platform admins can manage an account from either
// the Users list or an open user record — without dropping to SQL or
// a custom Setup wizard.
{
name: 'ban_user',
label: 'Ban User',
icon: 'ban',
variant: 'danger',
locations: ['list_item'],
locations: ['list_item', 'record_header'],
type: 'api',
target: '/api/v1/auth/admin/ban-user',
recordIdParam: 'userId',
Expand All @@ -101,7 +103,7 @@ export const SysUser = ObjectSchema.create({
label: 'Unban User',
icon: 'check-circle-2',
variant: 'secondary',
locations: ['list_item'],
locations: ['list_item', 'record_header'],
type: 'api',
target: '/api/v1/auth/admin/unban-user',
recordIdParam: 'userId',
Expand All @@ -116,7 +118,7 @@ export const SysUser = ObjectSchema.create({
label: 'Unlock Account',
icon: 'lock-open',
variant: 'secondary',
locations: ['list_item'],
locations: ['list_item', 'record_header'],
type: 'api',
target: '/api/v1/auth/admin/unlock-user',
recordIdParam: 'userId',
Expand Down Expand Up @@ -185,7 +187,7 @@ export const SysUser = ObjectSchema.create({
label: 'Set Password',
icon: 'key-round',
variant: 'secondary',
locations: ['list_item'],
locations: ['list_item', 'record_header'],
type: 'api',
// #2766 V1 — same path as better-auth's stock route, but served by the
// plugin-auth wrapper registered ahead of the catch-all: it accepts the
Expand Down Expand Up @@ -228,7 +230,7 @@ export const SysUser = ObjectSchema.create({
label: 'Set Platform Role',
icon: 'shield-check',
variant: 'secondary',
locations: ['list_item'],
locations: ['list_item', 'record_header'],
type: 'api',
target: '/api/v1/auth/admin/set-role',
recordIdParam: 'userId',
Expand All @@ -243,7 +245,7 @@ export const SysUser = ObjectSchema.create({
label: 'Impersonate User',
icon: 'user-cog',
variant: 'secondary',
locations: ['list_item'],
locations: ['list_item', 'record_header'],
type: 'api',
target: '/api/v1/auth/admin/impersonate-user',
recordIdParam: 'userId',
Expand Down
17 changes: 17 additions & 0 deletions packages/platform-objects/src/platform-objects.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,23 @@ describe('@objectstack/platform-objects', () => {
'dnsRecordValue',
]);
});

it('SysUser admin actions surface on the record-detail header, not just the row menu', () => {
// An admin viewing an open user record must be able to run the same
// account-management actions available from the Users list row menu —
// otherwise they have to navigate back to the list. Each admin action is
// therefore declared on BOTH `list_item` and `record_header` (the detail
// header overflows extras into the ⋯ "More" menu). `record_header` is the
// only detail-surface location objectui consumes (it does NOT read
// `record_more`), so these must use `record_header` specifically.
const adminActions = ['ban_user', 'unban_user', 'unlock_user', 'set_user_password', 'set_user_role', 'impersonate_user'];
for (const name of adminActions) {
const a = (SysUser.actions ?? []).find((x) => x.name === name);
expect(a, `${name} action must exist`).toBeTruthy();
expect(a?.locations, `${name} locations`).toContain('list_item');
expect(a?.locations, `${name} must also surface on the detail header`).toContain('record_header');
}
});
});

describe('SETUP_APP (ADR-0029 D7 shell)', () => {
Expand Down