feat(web/m7-user-mgmt): /admin/users page + AdminTabs entry
New admin route /admin/users with two sections:
- Accounts — table of all users (username, optional display name,
admin badge, created_at). Per-row "Make admin" / "Remove admin"
button calls PUT /api/admin/users/{id}/admin. Last-admin guard
surfaces as a toast ("Can't remove the last admin — promote
someone else first").
- Invites — list of active invites with Copy + Revoke per row.
"Generate invite" button creates a 24h token. Operator shares
the token with the invitee, who enters it on /register.
AdminTabs gains a "Users" tab (5 tabs total). New typed client
functions in admin.ts (listUsers, updateUserAdmin, listInvites,
createInvite, deleteInvite) plus query factory functions
createAdminUsersQuery / createAdminInvitesQuery and query keys
qk.adminUsers / qk.adminInvites.
Tests cover: list rendering, admin badge, promote/demote actions,
last-admin guard toast, invite generation, revoke flow, empty states.
AdminTabs.test.ts updated to assert 5 tabs and Users active state.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,14 +3,12 @@
|
||||
|
||||
type Item = { href: string; label: string };
|
||||
|
||||
// Users and Library are deferred until they have real routes.
|
||||
// Adding them as disabled items clutters the tab strip without serving the
|
||||
// operator — they'll be added back when each surface ships.
|
||||
const items: Item[] = [
|
||||
{ href: '/admin', label: 'Overview' },
|
||||
{ href: '/admin/integrations', label: 'Integrations' },
|
||||
{ href: '/admin/requests', label: 'Requests' },
|
||||
{ href: '/admin/quarantine', label: 'Quarantine' }
|
||||
{ href: '/admin', label: 'Overview' },
|
||||
{ href: '/admin/integrations', label: 'Integrations' },
|
||||
{ href: '/admin/requests', label: 'Requests' },
|
||||
{ href: '/admin/quarantine', label: 'Quarantine' },
|
||||
{ href: '/admin/users', label: 'Users' }
|
||||
];
|
||||
|
||||
function isActive(href: string): boolean {
|
||||
|
||||
@@ -44,7 +44,16 @@ describe('AdminTabs', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('renders exactly four tabs (Users + Library deferred until built)', () => {
|
||||
test('Users tab is current when on /admin/users', () => {
|
||||
state.pageUrl = new URL('http://localhost/admin/users');
|
||||
render(AdminTabs);
|
||||
expect(screen.getByRole('link', { name: /users/i })).toHaveAttribute(
|
||||
'aria-current',
|
||||
'page'
|
||||
);
|
||||
});
|
||||
|
||||
test('renders exactly five tabs', () => {
|
||||
state.pageUrl = new URL('http://localhost/admin');
|
||||
render(AdminTabs);
|
||||
const links = screen.getAllByRole('link');
|
||||
@@ -52,7 +61,8 @@ describe('AdminTabs', () => {
|
||||
'Overview',
|
||||
'Integrations',
|
||||
'Requests',
|
||||
'Quarantine'
|
||||
'Quarantine',
|
||||
'Users'
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user