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:
@@ -79,6 +79,13 @@
|
||||
let deleteTarget = $state<AdminDto | null>(null);
|
||||
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._-]) -------------------
|
||||
const USERNAME_RE = /^[a-z0-9._-]{2,32}$/;
|
||||
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
@@ -219,19 +226,36 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleActive(row: AdminDto) {
|
||||
async function reactivate(row: AdminDto) {
|
||||
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));
|
||||
flash(
|
||||
'info',
|
||||
`${updated.username} ${updated.is_active ? 'reactivated' : 'deactivated'}.`
|
||||
);
|
||||
flash('info', `${updated.username} reactivated.`);
|
||||
} catch (e) {
|
||||
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) {
|
||||
deleteTarget = row;
|
||||
}
|
||||
@@ -353,7 +377,8 @@
|
||||
{ label: 'Edit', onClick: () => openEdit(row) },
|
||||
{
|
||||
label: row.is_active ? 'Deactivate' : 'Reactivate',
|
||||
onClick: () => toggleActive(row)
|
||||
onClick: () =>
|
||||
row.is_active ? askDeactivate(row) : reactivate(row)
|
||||
},
|
||||
{
|
||||
label: 'Delete',
|
||||
@@ -571,6 +596,30 @@
|
||||
</div>
|
||||
{/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 -->
|
||||
{#if deleteTarget}
|
||||
{@const dt = deleteTarget}
|
||||
|
||||
Reference in New Issue
Block a user