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 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}
|
||||||
|
|||||||
Reference in New Issue
Block a user