From 700ae7b7d1b528c4c74d82aa4553628463fb3cac Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Wed, 27 May 2026 08:05:02 +0200 Subject: [PATCH] feat(dashboard): users admin page with invite/edit/delete + password reveal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /admin/users is the owner+admin surface for managing the platform's user list. Members get bounced to /profile?denied=users. Invite generates a random 16-char password client-side, POSTs the new user, and surfaces the cleartext exactly once in a yellow- bordered reveal modal with a Copy button and an "I've shared it" acknowledgement gate. Owner role is intentionally not in the create form — promote via Edit after creation, matching the backend's deliberate-step comment. Edit handles username, email, role (with affordance hiding: admins see admin/member only), is_active toggle, and a separate "Reset password" button that re-uses the same reveal flow. Delete uses ConfirmModal with confirmPhrase=username and explains the last-owner/last-admin 422s up front. Username + email validated client-side against the same patterns the backend enforces so the form fails fast rather than always on the round-trip. Co-Authored-By: Claude Opus 4.7 (1M context) --- dashboard/src/routes/users/+page.svelte | 980 ++++++++++++++++++++++++ 1 file changed, 980 insertions(+) create mode 100644 dashboard/src/routes/users/+page.svelte diff --git a/dashboard/src/routes/users/+page.svelte b/dashboard/src/routes/users/+page.svelte new file mode 100644 index 0000000..88fbbeb --- /dev/null +++ b/dashboard/src/routes/users/+page.svelte @@ -0,0 +1,980 @@ + + + +
+

Users

+
+ + +
+
+ +{#if banner} + +{/if} + +{#if loadError} +
+ {loadError} + +
+{:else if admins.length === 0} +

No users yet. Invite one to get started.

+{:else} +
+
+
Username
+
Role
+
Email
+
Status
+
Created
+
Last login
+
+
+ {#each filtered as row (row.id)} +
+
+ {row.username} + {#if me && me.id === row.id} + (you) + {/if} +
+
+ +
+ {#if row.is_active} + ● Active + {:else} + ○ Inactive + {/if} +
+
{shortDate(row.created_at)}
+
{relative(row.last_login_at)}
+
+ + + {#if canDelete(row)} + + {/if} +
+
+ {/each} + {#if filtered.length === 0 && admins.length > 0} +
No matches for "{search}".
+ {/if} +
+{/if} + + +{#if inviteOpen} + +{/if} + + +{#if editTarget} + {@const target = editTarget} + +{/if} + + +{#if revealPassword} + +{/if} + + +{#if deleteTarget} + {@const dt = deleteTarget} + (deleteTarget = null)} + > + {#if me && me.id === dt.id} +

+ You're about to delete your own account. You'll be signed out + immediately and won't be able to sign back in. +

+ {:else} +

+ This permanently removes {dt.username}, all their sessions, and all + their API keys. This cannot be undone. +

+ {/if} +

+ If they're the only remaining owner or active admin the server will reject the request + with a 422 — promote/activate someone else first. +

+
+{/if} + +