feat(web/m7-user-mgmt): /admin/users CRUD actions (U2)

Extends the U1 /admin/users page with the four U2 admin endpoints.

- "New user" button opens a modal: username, optional display
  name, password + confirm, optional admin checkbox. Validates
  password match client-side; surface server-side errors
  (username_taken, username_invalid, password_too_short).

- Per-row "Delete" opens a confirm modal explaining the cascade
  (plays, likes, sessions). Last-admin guard surfaces as a clear
  toast if the server refuses.

- Per-row "Reset password" opens a small modal: new password +
  confirm. Toast confirms success.

- Per-row "Enable / Disable auto-approve" toggles the
  per-user flag (the #355 sub-feature surface). Inline button
  state reflects the current value.

AdminUser type extended with auto_approve_requests; the badge
appears next to the admin badge when enabled.

Tests cover create-user submit, delete confirm flow, last-admin
toast on delete, reset-password submit, auto-approve toggle.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-07 12:21:23 -04:00
parent 46d38afe2d
commit 777e32ca24
3 changed files with 479 additions and 6 deletions
+26
View File
@@ -385,6 +385,7 @@ export type AdminUser = {
username: string;
display_name: string | null;
is_admin: boolean;
auto_approve_requests: boolean;
created_at: string;
};
@@ -420,6 +421,31 @@ export async function deleteInvite(token: string): Promise<void> {
await api.del(`/api/admin/invites/${token}`);
}
// U2 user-management actions -----------------------------------------------
export type CreateUserInput = {
username: string;
password: string;
display_name?: string;
is_admin?: boolean;
};
export async function createUser(input: CreateUserInput): Promise<AdminUser> {
return api.post<AdminUser>('/api/admin/users', input);
}
export async function deleteUser(id: string): Promise<void> {
await api.del(`/api/admin/users/${id}`);
}
export async function resetUserPassword(id: string, password: string): Promise<void> {
await api.post(`/api/admin/users/${id}/reset-password`, { password });
}
export async function updateUserAutoApprove(id: string, autoApprove: boolean): Promise<AdminUser> {
return api.put<AdminUser>(`/api/admin/users/${id}/auto-approve`, { auto_approve: autoApprove });
}
export function createAdminUsersQuery() {
return createQuery({
queryKey: qk.adminUsers(),