@frigear-nu/account 0.0.10 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +7 -4
  2. package/dist/module.d.mts +5 -0
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +32 -26
  5. package/dist/runtime/app/components/Account/AccountApiTokenSlideover.d.vue.ts +11 -0
  6. package/dist/runtime/app/components/Account/AccountApiTokenSlideover.vue +95 -0
  7. package/dist/runtime/app/components/Account/AccountApiTokenSlideover.vue.d.ts +11 -0
  8. package/dist/runtime/app/components/Account/AccountHeader.vue +14 -34
  9. package/dist/runtime/app/components/Account/AccountMenu.d.vue.ts +10 -1
  10. package/dist/runtime/app/components/Account/AccountMenu.vue +100 -51
  11. package/dist/runtime/app/components/Account/AccountMenu.vue.d.ts +10 -1
  12. package/dist/runtime/app/composables/index.d.ts +2 -0
  13. package/dist/runtime/app/composables/index.js +1 -0
  14. package/dist/runtime/app/composables/useAccountNav.d.ts +20 -0
  15. package/dist/runtime/app/composables/useAccountNav.js +18 -0
  16. package/dist/runtime/app/layouts/account.d.vue.ts +13 -0
  17. package/dist/runtime/app/layouts/account.vue +54 -0
  18. package/dist/runtime/app/layouts/account.vue.d.ts +13 -0
  19. package/dist/runtime/app/pages/api-tokens.vue +193 -261
  20. package/dist/runtime/app/pages/documents.vue +25 -26
  21. package/dist/runtime/app/pages/index.vue +73 -17
  22. package/dist/runtime/app/pages/profile.vue +152 -169
  23. package/dist/runtime/app/pages/security.vue +156 -151
  24. package/dist/runtime/app/utils/api-tokens.d.ts +19 -0
  25. package/dist/runtime/app/utils/api-tokens.js +0 -0
  26. package/dist/runtime/i18n/lang/da.json +41 -9
  27. package/dist/runtime/i18n/lang/en.json +41 -9
  28. package/dist/runtime/menus.d.ts +1 -1
  29. package/dist/runtime/menus.js +10 -0
  30. package/package.json +21 -15
  31. package/dist/runtime/app/pages/layout.d.vue.ts +0 -3
  32. package/dist/runtime/app/pages/layout.vue +0 -10
  33. package/dist/runtime/app/pages/layout.vue.d.ts +0 -3
@@ -3,14 +3,13 @@ import { z } from "zod";
3
3
  defineOptions({ name: "AccountSecurityPage" });
4
4
  const t = useModuleT("account.security");
5
5
  const toast = useToast();
6
+ const showError = useErrorToast();
7
+ const confirm = useConfirmDialog();
8
+ const format = useFormat();
6
9
  const { user } = useUserSession();
7
- const { changePassword, pending } = useAccount();
10
+ const { changePassword } = useAccount();
8
11
  const webAuthnEnabled = useRuntimeConfig().public.authentication?.webAuthn === true;
9
12
  const passkeys = webAuthnEnabled ? usePasskeys() : void 0;
10
- function errorMessage(e, fallback) {
11
- const err = e;
12
- return err?.data?.statusMessage ?? err?.statusMessage ?? fallback;
13
- }
14
13
  const passwordSchema = z.object({
15
14
  currentPassword: z.string().min(1, t("changePassword.current.required", "Enter your current password.")),
16
15
  newPassword: z.string().min(8, t("changePassword.new.min", "Use at least 8 characters.")),
@@ -20,29 +19,31 @@ const passwordSchema = z.object({
20
19
  path: ["confirmNewPassword"]
21
20
  });
22
21
  const passwordState = reactive({ currentPassword: "", newPassword: "", confirmNewPassword: "" });
22
+ const passwordAction = useAction();
23
23
  async function onSubmitPassword(event) {
24
- try {
25
- await changePassword(event.data);
26
- passwordState.currentPassword = "";
27
- passwordState.newPassword = "";
28
- passwordState.confirmNewPassword = "";
29
- toast.add({ title: t("changePassword.success", "Your password was changed."), color: "success" });
30
- } catch (e) {
31
- toast.add({ title: errorMessage(e, t("changePassword.error", "Could not change your password.")), color: "error" });
32
- }
24
+ const changed = await passwordAction.run(
25
+ async () => {
26
+ await changePassword(event.data);
27
+ return true;
28
+ },
29
+ { success: t("changePassword.success", "Your password was changed."), error: t("changePassword.error", "Could not change your password.") }
30
+ );
31
+ if (!changed) return;
32
+ passwordState.currentPassword = "";
33
+ passwordState.newPassword = "";
34
+ passwordState.confirmNewPassword = "";
33
35
  }
34
36
  const passkeyList = ref([]);
35
37
  const passkeyName = ref("");
36
38
  const passkeyPending = ref(false);
37
39
  const passkeysLoading = ref(false);
38
- const removingId = ref(null);
39
40
  async function loadPasskeys() {
40
41
  if (!passkeys) return;
41
42
  passkeysLoading.value = true;
42
43
  try {
43
44
  passkeyList.value = await passkeys.listPasskeys();
44
45
  } catch (e) {
45
- toast.add({ title: errorMessage(e, t("passkeys.loadError", "Could not load your passkeys.")), color: "error" });
46
+ showError(e, t("passkeys.loadError", "Could not load your passkeys."));
46
47
  } finally {
47
48
  passkeysLoading.value = false;
48
49
  }
@@ -52,160 +53,164 @@ async function onRegisterPasskey() {
52
53
  try {
53
54
  await passkeys?.registerPasskey(user.value?.email ?? "", passkeyName.value);
54
55
  passkeyName.value = "";
55
- toast.add({ title: t("passkeys.registered", "Passkey added."), color: "success" });
56
+ toast.add({ title: t("passkeys.registered", "Passkey added."), color: "success", icon: "i-lucide-check" });
56
57
  await loadPasskeys();
57
58
  } catch (e) {
58
59
  if (e?.name !== "NotAllowedError") {
59
- toast.add({ title: errorMessage(e, t("passkeys.registerError", "Could not register the passkey.")), color: "error" });
60
+ showError(e, t("passkeys.registerError", "Could not register the passkey."));
60
61
  }
61
62
  } finally {
62
63
  passkeyPending.value = false;
63
64
  }
64
65
  }
65
- async function onRemovePasskey(id) {
66
- removingId.value = id;
66
+ const passkeyLabel = (passkey) => passkey.name || t("passkeys.unnamed", "Passkey");
67
+ async function onRemovePasskey(passkey) {
68
+ const confirmed = await confirm({
69
+ title: t("passkeys.removeConfirm.title", "Remove {name}?", { name: passkeyLabel(passkey) }),
70
+ description: t("passkeys.removeConfirm.description", "You can no longer sign in with this passkey. It stays on your device until you delete it there."),
71
+ confirmText: t("passkeys.remove", "Remove passkey"),
72
+ destructive: true
73
+ });
74
+ if (!confirmed) return;
67
75
  try {
68
- await passkeys?.removePasskey(id);
69
- toast.add({ title: t("passkeys.removed", "Passkey removed."), color: "success" });
76
+ await passkeys?.removePasskey(passkey.id);
77
+ toast.add({ title: t("passkeys.removed", "Passkey removed."), description: passkeyLabel(passkey), color: "success", icon: "i-lucide-check" });
70
78
  await loadPasskeys();
71
79
  } catch (e) {
72
- toast.add({ title: errorMessage(e, t("passkeys.removeError", "Could not remove the passkey.")), color: "error" });
73
- } finally {
74
- removingId.value = null;
80
+ showError(e, t("passkeys.removeError", "Could not remove the passkey."));
75
81
  }
76
82
  }
77
- const dateFormatter = new Intl.DateTimeFormat(void 0, { dateStyle: "medium" });
78
- function formatDate(value) {
79
- const date = new Date(value);
80
- return Number.isNaN(date.getTime()) ? "" : dateFormatter.format(date);
81
- }
83
+ const passkeyMenu = (passkey) => [[
84
+ { label: t("passkeys.remove", "Remove passkey"), icon: "i-lucide-trash-2", color: "error", onSelect: () => onRemovePasskey(passkey) }
85
+ ]];
82
86
  onMounted(loadPasskeys);
83
87
  </script>
84
88
 
85
89
  <template>
86
- <UContainer>
87
- <UPage>
88
- <UPageBody>
89
- <UPageCard
90
- :title="t('password.title', 'Password')"
91
- :description="t('password.description', 'Change your password.')"
92
- variant="subtle"
90
+ <div class="flex flex-col gap-4">
91
+ <SectionCard
92
+ :title="t('password.title', 'Password')"
93
+ :description="t('password.description', 'Change your password.')"
94
+ >
95
+ <UForm
96
+ id="account-change-password"
97
+ :schema="passwordSchema"
98
+ :state="passwordState"
99
+ class="flex flex-col gap-4"
100
+ @submit="onSubmitPassword"
101
+ >
102
+ <UFormField
103
+ :label="t('changePassword.current.label', 'Current password')"
104
+ name="currentPassword"
105
+ required
93
106
  >
94
- <UForm
95
- :schema="passwordSchema"
96
- :state="passwordState"
97
- class="space-y-4"
98
- @submit="onSubmitPassword"
99
- >
100
- <UFormField
101
- :label="t('changePassword.current.label', 'Current password')"
102
- name="currentPassword"
103
- required
104
- >
105
- <UInput
106
- v-model="passwordState.currentPassword"
107
- type="password"
108
- autocomplete="current-password"
109
- class="w-full"
110
- />
111
- </UFormField>
112
- <UFormField
113
- :label="t('changePassword.new.label', 'New password')"
114
- name="newPassword"
115
- required
116
- >
117
- <UInput
118
- v-model="passwordState.newPassword"
119
- type="password"
120
- autocomplete="new-password"
121
- class="w-full"
122
- />
123
- </UFormField>
124
- <UFormField
125
- :label="t('changePassword.confirm.label', 'Confirm new password')"
126
- name="confirmNewPassword"
127
- required
128
- >
129
- <UInput
130
- v-model="passwordState.confirmNewPassword"
131
- type="password"
132
- autocomplete="new-password"
133
- class="w-full"
134
- />
135
- </UFormField>
136
- <UButton
137
- type="submit"
138
- :loading="pending"
139
- :label="t('changePassword.submit', 'Change password')"
140
- />
141
- </UForm>
142
- </UPageCard>
143
-
144
- <UPageCard
145
- v-if="passkeys?.isEnabled.value"
146
- :title="t('passkeys.title', 'Passkeys')"
147
- :description="t('passkeys.description', 'Sign in with your fingerprint, face or device PIN instead of a password.')"
148
- variant="subtle"
107
+ <UInput
108
+ v-model="passwordState.currentPassword"
109
+ type="password"
110
+ autocomplete="current-password"
111
+ class="w-full sm:max-w-sm"
112
+ />
113
+ </UFormField>
114
+ <UFormField
115
+ :label="t('changePassword.new.label', 'New password')"
116
+ name="newPassword"
117
+ required
118
+ >
119
+ <UInput
120
+ v-model="passwordState.newPassword"
121
+ type="password"
122
+ autocomplete="new-password"
123
+ class="w-full sm:max-w-sm"
124
+ />
125
+ </UFormField>
126
+ <UFormField
127
+ :label="t('changePassword.confirm.label', 'Confirm new password')"
128
+ name="confirmNewPassword"
129
+ required
149
130
  >
150
- <ul
151
- v-if="passkeyList.length"
152
- class="divide-y divide-default"
153
- >
154
- <li
155
- v-for="passkey in passkeyList"
156
- :key="passkey.id"
157
- class="flex items-center justify-between gap-3 py-3"
158
- >
159
- <div class="flex min-w-0 items-center gap-3">
160
- <UIcon
161
- name="i-lucide-key-round"
162
- class="size-5 shrink-0 text-muted"
163
- />
164
- <div class="min-w-0">
165
- <p class="truncate font-medium text-highlighted">
166
- {{ passkey.name || t("passkeys.unnamed", "Passkey") }}
167
- </p>
168
- <p class="text-sm text-muted">
169
- {{ t("passkeys.created", "Added {date}", { date: formatDate(passkey.createdAt) }) }}
170
- </p>
171
- </div>
172
- </div>
173
- <UButton
174
- color="error"
175
- variant="ghost"
176
- icon="i-lucide-trash-2"
177
- :loading="removingId === passkey.id"
178
- :aria-label="t('passkeys.remove', 'Remove passkey')"
179
- @click="onRemovePasskey(passkey.id)"
180
- />
181
- </li>
182
- </ul>
183
- <p
184
- v-else-if="!passkeysLoading"
185
- class="text-sm text-muted"
186
- >
187
- {{ t("passkeys.empty", "You have not added any passkeys yet.") }}
188
- </p>
131
+ <UInput
132
+ v-model="passwordState.confirmNewPassword"
133
+ type="password"
134
+ autocomplete="new-password"
135
+ class="w-full sm:max-w-sm"
136
+ />
137
+ </UFormField>
138
+ </UForm>
139
+ <template #footer>
140
+ <div class="flex justify-end">
141
+ <UButton
142
+ type="submit"
143
+ form="account-change-password"
144
+ :loading="passwordAction.busy.value"
145
+ :label="t('changePassword.submit', 'Change password')"
146
+ />
147
+ </div>
148
+ </template>
149
+ </SectionCard>
189
150
 
190
- <form
191
- class="flex flex-col gap-2 sm:flex-row"
192
- @submit.prevent="onRegisterPasskey"
193
- >
194
- <UInput
195
- v-model="passkeyName"
196
- :placeholder="t('passkeys.name.placeholder', 'Name, e.g. MacBook')"
197
- :aria-label="t('passkeys.name.label', 'Passkey name')"
198
- maxlength="100"
199
- class="w-full sm:max-w-xs"
200
- />
201
- <UButton
202
- type="submit"
203
- :loading="passkeyPending"
204
- :label="t('passkeys.register', 'Register a passkey')"
151
+ <SectionCard
152
+ v-if="passkeys?.isEnabled.value"
153
+ :title="t('passkeys.title', 'Passkeys')"
154
+ :description="t('passkeys.description', 'Sign in with your fingerprint, face or device PIN instead of a password.')"
155
+ :loading="passkeysLoading && !passkeyList.length"
156
+ flush
157
+ >
158
+ <ul
159
+ v-if="passkeyList.length"
160
+ class="divide-y divide-default"
161
+ >
162
+ <li
163
+ v-for="passkey in passkeyList"
164
+ :key="passkey.id"
165
+ class="flex items-center justify-between gap-3 px-4 py-3"
166
+ >
167
+ <div class="flex min-w-0 items-center gap-3">
168
+ <UIcon
169
+ name="i-lucide-key-round"
170
+ class="size-5 shrink-0 text-muted"
205
171
  />
206
- </form>
207
- </UPageCard>
208
- </UPageBody>
209
- </UPage>
210
- </UContainer>
172
+ <div class="min-w-0">
173
+ <p class="truncate text-sm font-medium text-highlighted">
174
+ {{ passkeyLabel(passkey) }}
175
+ </p>
176
+ <p class="text-sm text-muted">
177
+ {{ t("passkeys.created", "Added {date}", { date: format.date(passkey.createdAt) }) }}
178
+ </p>
179
+ </div>
180
+ </div>
181
+ <RowMenu
182
+ :items="passkeyMenu(passkey)"
183
+ :subject="passkeyLabel(passkey)"
184
+ />
185
+ </li>
186
+ </ul>
187
+ <UEmpty
188
+ v-else
189
+ variant="naked"
190
+ icon="i-lucide-key-round"
191
+ :title="t('passkeys.empty', 'You have not added any passkeys yet.')"
192
+ />
193
+
194
+ <template #footer>
195
+ <form
196
+ class="flex flex-col gap-2 sm:flex-row sm:justify-end"
197
+ @submit.prevent="onRegisterPasskey"
198
+ >
199
+ <UInput
200
+ v-model="passkeyName"
201
+ :placeholder="t('passkeys.name.placeholder', 'Name, e.g. MacBook')"
202
+ :aria-label="t('passkeys.name.label', 'Passkey name')"
203
+ maxlength="100"
204
+ class="w-full sm:max-w-xs"
205
+ />
206
+ <UButton
207
+ type="submit"
208
+ icon="i-lucide-plus"
209
+ :loading="passkeyPending"
210
+ :label="t('passkeys.register', 'Register a passkey')"
211
+ />
212
+ </form>
213
+ </template>
214
+ </SectionCard>
215
+ </div>
211
216
  </template>
@@ -0,0 +1,19 @@
1
+ export interface ApiToken {
2
+ id: string;
3
+ name: string;
4
+ prefix: string;
5
+ last4: string;
6
+ scopes: string[];
7
+ expiresAt: string | null;
8
+ lastUsedAt: string | null;
9
+ createdAt: string;
10
+ status: 'ok' | 'revoked' | 'expired';
11
+ }
12
+ export interface ApiTokenScope {
13
+ name: string;
14
+ description?: string;
15
+ }
16
+ /** The response to creating a token: the only time its plaintext `token` exists. */
17
+ export type CreatedApiToken = ApiToken & {
18
+ token: string;
19
+ };
File without changes
@@ -7,6 +7,10 @@
7
7
  "label": "Konto",
8
8
  "description": "Din konto."
9
9
  },
10
+ "overview": {
11
+ "label": "Overblik",
12
+ "description": "Et overblik over din konto."
13
+ },
10
14
  "profile": {
11
15
  "label": "Profil",
12
16
  "description": "Administrer dine profiloplysninger og e-mailadresse."
@@ -24,6 +28,22 @@
24
28
  "description": "Opret nøgler til scripts og værktøjer."
25
29
  }
26
30
  },
31
+ "nav": {
32
+ "label": "Kontonavigation"
33
+ },
34
+ "accountMenu": "Kontomenu",
35
+ "appearance": {
36
+ "label": "Udseende",
37
+ "light": "Lyst",
38
+ "dark": "Mørkt",
39
+ "system": "System"
40
+ },
41
+ "overview": {
42
+ "title": "Konto",
43
+ "description": "Dine oplysninger og alt, der hører til din konto.",
44
+ "shortcuts": "Din konto",
45
+ "empty": "Her er ikke noget endnu."
46
+ },
27
47
  "profile": {
28
48
  "title": "Profil",
29
49
  "description": "Skift dine brugeroplysninger.",
@@ -97,6 +117,10 @@
97
117
  "unnamed": "Passkey",
98
118
  "created": "Tilføjet {date}",
99
119
  "remove": "Fjern passkey",
120
+ "removeConfirm": {
121
+ "title": "Fjern {name}?",
122
+ "description": "Du kan ikke længere logge ind med denne passkey. Den ligger stadig på din enhed, indtil du sletter den dér."
123
+ },
100
124
  "empty": "Du har ikke tilføjet nogen passkeys endnu.",
101
125
  "name": {
102
126
  "label": "Navn på passkey",
@@ -109,29 +133,31 @@
109
133
  "loadError": "Kunne ikke hente dine passkeys."
110
134
  }
111
135
  },
112
- "backTo": "Tilbage til konto",
113
136
  "signedInAs": "Logget ind som",
114
137
  "apiTokens": {
115
138
  "title": "API-nøgler",
116
139
  "description": "Opret nøgler til scripts og værktøjer.",
117
140
  "allScopes": "Fuld adgang",
118
- "copy": "Kopiér",
141
+ "copy": "Kopiér nøgle",
119
142
  "copied": "Nøglen er kopieret.",
120
143
  "copiedSnippet": "Kopieret.",
121
- "copyError": "Kunne ikke kopiere. Markér nøglen og kopiér den manuelt.",
144
+ "copySnippet": "Kopiér kommando",
122
145
  "create": {
123
- "title": "Opret en API-nøgle",
146
+ "title": "Opret API-nøgle",
124
147
  "description": "Nøgler lader scripts og værktøjer som MCP-klienter handle på dine vegne. Giv hver nøgle kun de rettigheder, den har brug for.",
125
- "submit": "Opret nøgle"
148
+ "submit": "Opret API-nøgle"
126
149
  },
127
150
  "createError": "Kunne ikke oprette nøglen.",
151
+ "created": "Nøglen er oprettet",
128
152
  "name": {
129
153
  "label": "Navn",
154
+ "description": "Så du kan genkende nøglen senere.",
130
155
  "placeholder": "fx Min MCP-klient",
131
156
  "required": "Giv nøglen et navn."
132
157
  },
133
158
  "scopes": {
134
159
  "label": "Rettigheder",
160
+ "description": "Hvad nøglen må.",
135
161
  "placeholder": "Vælg hvad nøglen må",
136
162
  "required": "Vælg mindst én rettighed."
137
163
  },
@@ -161,11 +187,13 @@
161
187
  },
162
188
  "list": {
163
189
  "title": "Dine nøgler",
164
- "description": "Træk en nøgle tilbage for at stoppe den med det samme.",
190
+ "description": "Nøgler lader scripts og værktøjer som MCP-klienter handle på dine vegne. Træk en nøgle tilbage for at stoppe den med det samme.",
165
191
  "empty": "Du har ikke oprettet nogen nøgler endnu.",
166
- "created": "Oprettet {date}",
167
- "lastUsed": "Sidst brugt {date}",
168
- "expires": "Udløber {date}"
192
+ "name": "Navn",
193
+ "createdColumn": "Oprettet",
194
+ "lastUsedColumn": "Sidst brugt",
195
+ "expiresColumn": "Udløber",
196
+ "neverUsed": "Aldrig"
169
197
  },
170
198
  "status": {
171
199
  "ok": "Aktiv",
@@ -173,6 +201,10 @@
173
201
  "expired": "Udløbet"
174
202
  },
175
203
  "revoke": "Træk nøgle tilbage",
204
+ "revokeConfirm": {
205
+ "title": "Træk {name} tilbage?",
206
+ "description": "Scripts og værktøjer, der bruger nøglen, holder op med at virke med det samme. Det kan ikke fortrydes."
207
+ },
176
208
  "revoked": "Nøglen er trukket tilbage.",
177
209
  "revokeError": "Kunne ikke trække nøglen tilbage."
178
210
  }
@@ -7,6 +7,10 @@
7
7
  "label": "Account",
8
8
  "description": "Your account."
9
9
  },
10
+ "overview": {
11
+ "label": "Overview",
12
+ "description": "A summary of your account."
13
+ },
10
14
  "profile": {
11
15
  "label": "Profile",
12
16
  "description": "Manage your profile details and email address."
@@ -24,6 +28,22 @@
24
28
  "description": "Create tokens for scripts and tools."
25
29
  }
26
30
  },
31
+ "nav": {
32
+ "label": "Account navigation"
33
+ },
34
+ "accountMenu": "Account menu",
35
+ "appearance": {
36
+ "label": "Appearance",
37
+ "light": "Light",
38
+ "dark": "Dark",
39
+ "system": "System"
40
+ },
41
+ "overview": {
42
+ "title": "Account",
43
+ "description": "Your details and everything that belongs to your account.",
44
+ "shortcuts": "Your account",
45
+ "empty": "Nothing here yet."
46
+ },
27
47
  "profile": {
28
48
  "title": "Profile",
29
49
  "description": "Change your information.",
@@ -97,6 +117,10 @@
97
117
  "unnamed": "Passkey",
98
118
  "created": "Added {date}",
99
119
  "remove": "Remove passkey",
120
+ "removeConfirm": {
121
+ "title": "Remove {name}?",
122
+ "description": "You can no longer sign in with this passkey. It stays on your device until you delete it there."
123
+ },
100
124
  "empty": "You have not added any passkeys yet.",
101
125
  "name": {
102
126
  "label": "Passkey name",
@@ -109,29 +133,31 @@
109
133
  "loadError": "Could not load your passkeys."
110
134
  }
111
135
  },
112
- "backTo": "Back to account",
113
136
  "signedInAs": "Signed in as",
114
137
  "apiTokens": {
115
138
  "title": "API tokens",
116
139
  "description": "Create tokens for scripts and tools.",
117
140
  "allScopes": "Full access",
118
- "copy": "Copy",
141
+ "copy": "Copy token",
119
142
  "copied": "Token copied.",
120
143
  "copiedSnippet": "Copied.",
121
- "copyError": "Could not copy. Select the token and copy it manually.",
144
+ "copySnippet": "Copy command",
122
145
  "create": {
123
- "title": "Create an API token",
146
+ "title": "Create API token",
124
147
  "description": "Tokens let scripts and tools such as MCP clients act as you. Give each one only the scopes it needs.",
125
- "submit": "Create token"
148
+ "submit": "Create API token"
126
149
  },
127
150
  "createError": "Could not create the token.",
151
+ "created": "Token created",
128
152
  "name": {
129
153
  "label": "Name",
154
+ "description": "So you recognise the token later.",
130
155
  "placeholder": "e.g. My MCP client",
131
156
  "required": "Give the token a name."
132
157
  },
133
158
  "scopes": {
134
159
  "label": "Scopes",
160
+ "description": "What the token may do.",
135
161
  "placeholder": "Choose what the token may do",
136
162
  "required": "Pick at least one scope."
137
163
  },
@@ -161,11 +187,13 @@
161
187
  },
162
188
  "list": {
163
189
  "title": "Your tokens",
164
- "description": "Revoke a token to stop it working immediately.",
190
+ "description": "Tokens let scripts and tools such as MCP clients act as you. Revoke one to stop it working immediately.",
165
191
  "empty": "You have not created any tokens yet.",
166
- "created": "Created {date}",
167
- "lastUsed": "Last used {date}",
168
- "expires": "Expires {date}"
192
+ "name": "Name",
193
+ "createdColumn": "Created",
194
+ "lastUsedColumn": "Last used",
195
+ "expiresColumn": "Expires",
196
+ "neverUsed": "Never"
169
197
  },
170
198
  "status": {
171
199
  "ok": "Active",
@@ -173,6 +201,10 @@
173
201
  "expired": "Expired"
174
202
  },
175
203
  "revoke": "Revoke token",
204
+ "revokeConfirm": {
205
+ "title": "Revoke {name}?",
206
+ "description": "Scripts and tools using this token stop working immediately. This cannot be undone."
207
+ },
176
208
  "revoked": "Token revoked.",
177
209
  "revokeError": "Could not revoke the token."
178
210
  }
@@ -1,2 +1,2 @@
1
- declare const _default: import("@frigear-nu/menu/types").MenuSource;
1
+ declare const _default: import("@frigear-nu/menu").MenuSource;
2
2
  export default _default;
@@ -12,6 +12,16 @@ export default defineMenuSource(() => {
12
12
  to: config.pages.index,
13
13
  order: -99
14
14
  });
15
+ extendMenuItem("account", {
16
+ id: "account-overview",
17
+ label: "Overview",
18
+ labelKey: "account.menu.overview.label",
19
+ description: "A summary of your account.",
20
+ descriptionKey: "account.menu.overview.description",
21
+ icon: "i-lucide-layout-dashboard",
22
+ to: config.pages.index,
23
+ order: 0
24
+ });
15
25
  }
16
26
  if (config?.pages?.profile) {
17
27
  const profileItem = defineMenuItem({