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:
@@ -377,3 +377,59 @@ export function createScanScheduleQuery() {
|
||||
staleTime: 60_000
|
||||
});
|
||||
}
|
||||
|
||||
// Admin user-management ------------------------------------------------------
|
||||
|
||||
export type AdminUser = {
|
||||
id: string;
|
||||
username: string;
|
||||
display_name: string | null;
|
||||
is_admin: boolean;
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
export type AdminInvite = {
|
||||
token: string;
|
||||
invited_by: string;
|
||||
note: string | null;
|
||||
created_at: string;
|
||||
expires_at: string;
|
||||
redeemed_at: string | null;
|
||||
redeemed_by: string | null;
|
||||
};
|
||||
|
||||
export async function listUsers(): Promise<AdminUser[]> {
|
||||
const res = await api.get<{ users: AdminUser[] }>('/api/admin/users');
|
||||
return res.users;
|
||||
}
|
||||
|
||||
export async function updateUserAdmin(id: string, isAdmin: boolean): Promise<AdminUser> {
|
||||
return api.put<AdminUser>(`/api/admin/users/${id}/admin`, { is_admin: isAdmin });
|
||||
}
|
||||
|
||||
export async function listInvites(): Promise<AdminInvite[]> {
|
||||
const res = await api.get<{ invites: AdminInvite[] }>('/api/admin/invites');
|
||||
return res.invites;
|
||||
}
|
||||
|
||||
export async function createInvite(note?: string): Promise<AdminInvite> {
|
||||
return api.post<AdminInvite>('/api/admin/invites', note ? { note } : {});
|
||||
}
|
||||
|
||||
export async function deleteInvite(token: string): Promise<void> {
|
||||
await api.del(`/api/admin/invites/${token}`);
|
||||
}
|
||||
|
||||
export function createAdminUsersQuery() {
|
||||
return createQuery({
|
||||
queryKey: qk.adminUsers(),
|
||||
queryFn: listUsers
|
||||
});
|
||||
}
|
||||
|
||||
export function createAdminInvitesQuery() {
|
||||
return createQuery({
|
||||
queryKey: qk.adminInvites(),
|
||||
queryFn: listInvites
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user