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.
Files changed (28) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/dist/cli.mjs +1 -1
  3. package/dist/module.d.mts +21 -2
  4. package/dist/module.d.ts +21 -2
  5. package/dist/module.json +1 -1
  6. package/dist/module.mjs +19 -10
  7. package/dist/runtime/components/LogoutLink.vue +2 -2
  8. package/dist/runtime/components/ProfileInfo.vue +2 -2
  9. package/dist/runtime/composables/{useAuth.d.ts → useAuthentication.d.ts} +1 -1
  10. package/dist/runtime/composables/{useAuth.js → useAuthentication.js} +1 -1
  11. package/dist/runtime/constants.d.ts +1 -0
  12. package/dist/runtime/constants.js +1 -0
  13. package/dist/runtime/middleware/authorization.client.js +30 -0
  14. package/dist/runtime/server/api/auth/login.post.js +4 -2
  15. package/dist/runtime/server/middleware/{auth.server.js → authorization.server.js} +9 -4
  16. package/dist/runtime/server/services/password.js +35 -2
  17. package/dist/runtime/server/tasks/cleanup-tokens.d.ts +2 -0
  18. package/dist/runtime/server/tasks/cleanup-tokens.js +30 -0
  19. package/dist/runtime/server/utils/index.d.ts +1 -1
  20. package/dist/runtime/server/utils/index.js +1 -1
  21. package/dist/runtime/server/utils/user.d.ts +20 -0
  22. package/dist/runtime/server/utils/user.js +45 -1
  23. package/dist/runtime/utils/permissions.d.ts +22 -0
  24. package/dist/runtime/utils/permissions.js +36 -0
  25. package/package.json +11 -4
  26. package/dist/runtime/middleware/auth.client.js +0 -20
  27. /package/dist/runtime/middleware/{auth.client.d.ts → authorization.client.d.ts} +0 -0
  28. /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
@@ -247,7 +247,7 @@ const main = defineCommand({
247
247
  meta: {
248
248
  name: "nuxt-users",
249
249
  description: "CLI for Nuxt Users Module - Manage users, migrations, and database operations",
250
- version: "1.6.5"
250
+ version: "1.6.7"
251
251
  },
252
252
  subCommands: {
253
253
  migrate,
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 extends Omit<RuntimeModuleOptions, 'tables' | 'auth' | 'passwordValidation'> {
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<ModuleOptions, ModuleOptions, false>;
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 extends Omit<RuntimeModuleOptions, 'tables' | 'auth' | 'passwordValidation'> {
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<ModuleOptions, ModuleOptions, false>;
137
+ declare const _default: _nuxt_schema.NuxtModule<RuntimeModuleOptions, RuntimeModuleOptions, false>;
119
138
 
120
139
  export { _default as default, defaultOptions };
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-users",
3
3
  "configKey": "nuxtUsers",
4
- "version": "1.6.6",
4
+ "version": "1.7.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.1",
7
7
  "unbuild": "3.6.0"
package/dist/module.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { defineNuxtModule, createResolver, addPlugin, addImportsDir, addServerHandler, addRouteMiddleware, addComponent } from '@nuxt/kit';
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: ["/login"]
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: "auth.client",
92
- path: resolver.resolve("./runtime/middleware/auth.client"),
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 { useAuth } from "../composables/useAuth";
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 } = useAuth();
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
  <script setup>
2
- import { useAuth } from "../composables/useAuth";
3
- const { user } = useAuth();
2
+ import { useAuthentication } from "../composables/useAuthentication";
3
+ const { user } = useAuthentication();
4
4
  </script>
5
5
 
6
6
  <template>
@@ -1,5 +1,5 @@
1
1
  import type { User, UserWithoutPassword } from '../../types.js';
2
- export declare const useAuth: () => {
2
+ export declare const useAuthentication: () => {
3
3
  user: Readonly<import("vue").Ref<{
4
4
  readonly id: number;
5
5
  readonly email: string;
@@ -1,6 +1,6 @@
1
1
  import { useState } from "#app";
2
2
  import { computed, readonly } from "vue";
3
- export const useAuth = () => {
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
- const noAuthPaths = ["/login", "/api/auth/login", "/api/auth/forgot-password"];
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 (noAuthPaths.includes(event.path)) {
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.includes(event.path)) {
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
- console.log(`[Nuxt Users] server.middleware.auth.global: Authenticated request to ${event.path} for ${user.id}`);
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: ${options.passwordResetBaseUrl}`,
39
- html: `<p>Please click the following link to reset your password:</p><p><a href="${options.passwordResetBaseUrl}">${options.passwordResetBaseUrl}</a></p><p>This link will expire in ${TOKEN_EXPIRATION_HOURS} hour(s).</p>`
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,2 @@
1
+ declare const _default: import("nitropack").Task<"success">;
2
+ export default _default;
@@ -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.6.6",
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:deploy": "yarn docs:build && ./scripts/deploy-docs.sh"
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
- });