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,
"setup_required": setup_required,
"role": role,
"username": u.username if authenticated and u else None,
"registration_open": registration_open,
}

View File

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

View File

@ -30,6 +30,7 @@ import { cn } from '@/lib/utils';
interface UserActionsMenuProps {
user: AdminUserDetail;
currentUsername: string | null;
}
const ROLES: { value: UserRole; label: string }[] = [
@ -38,7 +39,7 @@ const ROLES: { value: UserRole; label: string }[] = [
{ 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 [roleSubmenuOpen, setRoleSubmenuOpen] = useState(false);
const [tempPassword, setTempPassword] = useState<string | null>(null);
@ -91,8 +92,14 @@ export default function UserActionsMenu({ user }: UserActionsMenuProps) {
handleAction(() => revokeSessions.mutateAsync(user.id), 'Sessions revoked');
});
const deleteUserConfirm = useConfirmAction(() => {
handleAction(() => deleteUser.mutateAsync(user.id), 'User permanently deleted');
const deleteUserConfirm = useConfirmAction(async () => {
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 =
@ -283,9 +290,11 @@ export default function UserActionsMenu({ user }: UserActionsMenuProps) {
{revokeSessionsConfirm.confirming ? 'Sure? Click to confirm' : 'Revoke All Sessions'}
</button>
{/* Delete User — hidden for own account */}
{currentUsername !== user.username && (
<>
<div className="my-1 border-t border-border" />
{/* Delete User — destructive, red two-click confirm */}
<button
className={cn(
'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" />
{deleteUserConfirm.confirming ? 'Sure? This is permanent' : 'Delete User'}
</button>
</>
)}
</div>
)}
</div>

View File

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