@frigear-nu/account 0.1.2 → 0.1.3

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 (35) hide show
  1. package/README.md +21 -9
  2. package/dist/module.d.mts +3 -1
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +4 -2
  5. package/dist/runtime/app/components/Account/AccountApiTokenSlideover.d.vue.ts +3 -3
  6. package/dist/runtime/app/components/Account/AccountApiTokenSlideover.vue +2 -1
  7. package/dist/runtime/app/components/Account/AccountApiTokenSlideover.vue.d.ts +3 -3
  8. package/dist/runtime/app/components/Account/AccountChangeEmailSlideover.d.vue.ts +14 -0
  9. package/dist/runtime/app/components/Account/AccountChangeEmailSlideover.vue +54 -0
  10. package/dist/runtime/app/components/Account/AccountChangeEmailSlideover.vue.d.ts +14 -0
  11. package/dist/runtime/app/components/Account/AccountHeader.d.vue.ts +10 -1
  12. package/dist/runtime/app/components/Account/AccountHeader.vue +6 -3
  13. package/dist/runtime/app/components/Account/AccountHeader.vue.d.ts +10 -1
  14. package/dist/runtime/app/components/Account/AccountIdentity.vue +13 -0
  15. package/dist/runtime/app/components/Account/AccountMenu.vue +13 -3
  16. package/dist/runtime/app/components/Account/AccountPasskeySlideover.d.vue.ts +10 -0
  17. package/dist/runtime/app/components/Account/AccountPasskeySlideover.vue +60 -0
  18. package/dist/runtime/app/components/Account/AccountPasskeySlideover.vue.d.ts +10 -0
  19. package/dist/runtime/app/composables/useAccount.d.ts +2 -2
  20. package/dist/runtime/app/composables/useAccount.js +4 -3
  21. package/dist/runtime/app/layouts/account.d.vue.ts +2 -2
  22. package/dist/runtime/app/layouts/account.vue +13 -11
  23. package/dist/runtime/app/layouts/account.vue.d.ts +2 -2
  24. package/dist/runtime/app/pages/api-tokens.vue +23 -28
  25. package/dist/runtime/app/pages/documents.vue +22 -7
  26. package/dist/runtime/app/pages/index.vue +3 -6
  27. package/dist/runtime/app/pages/profile.vue +50 -89
  28. package/dist/runtime/app/pages/security.vue +40 -60
  29. package/dist/runtime/app/utils/api-tokens.d.ts +3 -14
  30. package/dist/runtime/i18n/lang/da.json +11 -9
  31. package/dist/runtime/i18n/lang/en.json +11 -9
  32. package/package.json +11 -11
  33. package/dist/runtime/app/components/Account/AccountHeaderRight.vue +0 -18
  34. /package/dist/runtime/app/components/Account/{AccountHeaderRight.d.vue.ts → AccountIdentity.d.vue.ts} +0 -0
  35. /package/dist/runtime/app/components/Account/{AccountHeaderRight.vue.d.ts → AccountIdentity.vue.d.ts} +0 -0
package/README.md CHANGED
@@ -10,9 +10,10 @@ A module for the account area (profile, security, documents, API tokens pages) i
10
10
  - 🔒 Change your password.
11
11
  - 🔑 Manage passkeys (via `@frigear-nu/auth`'s `usePasskeys`, when WebAuthn is enabled there).
12
12
  - 🧩 Overridable composable (`useAccount`) and opt-in profile/security pages (built with [`@nuxt/ui`](https://ui.nuxt.com)).
13
- - 🗂️ An `account` layout (inside the site layout) with a left account navigation built from the `account` menu
14
- of `@frigear-nu/menu` (when installed), an overview page listing the same entries, and an `account.overview` widget
15
- area (when `@frigear-nu/widget` is installed).
13
+ - 🗂️ An `account` layout: a settings page inside the public site (the site header and footer stay), with a centred
14
+ `max-w-5xl` content column. Wide screens get a left section list with the signed-in user, narrow screens a
15
+ `PageSectionSwitcher`, both built from the `account` menu of `@frigear-nu/menu` (when installed). Plus an overview
16
+ page listing the same entries, and an `account.overview` widget area (when `@frigear-nu/widget` is installed).
16
17
 
17
18
  This module is a UI shell: it owns no tables and no mutation routes. The forms call the self-service routes of
18
19
  `@frigear-nu/users` (`/api/users/me/*`) and the token routes of `@frigear-nu/auth`.
@@ -55,9 +56,11 @@ export default defineNuxtConfig({
55
56
 
56
57
  ## API routes
57
58
 
58
- - `POST /api/account/profile` - updates `name`/`avatar`.
59
- - `POST /api/account/change-email` - updates `email` (rejects if already taken by another user).
60
- - `POST /api/account/change-password` - verifies `currentPassword` and updates to `newPassword`.
59
+ These routes belong to `@frigear-nu/users`; the account pages call them:
60
+
61
+ - `PATCH /api/users/me` - updates `name`/`avatar`.
62
+ - `PUT /api/users/me/email` - updates `email` (rejects if already taken by another user).
63
+ - `PUT /api/users/me/password` - verifies `currentPassword` and updates to `newPassword`.
61
64
 
62
65
  All three require an authenticated session and re-map the updated user into it.
63
66
 
@@ -79,6 +82,15 @@ Strings live under the `account.*` namespace in `da`/`en` locale files and can b
79
82
  ## Overriding parts
80
83
 
81
84
  Any `Account*` component can be replaced by a same-named component in your app (the module's have a lower priority).
82
- `AccountHeaderRight` is the actions block of the account page heading (and of the overview's summary card);
83
- override it to show other details. Set `account.layout: false` to opt out of the account layout and render the
84
- pages in your own.
85
+ `AccountIdentity` is shown next to the user in the overview's summary card (by default a role badge for anyone who is
86
+ not a plain `user`); override it to show other details, such as a member or ID badge. To replace the whole layout,
87
+ define `app/layouts/account.vue` in your app: it wins over the module's, and the account pages still render in it. Set
88
+ `account.settings.layout: false` to remove the layout entirely and render the pages in whatever layout the app picks.
89
+
90
+ ## Page headings
91
+
92
+ The layout renders no heading: every page renders its own, as the first child of its root
93
+ `<div class="flex flex-col gap-4">`. The module's pages use `AccountHeader`, a `PageHeading` whose title is the page's
94
+ `title` meta, else the label of the `account` menu item the route belongs to; the description comes from the same
95
+ item, and `#actions` is passed through (the API tokens page puts "Create API token" there). Pages other modules add
96
+ under `/account` render a `PageHeading` with the same i18n keys as their menu item.
package/dist/module.d.mts CHANGED
@@ -28,7 +28,9 @@ type ModuleOptions = {
28
28
  settings?: {
29
29
  /**
30
30
  * Renders `/account`, `/account/**` and the configured page paths in the `account` layout (the site layout with
31
- * a left account navigation). `false` leaves the layout to the host. Defaults to `true`.
31
+ * a left account navigation). An `account` layout of the app (`app/layouts/account.vue`) or a layer replaces the
32
+ * module's and the area still renders in it. `false` registers no layout and sets no layout rule: the pages render
33
+ * in whatever layout the app picks. Defaults to `true`.
32
34
  */
33
35
  layout?: boolean;
34
36
  pages?: {
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@frigear-nu/account",
3
3
  "configKey": "account",
4
- "version": "0.1.2",
4
+ "version": "0.1.3",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.3",
7
7
  "unbuild": "unknown"
package/dist/module.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { defineNuxtModule, createResolver, hasNuxtModule, addImports, addLayout, addTypeTemplate, addRouteMiddleware, addPlugin, addComponentsDir } from '@nuxt/kit';
1
+ import { defineNuxtModule, createResolver, hasNuxtModule, addImports, addTypeTemplate, addRouteMiddleware, addPlugin, addComponentsDir } from '@nuxt/kit';
2
2
  import { registerModule, registerTailwindSource, registerLocales, resolvePagePath, addModulePages, defineVirtualRegistry, getBootstrapProviders, addOptionalMenuSource } from '@frigear-nu/module-kit/utils';
3
3
 
4
4
  const module$1 = defineNuxtModule({
@@ -68,7 +68,9 @@ const module$1 = defineNuxtModule({
68
68
  const outside = [indexPath, profilePath, securityPath, documentsPath, apiTokensPath].filter((path) => !!path && path !== "/account" && !path.startsWith("/account/"));
69
69
  const areaPatterns = prefixes.flatMap((prefix) => [`${prefix}/account`, `${prefix}/account/**`, ...outside.map((path) => `${prefix}${path}`)]);
70
70
  if (settings.layout !== false) {
71
- addLayout({ src: resolver.resolve("./runtime/app/layouts/account.vue"), filename: "account.vue" }, "account");
71
+ nuxt.hook("app:resolve", (app) => {
72
+ app.layouts.account ||= { name: "account", file: resolver.resolve("./runtime/app/layouts/account.vue") };
73
+ });
72
74
  const rules = nuxt.options.routeRules ||= {};
73
75
  for (const pattern of areaPatterns) {
74
76
  rules[pattern] = { appLayout: "account", ...rules[pattern] };
@@ -1,11 +1,11 @@
1
- import type { ApiTokenScope, CreatedApiToken } from '../../utils/api-tokens.js';
1
+ import type { ApiTokenScope } from '../../utils/api-tokens.js';
2
2
  type __VLS_Props = {
3
3
  scopes: ApiTokenScope[];
4
4
  };
5
5
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
6
- close: (created: false | CreatedApiToken) => any;
6
+ close: (created: false | import("@frigear-nu/auth").AuthCreatedTokenDto) => any;
7
7
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
8
- onClose?: ((created: false | CreatedApiToken) => any) | undefined;
8
+ onClose?: ((created: false | import("@frigear-nu/auth").AuthCreatedTokenDto) => any) | undefined;
9
9
  }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
10
10
  declare const _default: typeof __VLS_export;
11
11
  export default _default;
@@ -1,5 +1,6 @@
1
1
  <script setup>
2
2
  import { z } from "zod";
3
+ import { authApi } from "@frigear-nu/auth/api";
3
4
  const props = defineProps({
4
5
  scopes: { type: Array, required: true }
5
6
  });
@@ -25,7 +26,7 @@ const schema = z.object({
25
26
  const state = reactive({ name: "", scopes: [], expiresInDays: 90 });
26
27
  async function onSubmit(event) {
27
28
  const created = await run(
28
- () => $fetch("/api/auth/tokens", {
29
+ () => $fetch(authApi.me.tokens, {
29
30
  method: "POST",
30
31
  body: { name: event.data.name, scopes: event.data.scopes, expiresInDays: event.data.expiresInDays || null }
31
32
  }),
@@ -1,11 +1,11 @@
1
- import type { ApiTokenScope, CreatedApiToken } from '../../utils/api-tokens.js';
1
+ import type { ApiTokenScope } from '../../utils/api-tokens.js';
2
2
  type __VLS_Props = {
3
3
  scopes: ApiTokenScope[];
4
4
  };
5
5
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
6
- close: (created: false | CreatedApiToken) => any;
6
+ close: (created: false | import("@frigear-nu/auth").AuthCreatedTokenDto) => any;
7
7
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
8
- onClose?: ((created: false | CreatedApiToken) => any) | undefined;
8
+ onClose?: ((created: false | import("@frigear-nu/auth").AuthCreatedTokenDto) => any) | undefined;
9
9
  }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
10
10
  declare const _default: typeof __VLS_export;
11
11
  export default _default;
@@ -0,0 +1,14 @@
1
+ type __VLS_Props = {
2
+ email?: string;
3
+ };
4
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
5
+ close: (result: false | {
6
+ verificationRequired: boolean;
7
+ }) => any;
8
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
9
+ onClose?: ((result: false | {
10
+ verificationRequired: boolean;
11
+ }) => any) | undefined;
12
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
13
+ declare const _default: typeof __VLS_export;
14
+ export default _default;
@@ -0,0 +1,54 @@
1
+ <script setup>
2
+ import { z } from "zod";
3
+ const props = defineProps({
4
+ email: { type: String, required: false }
5
+ });
6
+ const emit = defineEmits(["close"]);
7
+ const t = useModuleT("account.profile");
8
+ const { changeEmailAddress } = useAccount();
9
+ const { busy, run } = useAction();
10
+ const schema = z.object({
11
+ email: z.email(t("email.invalid", "Enter a valid email address."))
12
+ });
13
+ const state = reactive({ email: "" });
14
+ async function onSubmit(event) {
15
+ const result = await run(
16
+ () => changeEmailAddress({ email: event.data.email }),
17
+ { error: t("email.change.error", "Could not change your email address.") }
18
+ );
19
+ if (!result) return;
20
+ emit("close", { verificationRequired: result.verificationRequired === true });
21
+ }
22
+ </script>
23
+
24
+ <template>
25
+ <FormSlideover
26
+ form-id="account-change-email"
27
+ :title="t('email.change.title', 'Change email')"
28
+ :description="props.email ? t('email.change.current', 'Your address is now {email}.', { email: props.email }) : void 0"
29
+ :submit-label="t('email.change.title', 'Change email')"
30
+ :loading="busy"
31
+ @close="emit('close', false)"
32
+ >
33
+ <UForm
34
+ id="account-change-email"
35
+ :schema="schema"
36
+ :state="state"
37
+ class="flex flex-col gap-4"
38
+ @submit="onSubmit"
39
+ >
40
+ <UFormField
41
+ name="email"
42
+ :label="t('email.change.label', 'New email')"
43
+ required
44
+ >
45
+ <UInput
46
+ v-model="state.email"
47
+ type="email"
48
+ autocomplete="email"
49
+ class="w-full"
50
+ />
51
+ </UFormField>
52
+ </UForm>
53
+ </FormSlideover>
54
+ </template>
@@ -0,0 +1,14 @@
1
+ type __VLS_Props = {
2
+ email?: string;
3
+ };
4
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
5
+ close: (result: false | {
6
+ verificationRequired: boolean;
7
+ }) => any;
8
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
9
+ onClose?: ((result: false | {
10
+ verificationRequired: boolean;
11
+ }) => any) | undefined;
12
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
13
+ declare const _default: typeof __VLS_export;
14
+ export default _default;
@@ -1,3 +1,12 @@
1
- declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
1
+ type __VLS_Slots = {
2
+ actions?: () => unknown;
3
+ };
4
+ declare const __VLS_base: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
5
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
2
6
  declare const _default: typeof __VLS_export;
3
7
  export default _default;
8
+ type __VLS_WithSlots<T, S> = T & {
9
+ new (): {
10
+ $slots: S;
11
+ };
12
+ };
@@ -1,4 +1,5 @@
1
1
  <script setup>
2
+ defineSlots();
2
3
  const t = useModuleT("account");
3
4
  const route = useRoute();
4
5
  const { current } = await useAccountNav();
@@ -12,12 +13,14 @@ useHead({ title });
12
13
 
13
14
  <template>
14
15
  <PageHeading
15
- v-if="route.meta.heading !== false"
16
16
  :title="title"
17
17
  :description="description"
18
18
  >
19
- <template #actions>
20
- <AccountHeaderRight />
19
+ <template
20
+ v-if="$slots.actions"
21
+ #actions
22
+ >
23
+ <slot name="actions" />
21
24
  </template>
22
25
  </PageHeading>
23
26
  </template>
@@ -1,3 +1,12 @@
1
- declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
1
+ type __VLS_Slots = {
2
+ actions?: () => unknown;
3
+ };
4
+ declare const __VLS_base: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
5
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
2
6
  declare const _default: typeof __VLS_export;
3
7
  export default _default;
8
+ type __VLS_WithSlots<T, S> = T & {
9
+ new (): {
10
+ $slots: S;
11
+ };
12
+ };
@@ -0,0 +1,13 @@
1
+ <script setup>
2
+ const { user } = useUserSession();
3
+ const role = computed(() => user.value?.role || void 0);
4
+ const roleLabel = computed(() => role.value && role.value !== "user" ? role.value.charAt(0).toUpperCase() + role.value.slice(1) : void 0);
5
+ </script>
6
+
7
+ <template>
8
+ <UBadge
9
+ v-if="roleLabel"
10
+ variant="subtle"
11
+ :label="roleLabel"
12
+ />
13
+ </template>
@@ -8,6 +8,7 @@ const { user } = useUserSession();
8
8
  const route = useRoute();
9
9
  const colorMode = useColorMode();
10
10
  const { localize, strip } = useLocaleLink();
11
+ const showError = useErrorToast();
11
12
  const dropdown = typeof useMenu === "function" ? useMenu("account.dropdown").data : ref([]);
12
13
  const sessionUser = computed(() => user.value);
13
14
  const displayName = computed(() => sessionUser.value?.name || sessionUser.value?.email || String(sessionUser.value?.id ?? t("title", "Account")));
@@ -19,13 +20,22 @@ const isHere = (to) => {
19
20
  const current = strip(route.path);
20
21
  return current === target || target !== "/" && current.startsWith(`${target}/`);
21
22
  };
22
- const toItem = (item) => ({
23
+ const runAction = async (item) => {
24
+ try {
25
+ await $fetch(item.to, { method: "POST" });
26
+ } catch (error) {
27
+ showError(error);
28
+ return;
29
+ }
30
+ await navigateTo(localize("/"), { external: true, replace: true });
31
+ };
32
+ const toItem = (item) => item.method && item.to ? { label: item.label, icon: item.icon, onSelect: () => runAction(item) } : {
23
33
  label: item.label,
24
34
  icon: item.icon,
25
35
  to: item.to && !item.external ? localize(item.to) : item.to,
26
36
  external: item.external,
27
37
  replace: item.external
28
- });
38
+ };
29
39
  const appearance = (label, value, icon) => ({
30
40
  label,
31
41
  icon,
@@ -39,7 +49,7 @@ const items = computed(() => {
39
49
  const visible = (dropdown.value ?? []).filter((item) => {
40
50
  const roles = item.meta?.roles;
41
51
  if (roles?.length && !(role.value && roles.includes(role.value))) return false;
42
- return !item.to || item.external || !isHere(item.to);
52
+ return !item.to || item.external || item.method || !isHere(item.to);
43
53
  });
44
54
  const last = (item) => (item.order ?? 0) >= 99;
45
55
  return [
@@ -0,0 +1,10 @@
1
+ type __VLS_Props = {
2
+ userName: string;
3
+ };
4
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
5
+ close: (added: boolean) => any;
6
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
7
+ onClose?: ((added: boolean) => any) | undefined;
8
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
9
+ declare const _default: typeof __VLS_export;
10
+ export default _default;
@@ -0,0 +1,60 @@
1
+ <script setup>
2
+ import { z } from "zod";
3
+ const props = defineProps({
4
+ userName: { type: String, required: true }
5
+ });
6
+ const emit = defineEmits(["close"]);
7
+ const t = useModuleT("account.security.passkeys");
8
+ const showError = useErrorToast();
9
+ const passkeys = usePasskeys();
10
+ const busy = ref(false);
11
+ const schema = z.object({
12
+ name: z.string().trim().max(100)
13
+ });
14
+ const state = reactive({ name: "" });
15
+ async function onSubmit(event) {
16
+ busy.value = true;
17
+ try {
18
+ await passkeys.registerPasskey(props.userName, event.data.name);
19
+ emit("close", true);
20
+ } catch (e) {
21
+ if (e?.name !== "NotAllowedError") {
22
+ showError(e, t("registerError", "Could not register the passkey."));
23
+ }
24
+ } finally {
25
+ busy.value = false;
26
+ }
27
+ }
28
+ </script>
29
+
30
+ <template>
31
+ <FormSlideover
32
+ form-id="account-passkey"
33
+ :title="t('add', 'Add passkey')"
34
+ :description="t('description', 'Sign in with your fingerprint, face or device PIN instead of a password.')"
35
+ :submit-label="t('add', 'Add passkey')"
36
+ :loading="busy"
37
+ @close="emit('close', false)"
38
+ >
39
+ <UForm
40
+ id="account-passkey"
41
+ :schema="schema"
42
+ :state="state"
43
+ class="flex flex-col gap-4"
44
+ @submit="onSubmit"
45
+ >
46
+ <UFormField
47
+ name="name"
48
+ :label="t('name.label', 'Passkey name')"
49
+ :description="t('name.description', 'Optional. Helps you tell your passkeys apart.')"
50
+ >
51
+ <UInput
52
+ v-model="state.name"
53
+ :placeholder="t('name.placeholder', 'Name, e.g. MacBook')"
54
+ maxlength="100"
55
+ class="w-full"
56
+ />
57
+ </UFormField>
58
+ </UForm>
59
+ </FormSlideover>
60
+ </template>
@@ -0,0 +1,10 @@
1
+ type __VLS_Props = {
2
+ userName: string;
3
+ };
4
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
5
+ close: (added: boolean) => any;
6
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
7
+ onClose?: ((added: boolean) => any) | undefined;
8
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
9
+ declare const _default: typeof __VLS_export;
10
+ export default _default;
@@ -1,7 +1,7 @@
1
1
  export declare const useAccount: () => {
2
2
  updateProfileDetails: (payload: {
3
- name: string;
4
- avatar?: string;
3
+ name?: string;
4
+ avatar?: string | null;
5
5
  }) => Promise<unknown>;
6
6
  changeEmailAddress: (payload: {
7
7
  email: string;
@@ -1,3 +1,4 @@
1
+ import { usersApi } from "@frigear-nu/users/api";
1
2
  export const useAccount = () => {
2
3
  const pending = ref(false);
3
4
  const error = ref(null);
@@ -17,19 +18,19 @@ export const useAccount = () => {
17
18
  }
18
19
  function updateProfileDetails(payload) {
19
20
  return withRequest(
20
- () => $fetch("/api/users/me/profile", { method: "POST", body: payload }),
21
+ () => $fetch(usersApi.me.profile, { method: "PATCH", body: payload }),
21
22
  "Failed to update your profile."
22
23
  );
23
24
  }
24
25
  function changeEmailAddress(payload) {
25
26
  return withRequest(
26
- () => $fetch("/api/users/me/change-email", { method: "POST", body: payload }),
27
+ () => $fetch(usersApi.me.email, { method: "PUT", body: payload }),
27
28
  "Failed to change your email address."
28
29
  );
29
30
  }
30
31
  function changePassword(payload) {
31
32
  return withRequest(
32
- () => $fetch("/api/users/me/change-password", { method: "POST", body: payload }),
33
+ () => $fetch(usersApi.me.password, { method: "PUT", body: payload }),
33
34
  "Failed to change your password."
34
35
  );
35
36
  }
@@ -1,6 +1,6 @@
1
- declare var __VLS_47: {};
1
+ declare var __VLS_29: {};
2
2
  type __VLS_Slots = {} & {
3
- default?: (props: typeof __VLS_47) => any;
3
+ default?: (props: typeof __VLS_29) => any;
4
4
  };
5
5
  declare const __VLS_base: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
6
6
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -3,7 +3,7 @@ defineOptions({ name: "AccountLayout" });
3
3
  const siteLayout = "default";
4
4
  const t = useModuleT("account");
5
5
  const { user } = useUserSession();
6
- const { links } = await useAccountNav();
6
+ const { links, current } = await useAccountNav();
7
7
  const profile = computed(() => {
8
8
  const value = user.value;
9
9
  const name = value?.name || value?.email || t("title", "Account");
@@ -13,20 +13,22 @@ const profile = computed(() => {
13
13
  avatar: { src: value?.avatar || void 0, alt: name }
14
14
  };
15
15
  });
16
+ useHead({ title: () => current.value?.label ?? t("title", "Account") }, { tagPriority: "low" });
16
17
  </script>
17
18
 
18
19
  <template>
19
20
  <NuxtLayout :name="siteLayout">
20
21
  <UContainer>
21
- <UPage>
22
- <template #left>
23
- <UPageAside>
22
+ <div class="mx-auto w-full max-w-5xl py-8 lg:grid lg:grid-cols-[14rem_minmax(0,1fr)] lg:gap-10 lg:py-10">
23
+ <aside class="hidden lg:block">
24
+ <div class="sticky top-[calc(var(--ui-header-height,4rem)+2.5rem)] flex flex-col gap-4">
24
25
  <UUser
25
26
  v-if="user"
26
27
  :name="profile.name"
27
28
  :description="profile.email"
28
29
  :avatar="profile.avatar"
29
- class="mb-4 px-2.5"
30
+ size="sm"
31
+ class="px-2.5"
30
32
  :ui="{ name: 'truncate', description: 'truncate' }"
31
33
  />
32
34
  <UNavigationMenu
@@ -35,20 +37,20 @@ const profile = computed(() => {
35
37
  highlight
36
38
  :aria-label="t('nav.label', 'Account navigation')"
37
39
  />
38
- </UPageAside>
39
- </template>
40
+ </div>
41
+ </aside>
40
42
 
41
- <div class="flex min-w-0 flex-col gap-6 py-8">
43
+ <!-- `self-start`: a stretched grid item has a definite height, which `h-full` cards inside a page would fill. -->
44
+ <div class="flex min-w-0 flex-col gap-4 lg:self-start">
42
45
  <PageSectionSwitcher
43
46
  v-if="links.length"
44
- class="lg:hidden"
47
+ class="mb-2 lg:hidden"
45
48
  :items="links"
46
49
  :label="t('nav.label', 'Account navigation')"
47
50
  />
48
- <AccountHeader />
49
51
  <slot />
50
52
  </div>
51
- </UPage>
53
+ </div>
52
54
  </UContainer>
53
55
  </NuxtLayout>
54
56
  </template>
@@ -1,6 +1,6 @@
1
- declare var __VLS_47: {};
1
+ declare var __VLS_29: {};
2
2
  type __VLS_Slots = {} & {
3
- default?: (props: typeof __VLS_47) => any;
3
+ default?: (props: typeof __VLS_29) => any;
4
4
  };
5
5
  declare const __VLS_base: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
6
6
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -1,4 +1,5 @@
1
1
  <script setup>
2
+ import { authApi } from "@frigear-nu/auth/api";
2
3
  import AccountApiTokenSlideover from "../components/Account/AccountApiTokenSlideover.vue";
3
4
  defineOptions({ name: "AccountApiTokensPage" });
4
5
  const t = useModuleT("account.apiTokens");
@@ -9,8 +10,8 @@ const format = useFormat();
9
10
  const { copy } = useCopyToClipboard();
10
11
  const overlay = useOverlay();
11
12
  const createSlideover = overlay.create(AccountApiTokenSlideover, { destroyOnClose: true });
12
- const { data: tokens, refresh, status } = await useCachedFetch("/api/auth/tokens", { key: "account-api-tokens", ttl: 6e4 });
13
- const { data: scopes } = await useCachedFetch("/api/auth/scopes", { key: "account-api-token-scopes" });
13
+ const { data: tokens, refresh, status } = await useCachedFetch(authApi.me.tokens, { key: "account-api-tokens", ttl: 6e4 });
14
+ const { data: scopes } = await useCachedFetch(authApi.scopes, { key: "account-api-token-scopes" });
14
15
  const revealed = ref(null);
15
16
  async function onCreate() {
16
17
  const created = await createSlideover.open({ scopes: scopes.value ?? [] });
@@ -29,7 +30,7 @@ async function onRevoke(token) {
29
30
  });
30
31
  if (!confirmed) return;
31
32
  try {
32
- await $fetch(`/api/auth/tokens/${token.id}`, { method: "DELETE" });
33
+ await $fetch(authApi.me.token(token.id), { method: "DELETE" });
33
34
  toast.add({ title: t("revoked", "Token revoked."), description: token.name, color: "success", icon: "i-lucide-check" });
34
35
  await refresh();
35
36
  } catch (e) {
@@ -45,9 +46,7 @@ const columns = computed(() => [
45
46
  { accessorKey: "expiresAt", header: t("list.expiresColumn", "Expires") },
46
47
  { id: "actions", meta: { class: { td: "w-px text-right" } } }
47
48
  ]);
48
- function tokenStatusColor(status2) {
49
- return statusColor(status2, { ok: "success", expired: "warning" });
50
- }
49
+ const tokenStatusColors = { ok: "success", expired: "warning" };
51
50
  const mcpEnabled = useRuntimeConfig().public.users?.mcp === true;
52
51
  const activeConnect = ref("claude");
53
52
  const mcpUrl = `${useRequestURL().origin}/mcp`;
@@ -76,6 +75,16 @@ const activeTab = computed(() => connectTabs.value.find((tab) => tab.value === a
76
75
 
77
76
  <template>
78
77
  <div class="flex flex-col gap-4">
78
+ <AccountHeader>
79
+ <template #actions>
80
+ <UButton
81
+ icon="i-lucide-plus"
82
+ :label="t('create.submit', 'Create API token')"
83
+ @click="onCreate"
84
+ />
85
+ </template>
86
+ </AccountHeader>
87
+
79
88
  <UAlert
80
89
  v-if="revealed"
81
90
  color="success"
@@ -107,25 +116,11 @@ const activeTab = computed(() => connectTabs.value.find((tab) => tab.value === a
107
116
  </UAlert>
108
117
 
109
118
  <TableCard
110
- :loading="status === 'pending' && !tokens?.length"
111
- :refreshing="status === 'pending' && !!tokens?.length"
119
+ :title="t('list.title', 'Your tokens')"
120
+ :description="t('list.description', 'Tokens let scripts and tools such as MCP clients act as you. Revoke one to stop it working immediately.')"
121
+ :loading="status === 'pending' && !tokens"
122
+ :refreshing="status === 'pending' && !!tokens"
112
123
  >
113
- <template #header>
114
- <div class="min-w-0">
115
- <h2 class="text-sm font-semibold text-highlighted">
116
- {{ t("list.title", "Your tokens") }}
117
- </h2>
118
- <p class="text-sm text-muted">
119
- {{ t("list.description", "Tokens let scripts and tools such as MCP clients act as you. Revoke one to stop it working immediately.") }}
120
- </p>
121
- </div>
122
- <UButton
123
- icon="i-lucide-plus"
124
- :label="t('create.submit', 'Create API token')"
125
- @click="onCreate"
126
- />
127
- </template>
128
-
129
124
  <UTable
130
125
  :data="tokens ?? []"
131
126
  :columns="columns"
@@ -137,11 +132,11 @@ const activeTab = computed(() => connectTabs.value.find((tab) => tab.value === a
137
132
  :description="`${row.original.prefix}\u2026${row.original.last4}`"
138
133
  >
139
134
  <template #badges>
140
- <UBadge
141
- :color="tokenStatusColor(row.original.status)"
142
- variant="subtle"
143
- size="sm"
135
+ <StatusBadge
136
+ :status="row.original.status"
144
137
  :label="t(`status.${row.original.status}`, row.original.status)"
138
+ :colors="tokenStatusColors"
139
+ size="sm"
145
140
  />
146
141
  </template>
147
142
  </TableIdentity>