@frigear-nu/account 0.0.0-stage → 0.0.2
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.
- package/README.md +70 -2
- package/dist/module.d.mts +31 -0
- package/dist/module.json +9 -0
- package/dist/module.mjs +88 -0
- package/dist/runtime/app/components/Account/AccountMenu.d.vue.ts +3 -0
- package/dist/runtime/app/components/Account/AccountMenu.vue +64 -0
- package/dist/runtime/app/components/Account/AccountMenu.vue.d.ts +3 -0
- package/dist/runtime/app/composables/index.d.ts +1 -0
- package/dist/runtime/app/composables/index.js +1 -0
- package/dist/runtime/app/composables/useAccount.d.ts +16 -0
- package/dist/runtime/app/composables/useAccount.js +37 -0
- package/dist/runtime/app/pages/index.d.vue.ts +3 -0
- package/dist/runtime/app/pages/index.vue +24 -0
- package/dist/runtime/app/pages/index.vue.d.ts +3 -0
- package/dist/runtime/app/pages/profile.d.vue.ts +3 -0
- package/dist/runtime/app/pages/profile.vue +76 -0
- package/dist/runtime/app/pages/profile.vue.d.ts +3 -0
- package/dist/runtime/app/pages/security.d.vue.ts +3 -0
- package/dist/runtime/app/pages/security.vue +91 -0
- package/dist/runtime/app/pages/security.vue.d.ts +3 -0
- package/dist/runtime/i18n/lang/da.json +64 -0
- package/dist/runtime/i18n/lang/en.json +64 -0
- package/dist/runtime/menus.d.ts +2 -0
- package/dist/runtime/menus.js +36 -0
- package/dist/runtime/server/db/schema.sqlite.d.ts +24 -0
- package/dist/runtime/server/db/schema.sqlite.js +4 -0
- package/dist/runtime/server/routes/account/change-email.post.d.ts +2 -0
- package/dist/runtime/server/routes/account/change-email.post.js +20 -0
- package/dist/runtime/server/routes/account/change-password.post.d.ts +2 -0
- package/dist/runtime/server/routes/account/change-password.post.js +26 -0
- package/dist/runtime/server/routes/account/profile.post.d.ts +2 -0
- package/dist/runtime/server/routes/account/profile.post.js +17 -0
- package/dist/types.d.mts +3 -0
- package/package.json +77 -3
package/README.md
CHANGED
|
@@ -1,3 +1,71 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @frigear-nu/account
|
|
2
2
|
|
|
3
|
-
|
|
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 };
|
package/dist/module.json
ADDED
package/dist/module.mjs
ADDED
|
@@ -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,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,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,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,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
|
+
});
|
package/dist/types.d.mts
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,80 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frigear-nu/account",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.0.2",
|
|
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.3",
|
|
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.3",
|
|
32
|
+
"@frigear-nu/module-kit": "0.1.1",
|
|
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
|
}
|