nuxt-users 1.6.6 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +65 -0
- package/dist/cli.mjs +1 -1
- package/dist/module.d.mts +21 -2
- package/dist/module.d.ts +21 -2
- package/dist/module.json +1 -1
- package/dist/module.mjs +19 -10
- package/dist/runtime/components/LogoutLink.vue +2 -2
- package/dist/runtime/components/ProfileInfo.vue +2 -2
- package/dist/runtime/composables/{useAuth.d.ts → useAuthentication.d.ts} +1 -1
- package/dist/runtime/composables/{useAuth.js → useAuthentication.js} +1 -1
- package/dist/runtime/constants.d.ts +1 -0
- package/dist/runtime/constants.js +1 -0
- package/dist/runtime/middleware/authorization.client.js +30 -0
- package/dist/runtime/server/api/auth/login.post.js +4 -2
- package/dist/runtime/server/middleware/{auth.server.js → authorization.server.js} +9 -4
- package/dist/runtime/server/services/password.js +35 -2
- package/dist/runtime/server/tasks/cleanup-tokens.d.ts +2 -0
- package/dist/runtime/server/tasks/cleanup-tokens.js +30 -0
- package/dist/runtime/server/utils/index.d.ts +1 -1
- package/dist/runtime/server/utils/index.js +1 -1
- package/dist/runtime/server/utils/user.d.ts +20 -0
- package/dist/runtime/server/utils/user.js +45 -1
- package/dist/runtime/utils/permissions.d.ts +22 -0
- package/dist/runtime/utils/permissions.js +36 -0
- package/package.json +11 -4
- package/dist/runtime/middleware/auth.client.js +0 -20
- /package/dist/runtime/middleware/{auth.client.d.ts → authorization.client.d.ts} +0 -0
- /package/dist/runtime/server/middleware/{auth.server.d.ts → authorization.server.d.ts} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,71 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
## v1.7.0
|
|
5
|
+
|
|
6
|
+
[compare changes](https://github.com/rrd108/nuxt-users/compare/v1.6.7...v1.7.0)
|
|
7
|
+
|
|
8
|
+
### 🚀 Enhancements
|
|
9
|
+
|
|
10
|
+
- Implement RBAC closes #55 ([#55](https://github.com/rrd108/nuxt-users/issues/55))
|
|
11
|
+
|
|
12
|
+
### 🩹 Fixes
|
|
13
|
+
|
|
14
|
+
- Type errors ([c403645](https://github.com/rrd108/nuxt-users/commit/c403645))
|
|
15
|
+
|
|
16
|
+
### 💅 Refactors
|
|
17
|
+
|
|
18
|
+
- Token cleanup task and middleware ([d47a4a5](https://github.com/rrd108/nuxt-users/commit/d47a4a5))
|
|
19
|
+
- Renames `useAuth` to `useAuthentication` ([e611e62](https://github.com/rrd108/nuxt-users/commit/e611e62))
|
|
20
|
+
|
|
21
|
+
### 🏡 Chore
|
|
22
|
+
|
|
23
|
+
- **release:** V1.6.7 ([def2d5c](https://github.com/rrd108/nuxt-users/commit/def2d5c))
|
|
24
|
+
- Bump cli version number ([b4daec6](https://github.com/rrd108/nuxt-users/commit/b4daec6))
|
|
25
|
+
- Fix for linter ([ce1db37](https://github.com/rrd108/nuxt-users/commit/ce1db37))
|
|
26
|
+
|
|
27
|
+
### ✅ Tests
|
|
28
|
+
|
|
29
|
+
- Remove unused test ([3dc23ad](https://github.com/rrd108/nuxt-users/commit/3dc23ad))
|
|
30
|
+
- Fix response ([db3b03c](https://github.com/rrd108/nuxt-users/commit/db3b03c))
|
|
31
|
+
- Fix mysql field type ([2faf0c6](https://github.com/rrd108/nuxt-users/commit/2faf0c6))
|
|
32
|
+
|
|
33
|
+
### ❤️ Contributors
|
|
34
|
+
|
|
35
|
+
- Rrd108 ([@rrd108](https://github.com/rrd108))
|
|
36
|
+
|
|
37
|
+
## v1.6.7
|
|
38
|
+
|
|
39
|
+
[compare changes](https://github.com/rrd108/nuxt-users/compare/v1.6.6...v1.6.7)
|
|
40
|
+
|
|
41
|
+
### 🩹 Fixes
|
|
42
|
+
|
|
43
|
+
- Authentication middleware logic ([18a3bb5](https://github.com/rrd108/nuxt-users/commit/18a3bb5))
|
|
44
|
+
- Password validation and UI ([07b44b3](https://github.com/rrd108/nuxt-users/commit/07b44b3))
|
|
45
|
+
- Type errors ([c403645](https://github.com/rrd108/nuxt-users/commit/c403645))
|
|
46
|
+
|
|
47
|
+
### 💅 Refactors
|
|
48
|
+
|
|
49
|
+
- API routes ([3c7603f](https://github.com/rrd108/nuxt-users/commit/3c7603f))
|
|
50
|
+
- Authentication components and logic ([4da544f](https://github.com/rrd108/nuxt-users/commit/4da544f))
|
|
51
|
+
- Token cleanup task and middleware ([d47a4a5](https://github.com/rrd108/nuxt-users/commit/d47a4a5))
|
|
52
|
+
|
|
53
|
+
### 🏡 Chore
|
|
54
|
+
|
|
55
|
+
- **release:** V1.6.6 ([7b37694](https://github.com/rrd108/nuxt-users/commit/7b37694))
|
|
56
|
+
|
|
57
|
+
### ✅ Tests
|
|
58
|
+
|
|
59
|
+
- Fix mock ([791dc94](https://github.com/rrd108/nuxt-users/commit/791dc94))
|
|
60
|
+
- Adds unit tests for auth API endpoints ([d844630](https://github.com/rrd108/nuxt-users/commit/d844630))
|
|
61
|
+
- Remove unused test ([3dc23ad](https://github.com/rrd108/nuxt-users/commit/3dc23ad))
|
|
62
|
+
- Fix response ([db3b03c](https://github.com/rrd108/nuxt-users/commit/db3b03c))
|
|
63
|
+
- Fix mysql field type ([2faf0c6](https://github.com/rrd108/nuxt-users/commit/2faf0c6))
|
|
64
|
+
|
|
65
|
+
### ❤️ Contributors
|
|
66
|
+
|
|
67
|
+
- Rrd108 ([@rrd108](https://github.com/rrd108))
|
|
68
|
+
|
|
4
69
|
## v1.6.6
|
|
5
70
|
|
|
6
71
|
[compare changes](https://github.com/rrd108/nuxt-users/compare/v1.6.5...v1.6.6)
|
package/dist/cli.mjs
CHANGED
package/dist/module.d.mts
CHANGED
|
@@ -41,6 +41,21 @@ interface RuntimeModuleOptions {
|
|
|
41
41
|
* @example ['/login', '/register']
|
|
42
42
|
*/
|
|
43
43
|
whitelist?: string[];
|
|
44
|
+
/**
|
|
45
|
+
* Token expiration time in minutes
|
|
46
|
+
* @default 1440
|
|
47
|
+
*/
|
|
48
|
+
tokenExpiration?: number;
|
|
49
|
+
/**
|
|
50
|
+
* Role-based permissions configuration
|
|
51
|
+
* @default {}
|
|
52
|
+
* @example {
|
|
53
|
+
* admin: ['*'], // admin can access everything
|
|
54
|
+
* user: ['/profile', '/api/user/profile'],
|
|
55
|
+
* moderator: ['/admin/*', '/api/admin/*']
|
|
56
|
+
* }
|
|
57
|
+
*/
|
|
58
|
+
permissions?: Record<string, string[]>;
|
|
44
59
|
};
|
|
45
60
|
/**
|
|
46
61
|
* Password validation configuration
|
|
@@ -78,7 +93,7 @@ interface RuntimeModuleOptions {
|
|
|
78
93
|
preventCommonPasswords?: boolean;
|
|
79
94
|
};
|
|
80
95
|
}
|
|
81
|
-
interface ModuleOptions
|
|
96
|
+
interface ModuleOptions {
|
|
82
97
|
connector: {
|
|
83
98
|
name: DatabaseType;
|
|
84
99
|
options: DatabaseConfig;
|
|
@@ -89,8 +104,12 @@ interface ModuleOptions extends Omit<RuntimeModuleOptions, 'tables' | 'auth' | '
|
|
|
89
104
|
personalAccessTokens: string;
|
|
90
105
|
passwordResetTokens: string;
|
|
91
106
|
};
|
|
107
|
+
mailer?: MailerOptions;
|
|
108
|
+
passwordResetBaseUrl?: string;
|
|
92
109
|
auth: {
|
|
93
110
|
whitelist: string[];
|
|
111
|
+
tokenExpiration: number;
|
|
112
|
+
permissions: Record<string, string[]>;
|
|
94
113
|
};
|
|
95
114
|
passwordValidation: {
|
|
96
115
|
minLength: number;
|
|
@@ -115,6 +134,6 @@ interface MailerOptions {
|
|
|
115
134
|
}
|
|
116
135
|
|
|
117
136
|
declare const defaultOptions: ModuleOptions;
|
|
118
|
-
declare const _default: _nuxt_schema.NuxtModule<
|
|
137
|
+
declare const _default: _nuxt_schema.NuxtModule<RuntimeModuleOptions, RuntimeModuleOptions, false>;
|
|
119
138
|
|
|
120
139
|
export { _default as default, defaultOptions };
|
package/dist/module.d.ts
CHANGED
|
@@ -41,6 +41,21 @@ interface RuntimeModuleOptions {
|
|
|
41
41
|
* @example ['/login', '/register']
|
|
42
42
|
*/
|
|
43
43
|
whitelist?: string[];
|
|
44
|
+
/**
|
|
45
|
+
* Token expiration time in minutes
|
|
46
|
+
* @default 1440
|
|
47
|
+
*/
|
|
48
|
+
tokenExpiration?: number;
|
|
49
|
+
/**
|
|
50
|
+
* Role-based permissions configuration
|
|
51
|
+
* @default {}
|
|
52
|
+
* @example {
|
|
53
|
+
* admin: ['*'], // admin can access everything
|
|
54
|
+
* user: ['/profile', '/api/user/profile'],
|
|
55
|
+
* moderator: ['/admin/*', '/api/admin/*']
|
|
56
|
+
* }
|
|
57
|
+
*/
|
|
58
|
+
permissions?: Record<string, string[]>;
|
|
44
59
|
};
|
|
45
60
|
/**
|
|
46
61
|
* Password validation configuration
|
|
@@ -78,7 +93,7 @@ interface RuntimeModuleOptions {
|
|
|
78
93
|
preventCommonPasswords?: boolean;
|
|
79
94
|
};
|
|
80
95
|
}
|
|
81
|
-
interface ModuleOptions
|
|
96
|
+
interface ModuleOptions {
|
|
82
97
|
connector: {
|
|
83
98
|
name: DatabaseType;
|
|
84
99
|
options: DatabaseConfig;
|
|
@@ -89,8 +104,12 @@ interface ModuleOptions extends Omit<RuntimeModuleOptions, 'tables' | 'auth' | '
|
|
|
89
104
|
personalAccessTokens: string;
|
|
90
105
|
passwordResetTokens: string;
|
|
91
106
|
};
|
|
107
|
+
mailer?: MailerOptions;
|
|
108
|
+
passwordResetBaseUrl?: string;
|
|
92
109
|
auth: {
|
|
93
110
|
whitelist: string[];
|
|
111
|
+
tokenExpiration: number;
|
|
112
|
+
permissions: Record<string, string[]>;
|
|
94
113
|
};
|
|
95
114
|
passwordValidation: {
|
|
96
115
|
minLength: number;
|
|
@@ -115,6 +134,6 @@ interface MailerOptions {
|
|
|
115
134
|
}
|
|
116
135
|
|
|
117
136
|
declare const defaultOptions: ModuleOptions;
|
|
118
|
-
declare const _default: _nuxt_schema.NuxtModule<
|
|
137
|
+
declare const _default: _nuxt_schema.NuxtModule<RuntimeModuleOptions, RuntimeModuleOptions, false>;
|
|
119
138
|
|
|
120
139
|
export { _default as default, defaultOptions };
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineNuxtModule, createResolver, addPlugin, addImportsDir,
|
|
1
|
+
import { defineNuxtModule, createResolver, addPlugin, addImportsDir, addRouteMiddleware, addServerHandler, addComponent } from '@nuxt/kit';
|
|
2
2
|
import { defu } from 'defu';
|
|
3
3
|
|
|
4
4
|
const defaultOptions = {
|
|
@@ -31,7 +31,11 @@ const defaultOptions = {
|
|
|
31
31
|
},
|
|
32
32
|
passwordResetBaseUrl: "http://localhost:3000",
|
|
33
33
|
auth: {
|
|
34
|
-
whitelist: [
|
|
34
|
+
whitelist: [],
|
|
35
|
+
tokenExpiration: 24 * 60,
|
|
36
|
+
// 24 hours
|
|
37
|
+
permissions: {}
|
|
38
|
+
// Empty by default - whitelist approach
|
|
35
39
|
},
|
|
36
40
|
passwordValidation: {
|
|
37
41
|
minLength: 8,
|
|
@@ -61,7 +65,9 @@ const module = defineNuxtModule({
|
|
|
61
65
|
passwordResetTokens: options.tables?.passwordResetTokens || defaultOptions.tables.passwordResetTokens
|
|
62
66
|
},
|
|
63
67
|
auth: {
|
|
64
|
-
whitelist: [...defaultOptions.auth?.whitelist || [], ...options.auth?.whitelist || []]
|
|
68
|
+
whitelist: [...defaultOptions.auth?.whitelist || [], ...options.auth?.whitelist || []],
|
|
69
|
+
tokenExpiration: options.auth?.tokenExpiration || defaultOptions.auth.tokenExpiration,
|
|
70
|
+
permissions: options.auth?.permissions || defaultOptions.auth.permissions
|
|
65
71
|
}
|
|
66
72
|
};
|
|
67
73
|
nuxt.options.runtimeConfig.public = nuxt.options.runtimeConfig.public || {};
|
|
@@ -75,7 +81,8 @@ const module = defineNuxtModule({
|
|
|
75
81
|
preventCommonPasswords: options.passwordValidation?.preventCommonPasswords ?? defaultOptions.passwordValidation.preventCommonPasswords
|
|
76
82
|
},
|
|
77
83
|
auth: {
|
|
78
|
-
whitelist: [...defaultOptions.auth?.whitelist || [], ...options.auth?.whitelist || []]
|
|
84
|
+
whitelist: [...defaultOptions.auth?.whitelist || [], ...options.auth?.whitelist || []],
|
|
85
|
+
permissions: options.auth?.permissions || defaultOptions.auth.permissions
|
|
79
86
|
}
|
|
80
87
|
};
|
|
81
88
|
addPlugin({
|
|
@@ -83,13 +90,9 @@ const module = defineNuxtModule({
|
|
|
83
90
|
mode: "server"
|
|
84
91
|
});
|
|
85
92
|
addImportsDir(resolver.resolve("./runtime/composables"));
|
|
86
|
-
addServerHandler({
|
|
87
|
-
middleware: true,
|
|
88
|
-
handler: resolver.resolve("./runtime/server/middleware/auth.server")
|
|
89
|
-
});
|
|
90
93
|
addRouteMiddleware({
|
|
91
|
-
name: "
|
|
92
|
-
path: resolver.resolve("./runtime/middleware/
|
|
94
|
+
name: "authorization.client",
|
|
95
|
+
path: resolver.resolve("./runtime/middleware/authorization.client"),
|
|
93
96
|
global: true
|
|
94
97
|
});
|
|
95
98
|
addServerHandler({
|
|
@@ -122,6 +125,12 @@ const module = defineNuxtModule({
|
|
|
122
125
|
method: "post",
|
|
123
126
|
handler: resolver.resolve("./runtime/server/api/auth/update-password.post")
|
|
124
127
|
});
|
|
128
|
+
nuxt.hook("nitro:config", async (nitroConfig) => {
|
|
129
|
+
nitroConfig.experimental = nitroConfig.experimental || {};
|
|
130
|
+
nitroConfig.experimental.tasks = true;
|
|
131
|
+
nitroConfig.scanDirs = nitroConfig.scanDirs || [];
|
|
132
|
+
nitroConfig.scanDirs.push(resolver.resolve("./runtime/server/tasks"));
|
|
133
|
+
});
|
|
125
134
|
addComponent({
|
|
126
135
|
name: "LoginForm",
|
|
127
136
|
filePath: resolver.resolve("./runtime/components/LoginForm.vue")
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { ref } from "vue";
|
|
3
3
|
import { navigateTo } from "#app";
|
|
4
|
-
import {
|
|
4
|
+
import { useAuthentication } from "../composables/useAuthentication";
|
|
5
5
|
const props = defineProps({
|
|
6
6
|
redirectTo: { type: String, required: false, default: "/login" },
|
|
7
7
|
linkText: { type: String, required: false, default: "Logout" },
|
|
@@ -16,7 +16,7 @@ const props = defineProps({
|
|
|
16
16
|
containerStyle: { type: [String, Object], required: false }
|
|
17
17
|
});
|
|
18
18
|
const emit = defineEmits(["success", "click", "error"]);
|
|
19
|
-
const { logout } =
|
|
19
|
+
const { logout } = useAuthentication();
|
|
20
20
|
const isLoading = ref(false);
|
|
21
21
|
const error = ref("");
|
|
22
22
|
const handleLogout = async (event) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useState } from "#app";
|
|
2
2
|
import { computed, readonly } from "vue";
|
|
3
|
-
export const
|
|
3
|
+
export const useAuthentication = () => {
|
|
4
4
|
const user = useState("user", () => null);
|
|
5
5
|
const isAuthenticated = computed(() => !!user.value);
|
|
6
6
|
const login = (userData) => {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const NO_AUTH_PATHS: string[];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const NO_AUTH_PATHS = ["/login", "/api/auth/login", "/api/auth/forgot-password"];
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { defineNuxtRouteMiddleware, navigateTo, useRuntimeConfig } from "#app";
|
|
2
|
+
import { useAuthentication } from "../composables/useAuthentication.js";
|
|
3
|
+
import { hasPermission, isWhitelisted } from "../utils/permissions.js";
|
|
4
|
+
import { NO_AUTH_PATHS } from "../constants.js";
|
|
5
|
+
export default defineNuxtRouteMiddleware((to) => {
|
|
6
|
+
const { public: { nuxtUsers: publicNuxtUsers } } = useRuntimeConfig();
|
|
7
|
+
const publicOptions = publicNuxtUsers;
|
|
8
|
+
if (NO_AUTH_PATHS.includes(to.path)) {
|
|
9
|
+
console.log(`[Nuxt Users] client.middleware.auth.global: ${to.path}`);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
if (isWhitelisted(to.path, publicOptions.auth.whitelist)) {
|
|
13
|
+
console.log(`[Nuxt Users] client.middleware.auth.global: Whitelisted: ${to.path}`);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const { isAuthenticated, user } = useAuthentication();
|
|
17
|
+
if (!isAuthenticated.value) {
|
|
18
|
+
console.log(`[Nuxt Users] client.middleware.auth.global: Unauthenticated ${to.path}, redirecting to /login`);
|
|
19
|
+
return navigateTo("/login");
|
|
20
|
+
}
|
|
21
|
+
if (!user.value || !hasPermission(user.value.role, to.path, publicOptions.auth.permissions)) {
|
|
22
|
+
console.log(`[Nuxt Users] client.middleware.auth.global: User with role ${user.value?.role} denied access to ${to.path}`);
|
|
23
|
+
return navigateTo("/login");
|
|
24
|
+
}
|
|
25
|
+
console.log("[Nuxt Users] client.middleware.auth.global", {
|
|
26
|
+
isAuthenticated: isAuthenticated.value,
|
|
27
|
+
userRole: user.value?.role,
|
|
28
|
+
to: to.path
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -35,9 +35,11 @@ export default defineEventHandler(async (event) => {
|
|
|
35
35
|
}
|
|
36
36
|
const token = crypto.randomBytes(64).toString("hex");
|
|
37
37
|
const tokenName = "auth_token";
|
|
38
|
+
const expiresAt = /* @__PURE__ */ new Date();
|
|
39
|
+
expiresAt.setMinutes(expiresAt.getMinutes() + options.auth.tokenExpiration);
|
|
38
40
|
await db.sql`
|
|
39
|
-
INSERT INTO {${personalAccessTokensTable}} (tokenable_type, tokenable_id, name, token, created_at, updated_at)
|
|
40
|
-
VALUES ('user', ${user.id}, ${tokenName}, ${token}, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
41
|
+
INSERT INTO {${personalAccessTokensTable}} (tokenable_type, tokenable_id, name, token, expires_at, created_at, updated_at)
|
|
42
|
+
VALUES ('user', ${user.id}, ${tokenName}, ${token}, ${expiresAt.toISOString().slice(0, 19).replace("T", " ")}, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
41
43
|
`;
|
|
42
44
|
setCookie(event, "auth_token", token, {
|
|
43
45
|
httpOnly: true,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { defineEventHandler, getCookie, sendRedirect } from "h3";
|
|
2
2
|
import { useRuntimeConfig } from "#imports";
|
|
3
3
|
import { getCurrentUserFromToken } from "../utils/index.js";
|
|
4
|
-
|
|
4
|
+
import { hasPermission, isWhitelisted } from "../../utils/permissions.js";
|
|
5
|
+
import { NO_AUTH_PATHS } from "../../constants.js";
|
|
5
6
|
export default defineEventHandler(async (event) => {
|
|
6
7
|
const { nuxtUsers } = useRuntimeConfig();
|
|
7
8
|
const options = nuxtUsers;
|
|
@@ -9,11 +10,11 @@ export default defineEventHandler(async (event) => {
|
|
|
9
10
|
if (!isPageOrApiRoute) {
|
|
10
11
|
return;
|
|
11
12
|
}
|
|
12
|
-
if (
|
|
13
|
+
if (NO_AUTH_PATHS.includes(event.path)) {
|
|
13
14
|
console.log("[Nuxt Users] server.middleware.auth.global: /login");
|
|
14
15
|
return;
|
|
15
16
|
}
|
|
16
|
-
if (options.auth.whitelist
|
|
17
|
+
if (isWhitelisted(event.path, options.auth.whitelist)) {
|
|
17
18
|
console.log(`[Nuxt Users] server.middleware.auth.global: whitelisted: ${event.path}`);
|
|
18
19
|
return;
|
|
19
20
|
}
|
|
@@ -27,6 +28,10 @@ export default defineEventHandler(async (event) => {
|
|
|
27
28
|
console.log(`[Nuxt Users] server.middleware.auth.global: ${event.path} No user found redirecting to login`);
|
|
28
29
|
return sendRedirect(event, "/login");
|
|
29
30
|
}
|
|
30
|
-
|
|
31
|
+
if (!hasPermission(user.role, event.path, options.auth.permissions)) {
|
|
32
|
+
console.log(`[Nuxt Users] server.middleware.auth.global: ${event.path} User ${user.id} with role ${user.role} denied access`);
|
|
33
|
+
return sendRedirect(event, "/login");
|
|
34
|
+
}
|
|
35
|
+
console.log(`[Nuxt Users] server.middleware.auth.global: Authenticated request to ${event.path} for ${user.id} with role ${user.role}`);
|
|
31
36
|
return;
|
|
32
37
|
});
|
|
@@ -30,13 +30,46 @@ export const sendPasswordResetLink = async (email, options) => {
|
|
|
30
30
|
pass: options.mailer.auth.pass
|
|
31
31
|
}
|
|
32
32
|
});
|
|
33
|
+
const resetUrl = new URL("/reset-password", options.passwordResetBaseUrl || "http://localhost:3000");
|
|
34
|
+
resetUrl.searchParams.set("token", token);
|
|
35
|
+
resetUrl.searchParams.set("email", email);
|
|
36
|
+
const resetLink = resetUrl.toString();
|
|
33
37
|
try {
|
|
34
38
|
await transporter.sendMail({
|
|
35
39
|
from: options.mailer.defaults?.from || '"Nuxt Users" <noreply@example.com>',
|
|
36
40
|
to: email,
|
|
37
41
|
subject: "Password Reset Request",
|
|
38
|
-
text: `Please click the following link to reset your password: ${
|
|
39
|
-
|
|
42
|
+
text: `Please click the following link to reset your password: ${resetLink}
|
|
43
|
+
|
|
44
|
+
This link will expire in ${TOKEN_EXPIRATION_HOURS} hour(s).
|
|
45
|
+
|
|
46
|
+
If you did not request this password reset, please ignore this email.`,
|
|
47
|
+
html: `
|
|
48
|
+
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
|
49
|
+
<h2>Password Reset Request</h2>
|
|
50
|
+
<p>You have requested to reset your password. Please click the button below to set a new password:</p>
|
|
51
|
+
|
|
52
|
+
<div style="text-align: center; margin: 30px 0;">
|
|
53
|
+
<a href="${resetLink}"
|
|
54
|
+
style="background-color: #007bff; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px; display: inline-block;">
|
|
55
|
+
Reset Password
|
|
56
|
+
</a>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<p>Or copy and paste the following link into your browser:</p>
|
|
60
|
+
<p style="word-break: break-all; background-color: #f8f9fa; padding: 10px; border-radius: 3px;">
|
|
61
|
+
<a href="${resetLink}">${resetLink}</a>
|
|
62
|
+
</p>
|
|
63
|
+
|
|
64
|
+
<p style="color: #6c757d; font-size: 14px;">
|
|
65
|
+
This link will expire in ${TOKEN_EXPIRATION_HOURS} hour(s).
|
|
66
|
+
</p>
|
|
67
|
+
|
|
68
|
+
<p style="color: #6c757d; font-size: 14px;">
|
|
69
|
+
If you did not request this password reset, please ignore this email. Your password will remain unchanged.
|
|
70
|
+
</p>
|
|
71
|
+
</div>
|
|
72
|
+
`
|
|
40
73
|
});
|
|
41
74
|
console.log(`[Nuxt Users] Password reset email sent to ${email}`);
|
|
42
75
|
} catch (error) {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { defineTask } from "nitropack/runtime/task";
|
|
2
|
+
export default defineTask({
|
|
3
|
+
meta: {
|
|
4
|
+
name: "nuxt-users:cleanup-tokens",
|
|
5
|
+
description: "Clean up expired personal access tokens and tokens without expiration"
|
|
6
|
+
},
|
|
7
|
+
async run({ payload }) {
|
|
8
|
+
const { useRuntimeConfig } = await import("#imports");
|
|
9
|
+
const { cleanupPersonalAccessTokens } = await import("../utils/index.js");
|
|
10
|
+
const { nuxtUsers } = useRuntimeConfig();
|
|
11
|
+
const options = nuxtUsers;
|
|
12
|
+
if (!options) {
|
|
13
|
+
throw new Error("Nuxt Users module not configured");
|
|
14
|
+
}
|
|
15
|
+
const includeNoExpiration = payload?.includeNoExpiration ?? true;
|
|
16
|
+
try {
|
|
17
|
+
const result = await cleanupPersonalAccessTokens(options, includeNoExpiration);
|
|
18
|
+
return {
|
|
19
|
+
result: "success",
|
|
20
|
+
expiredTokensRemoved: result.expiredCount,
|
|
21
|
+
noExpirationTokensRemoved: result.noExpirationCount,
|
|
22
|
+
totalTokensCleaned: result.totalCount,
|
|
23
|
+
includeNoExpiration
|
|
24
|
+
};
|
|
25
|
+
} catch (error) {
|
|
26
|
+
console.error("[Nuxt Users] Token cleanup failed:", error);
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { useDb, checkTableExists } from './db.js';
|
|
2
|
-
export { createUser, findUserByEmail, updateUserPassword, getCurrentUserFromToken, hasAnyUsers } from './user.js';
|
|
2
|
+
export { createUser, findUserByEmail, updateUserPassword, getCurrentUserFromToken, hasAnyUsers, deleteExpiredPersonalAccessTokens, deleteTokensWithoutExpiration, cleanupPersonalAccessTokens, revokeUserTokens } from './user.js';
|
|
3
3
|
export { createUsersTable } from './create-users-table.js';
|
|
4
4
|
export { createPersonalAccessTokensTable } from './create-personal-access-tokens-table.js';
|
|
5
5
|
export { createPasswordResetTokensTable } from './create-password-reset-tokens-table.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { useDb, checkTableExists } from "./db.js";
|
|
2
|
-
export { createUser, findUserByEmail, updateUserPassword, getCurrentUserFromToken, hasAnyUsers } from "./user.js";
|
|
2
|
+
export { createUser, findUserByEmail, updateUserPassword, getCurrentUserFromToken, hasAnyUsers, deleteExpiredPersonalAccessTokens, deleteTokensWithoutExpiration, cleanupPersonalAccessTokens, revokeUserTokens } from "./user.js";
|
|
3
3
|
export { createUsersTable } from "./create-users-table.js";
|
|
4
4
|
export { createPersonalAccessTokensTable } from "./create-personal-access-tokens-table.js";
|
|
5
5
|
export { createPasswordResetTokensTable } from "./create-password-reset-tokens-table.js";
|
|
@@ -24,4 +24,24 @@ export declare const hasAnyUsers: (options: ModuleOptions) => Promise<boolean>;
|
|
|
24
24
|
* Gets the current user from an authentication token.
|
|
25
25
|
*/
|
|
26
26
|
export declare const getCurrentUserFromToken: <T extends boolean = false>(token: string, options: ModuleOptions, withPass?: T) => Promise<T extends true ? User | null : UserWithoutPassword | null>;
|
|
27
|
+
/**
|
|
28
|
+
* Deletes expired personal access tokens from the database.
|
|
29
|
+
*/
|
|
30
|
+
export declare const deleteExpiredPersonalAccessTokens: (options: ModuleOptions) => Promise<number>;
|
|
31
|
+
/**
|
|
32
|
+
* Deletes tokens without expiration dates (legacy tokens or security cleanup).
|
|
33
|
+
*/
|
|
34
|
+
export declare const deleteTokensWithoutExpiration: (options: ModuleOptions) => Promise<number>;
|
|
35
|
+
/**
|
|
36
|
+
* Comprehensive token cleanup: removes expired tokens and optionally tokens without expiration.
|
|
37
|
+
*/
|
|
38
|
+
export declare const cleanupPersonalAccessTokens: (options: ModuleOptions, includeNoExpiration?: boolean) => Promise<{
|
|
39
|
+
expiredCount: number;
|
|
40
|
+
noExpirationCount: number;
|
|
41
|
+
totalCount: number;
|
|
42
|
+
}>;
|
|
43
|
+
/**
|
|
44
|
+
* Revokes all tokens for a specific user.
|
|
45
|
+
*/
|
|
46
|
+
export declare const revokeUserTokens: (userId: number, options: ModuleOptions) => Promise<void>;
|
|
27
47
|
export {};
|
|
@@ -76,13 +76,19 @@ export const getCurrentUserFromToken = async (token, options, withPass = false)
|
|
|
76
76
|
const personalAccessTokensTable = options.tables.personalAccessTokens;
|
|
77
77
|
const usersTable = options.tables.users;
|
|
78
78
|
const tokenResult = await db.sql`
|
|
79
|
-
SELECT tokenable_id FROM {${personalAccessTokensTable}}
|
|
79
|
+
SELECT tokenable_id, expires_at FROM {${personalAccessTokensTable}}
|
|
80
80
|
WHERE token = ${token}
|
|
81
|
+
AND (expires_at IS NULL OR expires_at > CURRENT_TIMESTAMP)
|
|
81
82
|
`;
|
|
82
83
|
if (tokenResult.rows.length === 0) {
|
|
83
84
|
return null;
|
|
84
85
|
}
|
|
85
86
|
const userId = tokenResult.rows[0].tokenable_id;
|
|
87
|
+
await db.sql`
|
|
88
|
+
UPDATE {${personalAccessTokensTable}}
|
|
89
|
+
SET last_used_at = CURRENT_TIMESTAMP
|
|
90
|
+
WHERE token = ${token}
|
|
91
|
+
`;
|
|
86
92
|
const userResult = await db.sql`
|
|
87
93
|
SELECT * FROM {${usersTable}}
|
|
88
94
|
WHERE id = ${userId}
|
|
@@ -97,3 +103,41 @@ export const getCurrentUserFromToken = async (token, options, withPass = false)
|
|
|
97
103
|
const { password: _, ...userWithoutPassword } = user;
|
|
98
104
|
return userWithoutPassword;
|
|
99
105
|
};
|
|
106
|
+
export const deleteExpiredPersonalAccessTokens = async (options) => {
|
|
107
|
+
const db = await useDb(options);
|
|
108
|
+
const personalAccessTokensTable = options.tables.personalAccessTokens;
|
|
109
|
+
const result = await db.sql`
|
|
110
|
+
DELETE FROM {${personalAccessTokensTable}}
|
|
111
|
+
WHERE expires_at IS NOT NULL AND expires_at <= CURRENT_TIMESTAMP
|
|
112
|
+
`;
|
|
113
|
+
const deletedCount = result.rows?.length || 0;
|
|
114
|
+
console.log(`[Nuxt Users] ${deletedCount} expired personal access tokens deleted.`);
|
|
115
|
+
return deletedCount;
|
|
116
|
+
};
|
|
117
|
+
export const deleteTokensWithoutExpiration = async (options) => {
|
|
118
|
+
const db = await useDb(options);
|
|
119
|
+
const personalAccessTokensTable = options.tables.personalAccessTokens;
|
|
120
|
+
const result = await db.sql`
|
|
121
|
+
DELETE FROM {${personalAccessTokensTable}}
|
|
122
|
+
WHERE expires_at IS NULL
|
|
123
|
+
`;
|
|
124
|
+
const deletedCount = result.rows?.length || 0;
|
|
125
|
+
console.log(`[Nuxt Users] ${deletedCount} tokens without expiration deleted.`);
|
|
126
|
+
return deletedCount;
|
|
127
|
+
};
|
|
128
|
+
export const cleanupPersonalAccessTokens = async (options, includeNoExpiration = true) => {
|
|
129
|
+
const expiredCount = await deleteExpiredPersonalAccessTokens(options);
|
|
130
|
+
const noExpirationCount = includeNoExpiration ? await deleteTokensWithoutExpiration(options) : 0;
|
|
131
|
+
const totalCount = expiredCount + noExpirationCount;
|
|
132
|
+
console.log(`[Nuxt Users] Token cleanup completed: ${expiredCount} expired + ${noExpirationCount} without expiration = ${totalCount} total tokens removed.`);
|
|
133
|
+
return { expiredCount, noExpirationCount, totalCount };
|
|
134
|
+
};
|
|
135
|
+
export const revokeUserTokens = async (userId, options) => {
|
|
136
|
+
const db = await useDb(options);
|
|
137
|
+
const personalAccessTokensTable = options.tables.personalAccessTokens;
|
|
138
|
+
await db.sql`
|
|
139
|
+
DELETE FROM {${personalAccessTokensTable}}
|
|
140
|
+
WHERE tokenable_type = 'user' AND tokenable_id = ${userId}
|
|
141
|
+
`;
|
|
142
|
+
console.log(`[Nuxt Users] All tokens revoked for user ID: ${userId}`);
|
|
143
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks if a path matches a pattern (supports wildcards)
|
|
3
|
+
* @param path - The path to check
|
|
4
|
+
* @param pattern - The pattern to match against (supports * wildcard)
|
|
5
|
+
* @returns true if the path matches the pattern
|
|
6
|
+
*/
|
|
7
|
+
export declare const pathMatchesPattern: (path: string, pattern: string) => boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Checks if a user has permission to access a specific path
|
|
10
|
+
* @param userRole - The user's role
|
|
11
|
+
* @param path - The path being accessed
|
|
12
|
+
* @param permissions - The permissions configuration
|
|
13
|
+
* @returns true if the user has permission to access the path
|
|
14
|
+
*/
|
|
15
|
+
export declare const hasPermission: (userRole: string, path: string, permissions: Record<string, string[]>) => boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Checks if a path is accessible without authentication
|
|
18
|
+
* @param path - The path being accessed
|
|
19
|
+
* @param whitelist - Array of whitelisted paths
|
|
20
|
+
* @returns true if the path is whitelisted
|
|
21
|
+
*/
|
|
22
|
+
export declare const isWhitelisted: (path: string, whitelist: string[]) => boolean;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export const pathMatchesPattern = (path, pattern) => {
|
|
2
|
+
if (path === pattern) {
|
|
3
|
+
return true;
|
|
4
|
+
}
|
|
5
|
+
if (pattern === "*") {
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
if ((pattern.match(/\*/g) || []).length > 1) {
|
|
9
|
+
const regexPattern = pattern.replace(/\*/g, "[^/]*");
|
|
10
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
11
|
+
return regex.test(path);
|
|
12
|
+
}
|
|
13
|
+
if (pattern.endsWith("/*")) {
|
|
14
|
+
const basePattern = pattern.slice(0, -2);
|
|
15
|
+
return path === basePattern || path.startsWith(basePattern + "/");
|
|
16
|
+
}
|
|
17
|
+
if (pattern.includes("*")) {
|
|
18
|
+
const regexPattern = pattern.replace(/\*/g, "[^/]*");
|
|
19
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
20
|
+
return regex.test(path);
|
|
21
|
+
}
|
|
22
|
+
return false;
|
|
23
|
+
};
|
|
24
|
+
export const hasPermission = (userRole, path, permissions) => {
|
|
25
|
+
if (!permissions || Object.keys(permissions).length === 0) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
const rolePermissions = permissions[userRole];
|
|
29
|
+
if (!rolePermissions) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
return rolePermissions.some((pattern) => pathMatchesPattern(path, pattern));
|
|
33
|
+
};
|
|
34
|
+
export const isWhitelisted = (path, whitelist) => {
|
|
35
|
+
return whitelist.some((whitelistedPath) => pathMatchesPattern(path, whitelistedPath));
|
|
36
|
+
};
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxt-users",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "A comprehensive user management module for Nuxt 3 applications with authentication, database support, and CLI tools",
|
|
3
|
+
"version": "1.7.0",
|
|
4
|
+
"description": "A comprehensive user management module for Nuxt 3 and Nuxt 4 applications with authentication, authorization, database support, and CLI tools",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"nuxt",
|
|
7
7
|
"nuxt3",
|
|
8
8
|
"nuxt-module",
|
|
9
9
|
"users",
|
|
10
10
|
"authentication",
|
|
11
|
+
"authorization",
|
|
11
12
|
"auth",
|
|
12
13
|
"user-management",
|
|
13
14
|
"sqlite",
|
|
@@ -82,7 +83,7 @@
|
|
|
82
83
|
"docs:dev": "vitepress dev docs",
|
|
83
84
|
"docs:build": "vitepress build docs",
|
|
84
85
|
"docs:preview": "vitepress preview docs",
|
|
85
|
-
"docs:
|
|
86
|
+
"docs:release": "yarn docs:build && ./scripts/deploy-docs.sh"
|
|
86
87
|
},
|
|
87
88
|
"dependencies": {
|
|
88
89
|
"bcrypt": "^6.0.0",
|
|
@@ -98,7 +99,13 @@
|
|
|
98
99
|
"peerDependencies": {
|
|
99
100
|
"@nuxt/kit": "^3.17.6",
|
|
100
101
|
"bcrypt": "^6.0.0",
|
|
101
|
-
"nodemailer": "^7.0.5"
|
|
102
|
+
"nodemailer": "^7.0.5",
|
|
103
|
+
"nuxt-api-shield": "^0.1.0 || ^0.8.0"
|
|
104
|
+
},
|
|
105
|
+
"peerDependenciesMeta": {
|
|
106
|
+
"nuxt-api-shield": {
|
|
107
|
+
"optional": true
|
|
108
|
+
}
|
|
102
109
|
},
|
|
103
110
|
"devDependencies": {
|
|
104
111
|
"@nuxt/devtools": "^2.6.2",
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { defineNuxtRouteMiddleware, navigateTo, useRuntimeConfig } from "#app";
|
|
2
|
-
import { useAuth } from "../composables/useAuth.js";
|
|
3
|
-
export default defineNuxtRouteMiddleware((to) => {
|
|
4
|
-
const { public: { nuxtUsers: publicNuxtUsers } } = useRuntimeConfig();
|
|
5
|
-
const publicOptions = publicNuxtUsers;
|
|
6
|
-
if (to.path === "/login") {
|
|
7
|
-
console.log("[Nuxt Users] client.middleware.auth.global: /login");
|
|
8
|
-
return;
|
|
9
|
-
}
|
|
10
|
-
if (publicOptions.auth.whitelist.includes(to.path)) {
|
|
11
|
-
console.log(`[Nuxt Users] client.middleware.auth.global: Whitelisted: ${to.path}`);
|
|
12
|
-
return;
|
|
13
|
-
}
|
|
14
|
-
const { isAuthenticated } = useAuth();
|
|
15
|
-
if (!isAuthenticated.value) {
|
|
16
|
-
console.log(`[Nuxt Users] client.middleware.auth.global: Unauthenticated ${to.path}, redirecting to /login`);
|
|
17
|
-
return navigateTo("/login");
|
|
18
|
-
}
|
|
19
|
-
console.log("[Nuxt Users] client.middleware.auth.global", { isAuthenticated: isAuthenticated.value, to: to.path });
|
|
20
|
-
});
|
|
File without changes
|
|
File without changes
|