feat(dashboard): confirm modal for user deactivate

Deactivation signs the user out and expires every API key they hold —
warrants a styled confirm. Reactivation stays one-click since it's
non-destructive.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-05-28 19:36:17 +02:00
parent 2f6840fe3e
commit c4fa53052d

View File

@@ -79,6 +79,13 @@
let deleteTarget = $state<AdminDto | null>(null); let deleteTarget = $state<AdminDto | null>(null);
let deletePending = $state(false); let deletePending = $state(false);
// Deactivate modal -------------------------------------------------------
// Reactivate is one-click (non-destructive); deactivate routes
// through the modal because it signs the user out and expires
// every API key they hold.
let deactivateTarget = $state<AdminDto | null>(null);
let deactivatePending = $state(false);
// Validation rules (mirror backend: 2-32, [a-z0-9._-]) ------------------- // Validation rules (mirror backend: 2-32, [a-z0-9._-]) -------------------
const USERNAME_RE = /^[a-z0-9._-]{2,32}$/; const USERNAME_RE = /^[a-z0-9._-]{2,32}$/;
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
@@ -219,19 +226,36 @@
} }
} }
async function toggleActive(row: AdminDto) { async function reactivate(row: AdminDto) {
try { try {
const updated = await api.admins.update(row.id, { is_active: !row.is_active }); const updated = await api.admins.update(row.id, { is_active: true });
admins = admins.map((a) => (a.id === updated.id ? updated : a)); admins = admins.map((a) => (a.id === updated.id ? updated : a));
flash( flash('info', `${updated.username} reactivated.`);
'info',
`${updated.username} ${updated.is_active ? 'reactivated' : 'deactivated'}.`
);
} catch (e) { } catch (e) {
flash('error', e instanceof ApiError ? e.message : 'failed to update user'); flash('error', e instanceof ApiError ? e.message : 'failed to update user');
} }
} }
function askDeactivate(row: AdminDto) {
deactivateTarget = row;
}
async function confirmDeactivate() {
if (!deactivateTarget) return;
deactivatePending = true;
const target = deactivateTarget;
try {
const updated = await api.admins.update(target.id, { is_active: false });
admins = admins.map((a) => (a.id === updated.id ? updated : a));
deactivateTarget = null;
flash('info', `${updated.username} deactivated.`);
} catch (e) {
flash('error', e instanceof ApiError ? e.message : 'failed to update user');
} finally {
deactivatePending = false;
}
}
function openDelete(row: AdminDto) { function openDelete(row: AdminDto) {
deleteTarget = row; deleteTarget = row;
} }
@@ -353,7 +377,8 @@
{ label: 'Edit', onClick: () => openEdit(row) }, { label: 'Edit', onClick: () => openEdit(row) },
{ {
label: row.is_active ? 'Deactivate' : 'Reactivate', label: row.is_active ? 'Deactivate' : 'Reactivate',
onClick: () => toggleActive(row) onClick: () =>
row.is_active ? askDeactivate(row) : reactivate(row)
}, },
{ {
label: 'Delete', label: 'Delete',
@@ -571,6 +596,30 @@
</div> </div>
{/if} {/if}
<!-- Deactivate confirmation -->
{#if deactivateTarget}
{@const dt = deactivateTarget}
<ConfirmModal
title="Deactivate {dt.username}?"
variant="danger"
confirmLabel="Deactivate"
busyLabel="Deactivating…"
busy={deactivatePending}
onConfirm={confirmDeactivate}
onCancel={() => (deactivateTarget = null)}
>
<p>
Deactivating signs <strong>{dt.username}</strong> out immediately and
expires <strong>every API key</strong> they hold. Their sessions and keys
won't come back if you reactivate — they'll need to log in again and
mint new keys.
</p>
<p class="muted">
Reactivation is one click — this isn't permanent.
</p>
</ConfirmModal>
{/if}
<!-- Delete confirmation --> <!-- Delete confirmation -->
{#if deleteTarget} {#if deleteTarget}
{@const dt = deleteTarget} {@const dt = deleteTarget}