From f16ff22a5ae476fffc640663fd2f4c1ac5a455e7 Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Wed, 27 May 2026 08:02:40 +0200 Subject: [PATCH] feat(dashboard): profile page with API-key list, mint, and revoke /admin/profile is the per-principal page available to every authenticated user (owner, admin, member). Shows the caller's identity (username, role chip, email, id) plus a full API-key list/mint/revoke surface. Minting reveals the raw token exactly once in a yellow-bordered panel with a Copy button and an "I've saved it" acknowledgement gate before the Done button enables, matching the spec's one-shot secret-display pattern. Live mirrors the backend bound-key guard: picking an app from the binding dropdown drops any instance:* scopes from the selection and greys out their checkboxes with a tooltip, so submit never hits a 422 on that case. Also surfaces a one-shot info banner when /admin/users redirects a member here with ?denied=users. Co-Authored-By: Claude Opus 4.7 (1M context) --- dashboard/src/routes/profile/+page.svelte | 760 ++++++++++++++++++++++ 1 file changed, 760 insertions(+) create mode 100644 dashboard/src/routes/profile/+page.svelte diff --git a/dashboard/src/routes/profile/+page.svelte b/dashboard/src/routes/profile/+page.svelte new file mode 100644 index 0000000..ce9bf00 --- /dev/null +++ b/dashboard/src/routes/profile/+page.svelte @@ -0,0 +1,760 @@ + + + +{#if me} +
+
+

{me.username}

+ +
+
+
+
Email
+
{me.email ?? 'No email set'}
+
+
+
User ID
+
{me.id}
+
+
+
+{/if} + +{#if deniedFromUsers} + +{/if} + +{#if banner} + +{/if} + +
+
+

API keys

+ {#if !mintOpen && !reveal} + + {/if} +
+ + {#if reveal} +
+

Save this token now — it will never be shown again.

+

+ Paste it into your CLI config or external integration. PiCloud only ever stores a hash; if + you lose it, mint a new one. +

+
+ {reveal.raw_token} + +
+ +
+ +
+
+ {/if} + + {#if mintOpen} +
+
+ + + + + +
+ +
+ Scopes +
+ {#each ALL_SCOPES as scope (scope)} + {@const instanceScope = scopeIsInstance(scope)} + {@const disabled = boundToApp && instanceScope} + + {/each} +
+ + {mintForm.scopes.size === 0 + ? 'Pick at least one scope.' + : `${mintForm.scopes.size} scope${mintForm.scopes.size === 1 ? '' : 's'} selected.`} + +
+ + {#if mintError} +
{mintError}
+ {/if} + +
+ + +
+
+ {/if} + + {#if loadError} +
+ {loadError} + +
+ {:else if keys.length === 0 && !reveal && !mintOpen} +

+ No API keys yet. Mint one to authenticate the CLI or external integrations. +

+ {:else if keys.length > 0} +
+
+
Name
+
Prefix
+
Scopes
+
Binding
+
Created
+
Last used
+
Expires
+
+
+ {#each keys as key (key.id)} +
+
{key.name}
+
{key.prefix}
+
+ {#each key.scopes as s (s)} + {s} + {/each} +
+
{appLabel(key.app_id)}
+
{shortDate(key.created_at)}
+
{relative(key.last_used_at)}
+
{key.expires_at ? shortDate(key.expires_at) : 'Never'}
+
+ +
+
+ {/each} +
+ {/if} +
+ +{#if revokeTarget} + (revokeTarget = null)} + > +

+ Revoking {revokeTarget.name} ({revokeTarget.prefix}) takes + effect immediately. Any CLI or integration using it will start returning 401 + on the next request. +

+

This can't be undone — mint a new key if you need one again.

+
+{/if} + +