@frigear-nu/account 0.0.0-stage → 0.0.1

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 (34) hide show
  1. package/README.md +70 -2
  2. package/dist/module.d.mts +31 -0
  3. package/dist/module.json +9 -0
  4. package/dist/module.mjs +88 -0
  5. package/dist/runtime/app/components/Account/AccountMenu.d.vue.ts +3 -0
  6. package/dist/runtime/app/components/Account/AccountMenu.vue +64 -0
  7. package/dist/runtime/app/components/Account/AccountMenu.vue.d.ts +3 -0
  8. package/dist/runtime/app/composables/index.d.ts +1 -0
  9. package/dist/runtime/app/composables/index.js +1 -0
  10. package/dist/runtime/app/composables/useAccount.d.ts +16 -0
  11. package/dist/runtime/app/composables/useAccount.js +37 -0
  12. package/dist/runtime/app/pages/index.d.vue.ts +3 -0
  13. package/dist/runtime/app/pages/index.vue +24 -0
  14. package/dist/runtime/app/pages/index.vue.d.ts +3 -0
  15. package/dist/runtime/app/pages/profile.d.vue.ts +3 -0
  16. package/dist/runtime/app/pages/profile.vue +76 -0
  17. package/dist/runtime/app/pages/profile.vue.d.ts +3 -0
  18. package/dist/runtime/app/pages/security.d.vue.ts +3 -0
  19. package/dist/runtime/app/pages/security.vue +91 -0
  20. package/dist/runtime/app/pages/security.vue.d.ts +3 -0
  21. package/dist/runtime/i18n/lang/da.json +64 -0
  22. package/dist/runtime/i18n/lang/en.json +64 -0
  23. package/dist/runtime/menus.d.ts +2 -0
  24. package/dist/runtime/menus.js +36 -0
  25. package/dist/runtime/server/db/schema.sqlite.d.ts +24 -0
  26. package/dist/runtime/server/db/schema.sqlite.js +4 -0
  27. package/dist/runtime/server/routes/account/change-email.post.d.ts +2 -0
  28. package/dist/runtime/server/routes/account/change-email.post.js +20 -0
  29. package/dist/runtime/server/routes/account/change-password.post.d.ts +2 -0
  30. package/dist/runtime/server/routes/account/change-password.post.js +26 -0
  31. package/dist/runtime/server/routes/account/profile.post.d.ts +2 -0
  32. package/dist/runtime/server/routes/account/profile.post.js +17 -0
  33. package/dist/types.d.mts +3 -0
  34. package/package.json +77 -3
package/README.md CHANGED
@@ -1,3 +1,71 @@
1
- # Temporary Holding Version
1
+ # @frigear-nu/account
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A module for the account area (profile, email, password, security) in the main frigear app. Migrated from
4
+ [`nuxt-frigear-nu`](https://github.com/Frigear-nu/nuxt-frigear-nu)'s `app/pages/account/**`.
5
+
6
+ ## Features
7
+
8
+ - 👤 Update your profile details (name, avatar).
9
+ - ✉️ Change your email address.
10
+ - 🔒 Change your password.
11
+ - 🔑 Manage passkeys (via `@frigear-nu/users`' `useUserPasskeys`, when WebAuthn is enabled there).
12
+ - 🧩 Overridable composable (`useAccount`) and opt-in profile/security pages (built with [`@nuxt/ui`](https://ui.nuxt.com)).
13
+ - 🗂️ Contributes "Profile"/"Security" tiles to `@frigear-nu/menu` (when installed), and an opt-in dashboard page
14
+ listing them.
15
+
16
+ This module reuses `@frigear-nu/users`' session/password utilities instead of registering its own `users` database
17
+ table or duplicating auth logic - it only adds the account-management routes that didn't already exist there.
18
+
19
+ ## Quick Setup
20
+
21
+ Install the module to your Nuxt application with one command:
22
+
23
+ ```bash
24
+ npx nuxt module add @frigear-nu/account
25
+ ```
26
+
27
+ This module requires [`@frigear-nu/users`](../users) to be installed in the host app, since it updates the same
28
+ `users` database table and reuses its session helpers (`mapUserToSession`, `hashPassword`/`verifyPassword`).
29
+
30
+ ## Configuration
31
+
32
+ Configure the module via the `account` key in `nuxt.config.ts`:
33
+
34
+ ```ts
35
+ export default defineNuxtConfig({
36
+ modules: [
37
+ '@nuxthub/core',
38
+ '@frigear-nu/users',
39
+ '@frigear-nu/menu', // optional - enables the dashboard tiles/page
40
+ '@frigear-nu/account',
41
+ ],
42
+ account: {
43
+ settings: {
44
+ pages: {
45
+ profile: true, // or a custom path, e.g. '/me'
46
+ security: true,
47
+ // Only registered when `@frigear-nu/menu` is also installed.
48
+ index: true,
49
+ },
50
+ },
51
+ },
52
+ })
53
+ ```
54
+
55
+ ## API routes
56
+
57
+ - `POST /api/account/profile` - updates `name`/`avatar`.
58
+ - `POST /api/account/change-email` - updates `email` (rejects if already taken by another user).
59
+ - `POST /api/account/change-password` - verifies `currentPassword` and updates to `newPassword`.
60
+
61
+ All three require an authenticated session and re-map the updated user into it.
62
+
63
+ ## Composable
64
+
65
+ ```ts
66
+ const { updateProfileDetails, changeEmailAddress, changePassword, pending, error } = useAccount()
67
+
68
+ await updateProfileDetails({ name: 'Ada Lovelace' })
69
+ await changeEmailAddress({ email: 'ada@example.com' })
70
+ await changePassword({ currentPassword: 'old', newPassword: 'new-password', confirmNewPassword: 'new-password' })
71
+ ```
@@ -0,0 +1,31 @@
1
+ import * as _nuxt_schema from '@nuxt/schema';
2
+
3
+ type PublicModuleOptions = {
4
+ pages: {
5
+ index?: string;
6
+ profile?: string;
7
+ security?: string;
8
+ };
9
+ };
10
+ type ModuleOptions = {
11
+ settings?: {
12
+ pages?: {
13
+ index?: string | boolean;
14
+ profile?: string | boolean;
15
+ security?: string | boolean;
16
+ };
17
+ };
18
+ };
19
+
20
+ declare module 'nuxt/schema' {
21
+ interface NuxtConfig {
22
+ account?: ModuleOptions;
23
+ }
24
+ interface PublicRuntimeConfig {
25
+ account?: PublicModuleOptions;
26
+ }
27
+ }
28
+ declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
29
+
30
+ export { _default as default };
31
+ export type { ModuleOptions, PublicModuleOptions };
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "@frigear-nu/account",
3
+ "configKey": "account",
4
+ "version": "0.0.1",
5
+ "builder": {
6
+ "@nuxt/module-builder": "1.0.3",
7
+ "unbuild": "unknown"
8
+ }
9
+ }
@@ -0,0 +1,88 @@
1
+ import { defineNuxtModule, createResolver, hasNuxtModule, addServerHandler, addImports, extendPages, addComponentsDir } from '@nuxt/kit';
2
+ import { registerModule, registerLocales, extendDatabaseTable } from '@frigear-nu/module-kit/utils';
3
+
4
+ function resolvePagePath(setting, defaultPath) {
5
+ if (typeof setting === "string") return setting;
6
+ if (setting === true) return defaultPath;
7
+ return void 0;
8
+ }
9
+ const module$1 = defineNuxtModule({
10
+ meta: { name: "@frigear-nu/account", configKey: "account" },
11
+ defaults: {
12
+ settings: { pages: {} }
13
+ },
14
+ moduleDependencies: {
15
+ "@nuxthub/core": {
16
+ version: ">=0.10"
17
+ },
18
+ "@frigear-nu/module-kit": {
19
+ version: ">=0.0.1"
20
+ },
21
+ "@frigear-nu/users": {
22
+ version: ">=0.0.1"
23
+ },
24
+ "@frigear-nu/menu": {
25
+ version: ">=0.0.1",
26
+ optional: true
27
+ }
28
+ },
29
+ async setup(options, nuxt) {
30
+ registerModule("@frigear-nu/account", nuxt);
31
+ const resolver = createResolver(import.meta.url);
32
+ registerLocales(nuxt, resolver.resolve("./runtime/i18n/lang"));
33
+ const settings = options.settings ?? {};
34
+ const withMenus = hasNuxtModule("@frigear-nu/menu");
35
+ addServerHandler({
36
+ route: "/api/account/profile",
37
+ method: "post",
38
+ handler: resolver.resolve("./runtime/server/routes/account/profile.post")
39
+ });
40
+ addServerHandler({
41
+ route: "/api/account/change-email",
42
+ method: "post",
43
+ handler: resolver.resolve("./runtime/server/routes/account/change-email.post")
44
+ });
45
+ addServerHandler({
46
+ route: "/api/account/change-password",
47
+ method: "post",
48
+ handler: resolver.resolve("./runtime/server/routes/account/change-password.post")
49
+ });
50
+ addImports([{ name: "useAccount", from: resolver.resolve("./runtime/app/composables/useAccount") }]);
51
+ const profilePath = resolvePagePath(settings.pages?.profile, "/account/profile");
52
+ const securityPath = resolvePagePath(settings.pages?.security, "/account/security");
53
+ const indexPath = withMenus ? resolvePagePath(settings.pages?.index, "/account") : void 0;
54
+ if (!withMenus && settings.pages?.index) {
55
+ console.warn("[@frigear-nu/account] `settings.pages.index` was set, but `@frigear-nu/menu` is not installed - skipping the dashboard page.");
56
+ }
57
+ nuxt.options.runtimeConfig.public.account = {
58
+ pages: { index: indexPath, profile: profilePath, security: securityPath }
59
+ };
60
+ if (indexPath || profilePath || securityPath) {
61
+ extendPages((pages) => {
62
+ const existingPaths = new Set(pages.map((page) => page.path));
63
+ if (indexPath && !existingPaths.has(indexPath)) {
64
+ pages.push({ name: "account-index", path: indexPath, file: resolver.resolve("./runtime/app/pages/index.vue") });
65
+ }
66
+ if (profilePath && !existingPaths.has(profilePath)) {
67
+ pages.push({ name: "account-profile", path: profilePath, file: resolver.resolve("./runtime/app/pages/profile.vue") });
68
+ }
69
+ if (securityPath && !existingPaths.has(securityPath)) {
70
+ pages.push({ name: "account-security", path: securityPath, file: resolver.resolve("./runtime/app/pages/security.vue") });
71
+ }
72
+ });
73
+ }
74
+ if (withMenus) {
75
+ const { registerMenuSource } = await import('@frigear-nu/menu/utils');
76
+ registerMenuSource(nuxt, { file: resolver.resolve("./runtime/menus"), menu: "dashboard.account" });
77
+ }
78
+ addComponentsDir({
79
+ path: resolver.resolve("./runtime/app/components")
80
+ });
81
+ extendDatabaseTable(nuxt, {
82
+ resolver,
83
+ file: (dialect) => `./runtime/server/db/schema.${dialect}`
84
+ });
85
+ }
86
+ });
87
+
88
+ export { module$1 as default };
@@ -0,0 +1,3 @@
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>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -0,0 +1,64 @@
1
+ <script setup>
2
+ import { computedAsync } from "@vueuse/core";
3
+ const { user } = useUserSession();
4
+ const { data: dropdownItems } = useMenu("account.dropdown");
5
+ const getFirstLetter = (name) => {
6
+ return String(typeof name === "string" ? name.charAt(0)?.toUpperCase() : name);
7
+ };
8
+ const items = computedAsync(async () => {
9
+ const items2 = [];
10
+ if (user.value) {
11
+ items2.push([
12
+ {
13
+ type: "label",
14
+ label: `${user.value.email || user.value.name || user.value.id}`,
15
+ avatar: {
16
+ text: getFirstLetter(user.value.email || user.value.name || user.value.id)
17
+ },
18
+ slot: "label"
19
+ }
20
+ ]);
21
+ }
22
+ items2.push(dropdownItems.value?.map((item) => ({
23
+ label: item.label,
24
+ to: item.to,
25
+ icon: item.icon,
26
+ external: item.external,
27
+ replace: item.external
28
+ })) || []);
29
+ return items2;
30
+ });
31
+ </script>
32
+
33
+ <template>
34
+ <UDropdownMenu
35
+ :items="items"
36
+ :content="{ align: 'start' }"
37
+ :ui="{ content: 'w-48' }"
38
+ >
39
+ <template #default>
40
+ <UButton
41
+ aria-label="Account Menu"
42
+ color="neutral"
43
+ variant="ghost"
44
+ icon="i-lucide-user"
45
+ />
46
+ </template>
47
+ <template #label="bindItem">
48
+ <LazyUUser
49
+ v-bind="bindItem.item"
50
+ >
51
+ <template #name>
52
+ <!-- <UBadge -->
53
+ <!-- v-if="user && !userHasRole(user, 'user')" -->
54
+ <!-- size="xs" -->
55
+ <!-- variant="subtle" -->
56
+ <!-- > -->
57
+ <!-- {{ upperFirst(user.role) }} -->
58
+ <!-- </UBadge> -->
59
+ <div>{{ bindItem.item?.label }}</div>
60
+ </template>
61
+ </LazyUUser>
62
+ </template>
63
+ </UDropdownMenu>
64
+ </template>
@@ -0,0 +1,3 @@
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>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -0,0 +1 @@
1
+ export { useAccount } from './useAccount.js';
@@ -0,0 +1 @@
1
+ export { useAccount } from "./useAccount.js";
@@ -0,0 +1,16 @@
1
+ export declare const useAccount: () => {
2
+ updateProfileDetails: (payload: {
3
+ name: string;
4
+ avatar?: string;
5
+ }) => Promise<unknown>;
6
+ changeEmailAddress: (payload: {
7
+ email: string;
8
+ }) => Promise<unknown>;
9
+ changePassword: (payload: {
10
+ currentPassword: string;
11
+ newPassword: string;
12
+ confirmNewPassword: string;
13
+ }) => Promise<unknown>;
14
+ pending: any;
15
+ error: any;
16
+ };
@@ -0,0 +1,37 @@
1
+ export const useAccount = () => {
2
+ const pending = ref(false);
3
+ const error = ref(null);
4
+ async function withRequest(request, fallbackMessage) {
5
+ pending.value = true;
6
+ error.value = null;
7
+ try {
8
+ const result = await request();
9
+ await useUserSession().fetch();
10
+ return result;
11
+ } catch (err) {
12
+ error.value = err instanceof Error ? err.message : fallbackMessage;
13
+ throw err;
14
+ } finally {
15
+ pending.value = false;
16
+ }
17
+ }
18
+ function updateProfileDetails(payload) {
19
+ return withRequest(
20
+ () => $fetch("/api/account/profile", { method: "POST", body: payload }),
21
+ "Failed to update your profile."
22
+ );
23
+ }
24
+ function changeEmailAddress(payload) {
25
+ return withRequest(
26
+ () => $fetch("/api/account/change-email", { method: "POST", body: payload }),
27
+ "Failed to change your email address."
28
+ );
29
+ }
30
+ function changePassword(payload) {
31
+ return withRequest(
32
+ () => $fetch("/api/account/change-password", { method: "POST", body: payload }),
33
+ "Failed to change your password."
34
+ );
35
+ }
36
+ return { updateProfileDetails, changeEmailAddress, changePassword, pending, error };
37
+ };
@@ -0,0 +1,3 @@
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>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -0,0 +1,24 @@
1
+ <script setup>
2
+ defineOptions({ name: "AccountIndexPage" });
3
+ const { data: tiles } = useMenu("account");
4
+ </script>
5
+
6
+ <template>
7
+ <UContainer>
8
+ <UPage>
9
+ <UPageBody>
10
+ <UPageGrid>
11
+ <UPageCard
12
+ v-for="tile in tiles"
13
+ :key="tile.id"
14
+ :title="tile.label"
15
+ :description="tile.description"
16
+ :icon="tile.icon"
17
+ :to="tile.to"
18
+ orientation="horizontal"
19
+ />
20
+ </UPageGrid>
21
+ </UPageBody>
22
+ </UPage>
23
+ </UContainer>
24
+ </template>
@@ -0,0 +1,3 @@
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>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -0,0 +1,3 @@
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>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -0,0 +1,76 @@
1
+ <script setup>
2
+ defineOptions({ name: "AccountProfilePage" });
3
+ const t = useModuleT("account.profile");
4
+ const { user } = useUserSession();
5
+ const { updateProfileDetails, changeEmailAddress, pending, error } = useAccount();
6
+ const name = ref(user.value?.name ?? "");
7
+ const avatar = ref(user.value?.avatar ?? void 0);
8
+ const email = ref(user.value?.email ?? "");
9
+ watch(user, (newUser) => {
10
+ if (!newUser) return;
11
+ name.value = newUser.name;
12
+ avatar.value = newUser.avatar;
13
+ email.value = newUser.email;
14
+ });
15
+ async function onSubmitProfile() {
16
+ await updateProfileDetails({ name: name.value, avatar: avatar.value });
17
+ }
18
+ async function onSubmitEmail() {
19
+ await changeEmailAddress({ email: email.value });
20
+ }
21
+ </script>
22
+
23
+ <template>
24
+ <UContainer>
25
+ <UPage>
26
+ <UPageBody>
27
+ <UPageCard :title="t('title', 'Profile')">
28
+ <UFormField
29
+ :label="t('name.label', 'Name')"
30
+ name="name"
31
+ >
32
+ <UInput v-model="name" />
33
+ </UFormField>
34
+ <UFormField
35
+ :label="t('avatar.url', 'Avatar URL')"
36
+ name="avatar"
37
+ >
38
+ <UInput v-model="avatar" />
39
+ </UFormField>
40
+ <UButton
41
+ class="mt-4"
42
+ :loading="pending"
43
+ :label="t('save', 'Save')"
44
+ @click="onSubmitProfile"
45
+ />
46
+ </UPageCard>
47
+ <UPageCard
48
+ class="mt-4"
49
+ :title="t('email.label', 'Email address')"
50
+ >
51
+ <UFormField
52
+ :label="t('email.label', 'Email')"
53
+ name="email"
54
+ >
55
+ <UInput
56
+ v-model="email"
57
+ type="email"
58
+ />
59
+ </UFormField>
60
+ <UButton
61
+ class="mt-4"
62
+ :loading="pending"
63
+ :label="t('email.change.title', 'Change email')"
64
+ @click="onSubmitEmail"
65
+ />
66
+ </UPageCard>
67
+ <UAlert
68
+ v-if="error"
69
+ class="mt-4"
70
+ color="error"
71
+ :title="error"
72
+ />
73
+ </UPageBody>
74
+ </UPage>
75
+ </UContainer>
76
+ </template>
@@ -0,0 +1,3 @@
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>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -0,0 +1,3 @@
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>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -0,0 +1,91 @@
1
+ <script setup>
2
+ defineOptions({ name: "AccountSecurityPage" });
3
+ const t = useModuleT("account.security");
4
+ const { user } = useUserSession();
5
+ const { changePassword, pending, error } = useAccount();
6
+ const webAuthnEnabled = useRuntimeConfig().public.users?.webAuthn === true;
7
+ const passkeys = webAuthnEnabled ? useUserPasskeys() : void 0;
8
+ const currentPassword = ref("");
9
+ const newPassword = ref("");
10
+ const confirmNewPassword = ref("");
11
+ const passkeyPending = ref(false);
12
+ async function onSubmitPassword() {
13
+ await changePassword({
14
+ currentPassword: currentPassword.value,
15
+ newPassword: newPassword.value,
16
+ confirmNewPassword: confirmNewPassword.value
17
+ });
18
+ currentPassword.value = "";
19
+ newPassword.value = "";
20
+ confirmNewPassword.value = "";
21
+ }
22
+ async function onRegisterPasskey() {
23
+ passkeyPending.value = true;
24
+ try {
25
+ await passkeys?.registerPasskey(user.value?.email ?? "");
26
+ } finally {
27
+ passkeyPending.value = false;
28
+ }
29
+ }
30
+ </script>
31
+
32
+ <template>
33
+ <UContainer>
34
+ <UPage>
35
+ <UPageBody>
36
+ <UPageCard :title="t('changePassword.title', 'Change password')">
37
+ <UFormField
38
+ :label="t('changePassword.current.label', 'Current password')"
39
+ name="currentPassword"
40
+ >
41
+ <UInput
42
+ v-model="currentPassword"
43
+ type="password"
44
+ />
45
+ </UFormField>
46
+ <UFormField
47
+ :label="t('changePassword.new.label', 'New password')"
48
+ name="newPassword"
49
+ >
50
+ <UInput
51
+ v-model="newPassword"
52
+ type="password"
53
+ />
54
+ </UFormField>
55
+ <UFormField
56
+ :label="t('changePassword.confirm.label', 'Confirm new password')"
57
+ name="confirmNewPassword"
58
+ >
59
+ <UInput
60
+ v-model="confirmNewPassword"
61
+ type="password"
62
+ />
63
+ </UFormField>
64
+ <UButton
65
+ class="mt-4"
66
+ :loading="pending"
67
+ :label="t('changePassword.submit', 'Change password')"
68
+ @click="onSubmitPassword"
69
+ />
70
+ </UPageCard>
71
+ <UPageCard
72
+ v-if="passkeys?.isEnabled.value"
73
+ class="mt-4"
74
+ :title="t('passkeys.title', 'Passkeys')"
75
+ >
76
+ <UButton
77
+ :loading="passkeyPending"
78
+ :label="t('passkeys.register', 'Register a passkey')"
79
+ @click="onRegisterPasskey"
80
+ />
81
+ </UPageCard>
82
+ <UAlert
83
+ v-if="error"
84
+ class="mt-4"
85
+ color="error"
86
+ :title="error"
87
+ />
88
+ </UPageBody>
89
+ </UPage>
90
+ </UContainer>
91
+ </template>
@@ -0,0 +1,3 @@
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>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -0,0 +1,64 @@
1
+ {
2
+ "account": {
3
+ "title": "Konto",
4
+ "menu": {
5
+ "label": "Konto Menu"
6
+ },
7
+ "identificationCode": {
8
+ "title": "Frigear ID",
9
+ "description": "Tryk her for at vise dit ID."
10
+ },
11
+ "profile": {
12
+ "title": "Profil",
13
+ "description": "Skift dine brugeroplysninger.",
14
+ "name": {
15
+ "label": "Navn",
16
+ "description": "Dit fulde navn."
17
+ },
18
+ "email": {
19
+ "label": "E-mail",
20
+ "description": "Din awesome e-mail.",
21
+ "change": {
22
+ "title": "Skift din e-mail",
23
+ "label": "Ny e-mail"
24
+ }
25
+ },
26
+ "avatar": {
27
+ "label": "Avatar",
28
+ "description": "JPG, GIF eller PNG. 1MB Max.",
29
+ "url": "Avatar URL"
30
+ },
31
+ "save": "Gem profil"
32
+ },
33
+ "documents": {
34
+ "title": "Dokumenter",
35
+ "description": "Dine dokumenter.",
36
+ "download": "Download"
37
+ },
38
+ "security": {
39
+ "title": "Sikkerhed",
40
+ "description": "Skift password, Social login etc.",
41
+ "password": {
42
+ "title": "Password",
43
+ "description": "Skift dit password."
44
+ },
45
+ "changePassword": {
46
+ "title": "Skift password",
47
+ "submit": "Skift password",
48
+ "new": {
49
+ "label": "Nyt password"
50
+ },
51
+ "confirm": {
52
+ "label": "Bekræft nyt password"
53
+ },
54
+ "current": {
55
+ "label": "Nuværende password"
56
+ }
57
+ },
58
+ "passkeys": {
59
+ "title": "Passkeys",
60
+ "register": "Registrér en passkey"
61
+ }
62
+ }
63
+ }
64
+ }
@@ -0,0 +1,64 @@
1
+ {
2
+ "account": {
3
+ "title": "Account",
4
+ "menu": {
5
+ "label": "Dashboard Menu"
6
+ },
7
+ "identificationCode": {
8
+ "title": "Frigear ID",
9
+ "description": "Press here to show your ID."
10
+ },
11
+ "profile": {
12
+ "title": "Profile",
13
+ "description": "Change your information.",
14
+ "name": {
15
+ "label": "Name",
16
+ "description": "Your full name."
17
+ },
18
+ "email": {
19
+ "label": "E-mail",
20
+ "description": "Your awesome E-mail.",
21
+ "change": {
22
+ "title": "Change e-mail",
23
+ "label": "New e-mail"
24
+ }
25
+ },
26
+ "avatar": {
27
+ "label": "Avatar",
28
+ "description": "JPG, GIF or PNG. 1MB Max.",
29
+ "url": "Avatar URL"
30
+ },
31
+ "save": "Save profile"
32
+ },
33
+ "documents": {
34
+ "title": "Documents",
35
+ "description": "Your documents.",
36
+ "download": "Download"
37
+ },
38
+ "security": {
39
+ "title": "Security",
40
+ "description": "Change password, Social login etc.",
41
+ "password": {
42
+ "title": "Password",
43
+ "description": "Change your password."
44
+ },
45
+ "changePassword": {
46
+ "title": "Change password",
47
+ "submit": "Change password",
48
+ "new": {
49
+ "label": "New password"
50
+ },
51
+ "confirm": {
52
+ "label": "Confirm new password"
53
+ },
54
+ "current": {
55
+ "label": "Current password"
56
+ }
57
+ },
58
+ "passkeys": {
59
+ "title": "Passkeys",
60
+ "register": "Register a passkey"
61
+ }
62
+ }
63
+ }
64
+ }
@@ -0,0 +1,2 @@
1
+ declare const _default: import("@frigear-nu/menu/types").MenuSource;
2
+ export default _default;
@@ -0,0 +1,36 @@
1
+ import { defineMenuItem, defineMenuSource, extendMenuItem } from "@frigear-nu/menu/utils";
2
+ export default defineMenuSource(() => {
3
+ const config = useRuntimeConfig().public.account;
4
+ if (config?.pages?.index) {
5
+ extendMenuItem("account.dropdown", {
6
+ id: "account",
7
+ label: "Account",
8
+ description: "Your account.",
9
+ icon: "i-lucide-layout-dashboard",
10
+ to: config.pages.index,
11
+ order: -99
12
+ });
13
+ }
14
+ if (config?.pages?.profile) {
15
+ const profileItem = defineMenuItem({
16
+ id: "profile",
17
+ label: "Profile",
18
+ description: "Manage your profile details and email address.",
19
+ icon: "i-lucide-user-round-pen",
20
+ to: config.pages.profile,
21
+ order: 10
22
+ });
23
+ extendMenuItem("account", profileItem);
24
+ extendMenuItem("account.dropdown", profileItem);
25
+ }
26
+ if (config?.pages?.security) {
27
+ extendMenuItem("account", {
28
+ id: "security",
29
+ label: "Security",
30
+ description: "Change your password and manage passkeys.",
31
+ icon: "i-lucide-shield-user",
32
+ to: config.pages.security,
33
+ order: 20
34
+ });
35
+ }
36
+ });
@@ -0,0 +1,24 @@
1
+ export declare const users: import("drizzle-orm/sqlite-core").SQLiteTableWithColumns<{
2
+ name: "users";
3
+ schema: undefined;
4
+ columns: {
5
+ acceptedTermsAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
6
+ name: "accepted_terms_at";
7
+ tableName: "users";
8
+ dataType: "number";
9
+ columnType: "SQLiteInteger";
10
+ data: number;
11
+ driverParam: number;
12
+ notNull: false;
13
+ hasDefault: false;
14
+ isPrimaryKey: false;
15
+ isAutoincrement: false;
16
+ hasRuntimeDefault: false;
17
+ enumValues: undefined;
18
+ baseColumn: never;
19
+ identity: undefined;
20
+ generated: undefined;
21
+ }, {}, {}>;
22
+ };
23
+ dialect: "sqlite";
24
+ }>;
@@ -0,0 +1,4 @@
1
+ import { sqliteTable, integer } from "drizzle-orm/sqlite-core";
2
+ export const users = sqliteTable("users", {
3
+ acceptedTermsAt: integer("accepted_terms_at")
4
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: any;
2
+ export default _default;
@@ -0,0 +1,20 @@
1
+ import { db, schema } from "@nuxthub/db";
2
+ import { eq } from "drizzle-orm";
3
+ import { z } from "zod";
4
+ const bodySchema = z.object({
5
+ email: z.string().email()
6
+ });
7
+ export default defineEventHandler(async (event) => {
8
+ const { user } = await requireUserSession(event);
9
+ const { email } = await readValidatedBody(event, bodySchema.parse);
10
+ const existing = await db.query.users.findFirst({ where: (t, { eq: eq2 }) => eq2(t.email, email) });
11
+ if (existing && existing.id !== user.id) {
12
+ throw createError({ statusCode: 409, statusMessage: "A user with this email already exists." });
13
+ }
14
+ const [updated] = await db.update(schema.users).set({ email }).where(eq(schema.users.id, user.id)).returning();
15
+ if (!updated) {
16
+ throw createError({ statusCode: 404, statusMessage: "User not found." });
17
+ }
18
+ await setUserSession(event, { user: mapUserToSession(updated) });
19
+ return { id: updated.id };
20
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: any;
2
+ export default _default;
@@ -0,0 +1,26 @@
1
+ import { db, schema } from "@nuxthub/db";
2
+ import { eq } from "drizzle-orm";
3
+ import { z } from "zod";
4
+ const bodySchema = z.object({
5
+ currentPassword: z.string().min(1),
6
+ newPassword: z.string().min(8),
7
+ confirmNewPassword: z.string().min(8)
8
+ }).refine((data) => data.newPassword === data.confirmNewPassword, {
9
+ message: "Passwords do not match.",
10
+ path: ["confirmNewPassword"]
11
+ });
12
+ export default defineEventHandler(async (event) => {
13
+ const { user } = await requireUserSession(event);
14
+ const { currentPassword, newPassword } = await readValidatedBody(event, bodySchema.parse);
15
+ const dbUser = await db.query.users.findFirst({ where: (t, { eq: eq2 }) => eq2(t.id, user.id) });
16
+ if (!dbUser?.password || !await verifyPassword(dbUser.password, currentPassword)) {
17
+ throw createError({ statusCode: 401, statusMessage: "Your current password is incorrect." });
18
+ }
19
+ const hashed = await hashPassword(newPassword);
20
+ const [updated] = await db.update(schema.users).set({ password: hashed }).where(eq(schema.users.id, user.id)).returning();
21
+ if (!updated) {
22
+ throw createError({ statusCode: 404, statusMessage: "User not found." });
23
+ }
24
+ await setUserSession(event, { user: mapUserToSession(updated) });
25
+ return { id: updated.id };
26
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: any;
2
+ export default _default;
@@ -0,0 +1,17 @@
1
+ import { db, schema } from "@nuxthub/db";
2
+ import { eq } from "drizzle-orm";
3
+ import { z } from "zod";
4
+ const bodySchema = z.object({
5
+ name: z.string().min(1),
6
+ avatar: z.string().optional()
7
+ });
8
+ export default defineEventHandler(async (event) => {
9
+ const { user } = await requireUserSession(event);
10
+ const { name, avatar } = await readValidatedBody(event, bodySchema.parse);
11
+ const [updated] = await db.update(schema.users).set({ name, avatar }).where(eq(schema.users.id, user.id)).returning();
12
+ if (!updated) {
13
+ throw createError({ statusCode: 404, statusMessage: "User not found." });
14
+ }
15
+ await setUserSession(event, { user: mapUserToSession(updated) });
16
+ return { id: updated.id };
17
+ });
@@ -0,0 +1,3 @@
1
+ export { default } from './module.mjs'
2
+
3
+ export { type ModuleOptions, type PublicModuleOptions } from './module.mjs'
package/package.json CHANGED
@@ -1,6 +1,80 @@
1
1
  {
2
2
  "name": "@frigear-nu/account",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.0.1",
4
+ "description": "A module for managing the account area (profile, email, password, security) in the main frigear app.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "your-org/@frigear-nu/account"
8
+ },
9
+ "license": "MIT",
10
+ "type": "module",
11
+ "main": "./dist/module.mjs",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/types.d.mts",
15
+ "import": "./dist/module.mjs"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "workspaces": [
22
+ "playground"
23
+ ],
24
+ "dependencies": {
25
+ "@frigear-nu/users": "1.0.2",
26
+ "@libsql/client": "^0.18.0",
27
+ "@nuxt/kit": "^4.6.0",
28
+ "drizzle-kit": "^0.31.11",
29
+ "drizzle-orm": "^0.45.3",
30
+ "zod": "^4.6.5",
31
+ "@frigear-nu/menu": "0.0.2",
32
+ "@frigear-nu/module-kit": "0.1.0",
33
+ "@vueuse/core": "^15.0.0"
34
+ },
35
+ "peerDependencies": {
36
+ "@nuxthub/core": "^0.10.8",
37
+ "@nuxt/ui": "^4.10.0"
38
+ },
39
+ "peerDependenciesMeta": {
40
+ "@nuxt/ui": {
41
+ "optional": true
42
+ }
43
+ },
44
+ "devDependencies": {
45
+ "@nuxt/devtools": "^3.4.2",
46
+ "@nuxt/eslint-config": "^1.17.0",
47
+ "@nuxt/module-builder": "^1.0.3",
48
+ "@nuxt/schema": "^4.6.0",
49
+ "@nuxt/test-utils": "^4.3.2",
50
+ "@nuxthub/core": "^0.10.8",
51
+ "@types/node": "latest",
52
+ "changelogen": "^0.6.2",
53
+ "eslint": "^10.10.0",
54
+ "nuxt": "^4.6.0",
55
+ "typescript": "^6.0.3",
56
+ "vitest": "^5.0.1",
57
+ "vue-tsc": "^3.3.11",
58
+ "@nuxt/ui": "^4.11.0"
59
+ },
60
+ "scripts": {
61
+ "postinstall": "nuxt-module-build build --stub && nuxt prepare",
62
+ "build": "nuxt-module-build build",
63
+ "dev": "npm run dev:prepare && nuxt dev playground",
64
+ "dev:build": "nuxt build playground",
65
+ "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxt prepare playground",
66
+ "release": "npm run lint && npm run test && npm run prepack && changelogen --release && npm publish && git push --follow-tags",
67
+ "lint": "eslint .",
68
+ "lint:fix": "eslint . --fix",
69
+ "test": "vitest run",
70
+ "test:watch": "vitest watch",
71
+ "test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit"
72
+ },
73
+ "typesVersions": {
74
+ "*": {
75
+ ".": [
76
+ "./dist/types.d.mts"
77
+ ]
78
+ }
79
+ }
6
80
  }