UX polish for delete-user: username toast, hide self-delete

S-03: Delete toast now shows the deleted username from the API response
S-04: Delete button hidden for the current admin's own row (backend
still guards with 403, but no reason to show a dead button)

Adds username to auth status response so the frontend can identify
the current user.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Kyle 2026-02-27 19:30:43 +08:00
parent e7cb6de7d5
commit 48e15fa677
4 changed files with 20 additions and 5 deletions

View File

@ -531,6 +531,7 @@ async def auth_status(
"authenticated": authenticated, "authenticated": authenticated,
"setup_required": setup_required, "setup_required": setup_required,
"role": role, "role": role,
"username": u.username if authenticated and u else None,
"registration_open": registration_open, "registration_open": registration_open,
} }

View File

@ -20,6 +20,7 @@ import {
useUpdateConfig, useUpdateConfig,
getErrorMessage, getErrorMessage,
} from '@/hooks/useAdmin'; } from '@/hooks/useAdmin';
import { useAuth } from '@/hooks/useAuth';
import { getRelativeTime } from '@/lib/date-utils'; import { getRelativeTime } from '@/lib/date-utils';
import type { AdminUserDetail, UserRole } from '@/types'; import type { AdminUserDetail, UserRole } from '@/types';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
@ -55,6 +56,7 @@ function RoleBadge({ role }: { role: UserRole }) {
export default function IAMPage() { export default function IAMPage() {
const [createOpen, setCreateOpen] = useState(false); const [createOpen, setCreateOpen] = useState(false);
const { authStatus } = useAuth();
const { data: users, isLoading: usersLoading } = useAdminUsers(); const { data: users, isLoading: usersLoading } = useAdminUsers();
const { data: dashboard } = useAdminDashboard(); const { data: dashboard } = useAdminDashboard();
@ -205,7 +207,7 @@ export default function IAMPage() {
{getRelativeTime(user.created_at)} {getRelativeTime(user.created_at)}
</td> </td>
<td className="px-5 py-3 text-right"> <td className="px-5 py-3 text-right">
<UserActionsMenu user={user} /> <UserActionsMenu user={user} currentUsername={authStatus?.username ?? null} />
</td> </td>
</tr> </tr>
))} ))}

View File

@ -30,6 +30,7 @@ import { cn } from '@/lib/utils';
interface UserActionsMenuProps { interface UserActionsMenuProps {
user: AdminUserDetail; user: AdminUserDetail;
currentUsername: string | null;
} }
const ROLES: { value: UserRole; label: string }[] = [ const ROLES: { value: UserRole; label: string }[] = [
@ -38,7 +39,7 @@ const ROLES: { value: UserRole; label: string }[] = [
{ value: 'public_event_manager', label: 'Public Event Manager' }, { value: 'public_event_manager', label: 'Public Event Manager' },
]; ];
export default function UserActionsMenu({ user }: UserActionsMenuProps) { export default function UserActionsMenu({ user, currentUsername }: UserActionsMenuProps) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [roleSubmenuOpen, setRoleSubmenuOpen] = useState(false); const [roleSubmenuOpen, setRoleSubmenuOpen] = useState(false);
const [tempPassword, setTempPassword] = useState<string | null>(null); const [tempPassword, setTempPassword] = useState<string | null>(null);
@ -91,8 +92,14 @@ export default function UserActionsMenu({ user }: UserActionsMenuProps) {
handleAction(() => revokeSessions.mutateAsync(user.id), 'Sessions revoked'); handleAction(() => revokeSessions.mutateAsync(user.id), 'Sessions revoked');
}); });
const deleteUserConfirm = useConfirmAction(() => { const deleteUserConfirm = useConfirmAction(async () => {
handleAction(() => deleteUser.mutateAsync(user.id), 'User permanently deleted'); try {
const result = await deleteUser.mutateAsync(user.id);
toast.success(`User '${(result as { deleted_username: string }).deleted_username}' permanently deleted`);
setOpen(false);
} catch (err) {
toast.error(getErrorMessage(err, 'Delete failed'));
}
}); });
const isLoading = const isLoading =
@ -283,9 +290,11 @@ export default function UserActionsMenu({ user }: UserActionsMenuProps) {
{revokeSessionsConfirm.confirming ? 'Sure? Click to confirm' : 'Revoke All Sessions'} {revokeSessionsConfirm.confirming ? 'Sure? Click to confirm' : 'Revoke All Sessions'}
</button> </button>
{/* Delete User — hidden for own account */}
{currentUsername !== user.username && (
<>
<div className="my-1 border-t border-border" /> <div className="my-1 border-t border-border" />
{/* Delete User — destructive, red two-click confirm */}
<button <button
className={cn( className={cn(
'flex w-full items-center gap-2 px-3 py-2 text-sm transition-colors', 'flex w-full items-center gap-2 px-3 py-2 text-sm transition-colors',
@ -298,6 +307,8 @@ export default function UserActionsMenu({ user }: UserActionsMenuProps) {
<Trash2 className="h-4 w-4" /> <Trash2 className="h-4 w-4" />
{deleteUserConfirm.confirming ? 'Sure? This is permanent' : 'Delete User'} {deleteUserConfirm.confirming ? 'Sure? This is permanent' : 'Delete User'}
</button> </button>
</>
)}
</div> </div>
)} )}
</div> </div>

View File

@ -194,6 +194,7 @@ export interface AuthStatus {
authenticated: boolean; authenticated: boolean;
setup_required: boolean; setup_required: boolean;
role: UserRole | null; role: UserRole | null;
username: string | null;
registration_open: boolean; registration_open: boolean;
} }