diff --git a/dashboard/src/routes/users/+page.svelte b/dashboard/src/routes/users/+page.svelte index e049dbb..c01f52b 100644 --- a/dashboard/src/routes/users/+page.svelte +++ b/dashboard/src/routes/users/+page.svelte @@ -79,6 +79,13 @@ let deleteTarget = $state(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(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 @@ {/if} + +{#if deactivateTarget} + {@const dt = deactivateTarget} + (deactivateTarget = null)} + > +

+ Deactivating signs {dt.username} out immediately and + expires every API key they hold. Their sessions and keys + won't come back if you reactivate — they'll need to log in again and + mint new keys. +

+

+ Reactivation is one click — this isn't permanent. +

+
+{/if} + {#if deleteTarget} {@const dt = deleteTarget}