nuxt-users 1.6.5 → 1.6.7

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 (39) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/dist/cli.mjs +1 -1
  3. package/dist/module.d.mts +4 -70
  4. package/dist/module.d.ts +4 -70
  5. package/dist/module.json +1 -1
  6. package/dist/module.mjs +28 -27
  7. package/dist/runtime/components/LoginForm.vue +6 -3
  8. package/dist/runtime/components/LoginForm.vue.d.ts +4 -4
  9. package/dist/runtime/components/LogoutLink.vue +21 -7
  10. package/dist/runtime/components/LogoutLink.vue.d.ts +22 -5
  11. package/dist/runtime/components/PasswordStrengthIndicator.vue +38 -27
  12. package/dist/runtime/components/ProfileInfo.vue +33 -0
  13. package/dist/runtime/components/ResetPasswordForm.vue +89 -96
  14. package/dist/runtime/components/ResetPasswordForm.vue.d.ts +15 -1
  15. package/dist/runtime/composables/useAuth.d.ts +2 -2
  16. package/dist/runtime/composables/useAuth.js +2 -2
  17. package/dist/runtime/composables/usePasswordValidation.d.ts +1 -1
  18. package/dist/runtime/middleware/auth.client.js +4 -4
  19. package/dist/runtime/server/api/{login.post.js → auth/login.post.js} +5 -3
  20. package/dist/runtime/server/api/{logout.get.js → auth/logout.get.js} +1 -1
  21. package/dist/runtime/server/api/{update-password.post.js → auth/update-password.post.js} +2 -2
  22. package/dist/runtime/server/api/user/profile.get.d.ts +4 -0
  23. package/dist/runtime/server/api/{profile.get.js → user/profile.get.js} +2 -2
  24. package/dist/runtime/server/middleware/auth.server.js +3 -2
  25. package/dist/runtime/server/tasks/cleanup-tokens.d.ts +2 -0
  26. package/dist/runtime/server/tasks/cleanup-tokens.js +30 -0
  27. package/dist/runtime/server/utils/index.d.ts +1 -1
  28. package/dist/runtime/server/utils/index.js +1 -1
  29. package/dist/runtime/server/utils/user.d.ts +20 -0
  30. package/dist/runtime/server/utils/user.js +45 -1
  31. package/package.json +1 -1
  32. package/dist/runtime/components/ForgotPasswordForm.vue +0 -77
  33. package/dist/runtime/components/ProfileForm.vue +0 -240
  34. package/dist/runtime/components/ProfileForm.vue.d.ts +0 -20
  35. package/dist/runtime/server/api/profile.get.d.ts +0 -2
  36. /package/dist/runtime/components/{ForgotPasswordForm.vue.d.ts → ProfileInfo.vue.d.ts} +0 -0
  37. /package/dist/runtime/server/api/{login.post.d.ts → auth/login.post.d.ts} +0 -0
  38. /package/dist/runtime/server/api/{logout.get.d.ts → auth/logout.get.d.ts} +0 -0
  39. /package/dist/runtime/server/api/{update-password.post.d.ts → auth/update-password.post.d.ts} +0 -0
@@ -1,118 +1,98 @@
1
1
  <script setup>
2
- import { ref, reactive, onMounted, computed, watch } from "vue";
3
- import { useRoute, useRouter } from "#app";
2
+ import { ref, watch } from "vue";
4
3
  import { usePasswordValidation } from "../composables/usePasswordValidation";
5
4
  import { useRuntimeConfig } from "#imports";
6
5
  import PasswordStrengthIndicator from "./PasswordStrengthIndicator.vue";
7
- const formData = reactive({
8
- password: "",
9
- password_confirm: ""
6
+ const props = defineProps({
7
+ apiEndpoint: { type: String, required: false, default: "/api/user/profile" },
8
+ updatePasswordEndpoint: { type: String, required: false, default: "/api/auth/update-password" }
10
9
  });
11
- const message = ref("");
12
- const loading = ref(false);
13
- const isError = ref(false);
14
- const formInvalidDueToToken = ref(false);
15
- const route = useRoute();
16
- const router = useRouter();
17
- const tokenFromUrl = ref(null);
18
- const emailFromUrl = ref(null);
19
- onMounted(() => {
20
- if (typeof route.query.token === "string") {
21
- tokenFromUrl.value = route.query.token;
22
- }
23
- if (typeof route.query.email === "string") {
24
- emailFromUrl.value = decodeURIComponent(route.query.email);
25
- }
26
- if (!tokenFromUrl.value || !emailFromUrl.value) {
27
- message.value = "Invalid or missing password reset token or email in URL. Cannot reset password.";
28
- isError.value = true;
29
- formInvalidDueToToken.value = true;
30
- }
10
+ const emit = defineEmits(["success", "error", "password-error", "password-updated"]);
11
+ const isPasswordLoading = ref(false);
12
+ const passwordError = ref("");
13
+ const passwordSuccess = ref("");
14
+ const passwordForm = ref({
15
+ currentPassword: "",
16
+ newPassword: "",
17
+ newPasswordConfirmation: ""
31
18
  });
32
- const canSubmit = computed(() => !!tokenFromUrl.value && !!emailFromUrl.value && !formInvalidDueToToken.value);
33
19
  const { public: { nuxtUsers } } = useRuntimeConfig();
34
20
  const moduleOptions = nuxtUsers;
35
21
  const passwordValidation = usePasswordValidation(moduleOptions);
36
- watch(() => formData.password, (newPassword) => {
22
+ watch(() => passwordForm.value.newPassword, (newPassword) => {
37
23
  if (newPassword) {
38
24
  passwordValidation.validate(newPassword);
39
25
  } else {
40
26
  passwordValidation.clearValidation();
41
27
  }
42
28
  });
43
- const handleResetPassword = async () => {
44
- if (!canSubmit.value) {
45
- message.value = "Token or email is missing or invalid. Cannot reset password.";
46
- isError.value = true;
47
- return;
48
- }
49
- if (formData.password !== formData.password_confirm) {
50
- message.value = "Passwords do not match.";
51
- isError.value = true;
52
- return;
53
- }
54
- loading.value = true;
55
- message.value = "";
56
- isError.value = false;
29
+ const updatePassword = async () => {
30
+ isPasswordLoading.value = true;
31
+ passwordError.value = "";
32
+ passwordSuccess.value = "";
57
33
  try {
58
- const response = await fetch("/api/reset-password", {
34
+ await $fetch(props.updatePasswordEndpoint, {
59
35
  method: "POST",
60
- headers: {
61
- "Content-Type": "application/json",
62
- "Accept": "application/json"
63
- },
64
- body: JSON.stringify({
65
- token: tokenFromUrl.value,
66
- email: emailFromUrl.value,
67
- password: formData.password,
68
- password_confirmation: formData.password_confirm
69
- })
36
+ body: passwordForm.value
70
37
  });
71
- const responseData = await response.json();
72
- if (!response.ok) {
73
- throw new Error(responseData.message || responseData.statusMessage || "An error occurred.");
74
- }
75
- message.value = responseData.message + " Redirecting to login...";
76
- isError.value = false;
77
- formData.password = "";
78
- formData.password_confirm = "";
79
- setTimeout(() => {
80
- router.push("/login");
81
- }, 3e3);
38
+ passwordSuccess.value = "Password updated successfully";
39
+ emit("password-updated");
40
+ passwordForm.value = {
41
+ currentPassword: "",
42
+ newPassword: "",
43
+ newPasswordConfirmation: ""
44
+ };
82
45
  } catch (err) {
83
- if (err instanceof Error) {
84
- message.value = err.message;
85
- } else {
86
- message.value = "Failed to reset password.";
87
- }
88
- console.error("[Nuxt Users] Reset Password Form Error:", err);
89
- isError.value = true;
46
+ const errorMessage = err && typeof err === "object" && "data" in err && err.data && typeof err.data === "object" && "statusMessage" in err.data ? String(err.data.statusMessage) : err instanceof Error ? err.message : "Failed to update password";
47
+ passwordError.value = errorMessage;
48
+ emit("password-error", errorMessage);
90
49
  } finally {
91
- loading.value = false;
50
+ isPasswordLoading.value = false;
92
51
  }
93
52
  };
94
53
  </script>
95
54
 
96
55
  <template>
97
56
  <div>
98
- <h2>Reset Password</h2>
99
- <form @submit.prevent="handleResetPassword">
57
+ <h2>Change Password</h2>
58
+
59
+ <form @submit.prevent="updatePassword">
60
+ <div class="form-group">
61
+ <label
62
+ for="currentPassword"
63
+ class="form-label"
64
+ >Current Password</label>
65
+ <input
66
+ id="currentPassword"
67
+ v-model="passwordForm.currentPassword"
68
+ type="password"
69
+ class="form-input"
70
+ required
71
+ :disabled="isPasswordLoading"
72
+ >
73
+ </div>
74
+
100
75
  <div class="form-group">
101
- <label for="password">New Password</label>
76
+ <label
77
+ for="newPassword"
78
+ class="form-label"
79
+ >New Password</label>
102
80
  <input
103
- id="password"
104
- v-model="formData.password"
81
+ id="newPassword"
82
+ v-model="passwordForm.newPassword"
105
83
  type="password"
106
- name="password"
107
- placeholder="Enter new password"
84
+ class="form-input"
108
85
  required
109
86
  :minlength="moduleOptions.passwordValidation?.minLength || 8"
110
- :class="{ error: passwordValidation.errors.value.length > 0 && formData.password }"
87
+ :class="{ error: passwordValidation.errors.value.length > 0 && passwordForm.newPassword }"
88
+ :disabled="isPasswordLoading"
111
89
  >
90
+
112
91
  <PasswordStrengthIndicator
113
- :password="formData.password"
92
+ :password="passwordForm.newPassword"
114
93
  :validation-result="passwordValidation.validationResult.value"
115
94
  />
95
+
116
96
  <!-- Password requirements -->
117
97
  <small class="form-help">
118
98
  Password must contain at least {{ moduleOptions.passwordValidation?.minLength || 8 }} characters
@@ -122,35 +102,48 @@ const handleResetPassword = async () => {
122
102
  <span v-if="moduleOptions.passwordValidation?.requireSpecialChars">, and special characters</span>.
123
103
  </small>
124
104
  </div>
105
+
125
106
  <div class="form-group">
126
- <label for="password_confirm">Confirm New Password</label>
107
+ <label
108
+ for="newPasswordConfirmation"
109
+ class="form-label"
110
+ >Confirm New Password</label>
127
111
  <input
128
- id="password_confirm"
129
- v-model="formData.password_confirm"
112
+ id="newPasswordConfirmation"
113
+ v-model="passwordForm.newPasswordConfirmation"
130
114
  type="password"
131
- name="password_confirm"
132
- placeholder="Confirm new password"
115
+ class="form-input"
133
116
  required
134
- :minlength="moduleOptions.passwordValidation?.minLength || 8"
117
+ :disabled="isPasswordLoading"
135
118
  >
136
119
  </div>
120
+
121
+ <div
122
+ v-if="passwordError"
123
+ class="error-message"
124
+ >
125
+ {{ passwordError }}
126
+ </div>
127
+
128
+ <div
129
+ v-if="passwordSuccess"
130
+ class="success-message"
131
+ >
132
+ {{ passwordSuccess }}
133
+ </div>
134
+
137
135
  <button
138
136
  type="submit"
139
- :disabled="loading || formInvalidDueToToken"
137
+ class="btn btn-primary"
138
+ :disabled="isPasswordLoading"
140
139
  >
141
- {{ loading ? "Resetting..." : "Reset Password" }}
140
+ <span v-if="isPasswordLoading">Updating...</span>
141
+ <span v-else>Update Password</span>
142
142
  </button>
143
- <p
144
- v-if="message"
145
- :class="{ error: isError, success: !isError }"
146
- class="form-message"
147
- >
148
- {{ message }}
149
- </p>
150
143
  </form>
151
144
  </div>
152
145
  </template>
153
146
 
154
147
  <style scoped>
155
- .error{color:red}.success{color:green}.form-message{font-size:.9rem;margin-top:1em}.form-group{margin-bottom:1em}label{display:block;font-weight:700;margin-bottom:.25em}input[type=password]{border:1px solid var(--color-border-dark);border-radius:4px;box-sizing:border-box;padding:.75em;width:100%}input[type=password].error{border-color:#dc3545}.form-help{color:#6c757d;display:block;font-size:.875rem;margin-top:4px}button[type=submit]{background-color:var(--color-gray-800);border:none;border-radius:4px;color:#fff;cursor:pointer;padding:.75em 1.5em}button[type=submit]:disabled{background-color:var(--color-gray-500);cursor:not-allowed}
148
+ div{background-color:var(--color-bg-primary);border-bottom-left-radius:.5em;border-bottom-right-radius:.5em;margin:0 auto;max-width:42em;padding:1em}h2{border-bottom:.1em solid var(--color-border)}form{gap:1em}.form-group,form{display:flex;flex-direction:column}.form-group{gap:.5em;width:90%}.form-label{color:var(--color-text-primary);font-size:1rem;font-weight:500}.form-input{border:1px solid var(--color-border);border-radius:.5em;font-size:1rem;padding:.5em 1em;transition:border-color .2s,box-shadow .2s}.form-input:focus{border-color:var(--color-primary);box-shadow:0 0 0 .1em rgba(var(--color-primary-rgb),.1);outline:none}.form-input:disabled{background-color:var(--color-bg-secondary);cursor:not-allowed}.form-help{color:var(--color-text-secondary);font-size:.75rem;margin-top:.25em}.btn{align-self:flex-start;border:none;border-radius:.5em;cursor:pointer;font-size:1rem;font-weight:500;padding:.5em 1em;transition:background-color .2s,transform .1s}.btn:disabled{cursor:not-allowed;opacity:.6;transform:none}.btn-primary{background-color:var(--color-primary);color:#fff}.btn-primary:hover:not(:disabled){background-color:var(--color-primary-dark);transform:translateY(-1px)}.error-message{background-color:var(--color-error-bg);border:1px solid var(--color-error-border);border-radius:.5em;color:var(--color-error);font-size:.875rem;padding:12px}.success-message{background-color:#f0fff4;border:1px solid #9ae6b4;border-radius:6px;color:#38a169;font-size:.875rem;padding:12px}
156
149
  </style>
@@ -1,2 +1,16 @@
1
- declare const _default: import("vue").DefineComponent<{}, void, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
1
+ import type { UserWithoutPassword, ResetPasswordFormProps } from '../../types.js';
2
+ declare const _default: import("vue").DefineComponent<ResetPasswordFormProps, void, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
3
+ success: (user: UserWithoutPassword) => any;
4
+ error: (error: string) => any;
5
+ "password-error": (error: string) => any;
6
+ "password-updated": () => any;
7
+ }, string, import("vue").PublicProps, Readonly<ResetPasswordFormProps> & Readonly<{
8
+ onSuccess?: ((user: UserWithoutPassword) => any) | undefined;
9
+ onError?: ((error: string) => any) | undefined;
10
+ "onPassword-error"?: ((error: string) => any) | undefined;
11
+ "onPassword-updated"?: (() => any) | undefined;
12
+ }>, {
13
+ apiEndpoint: string;
14
+ updatePasswordEndpoint: string;
15
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
2
16
  export default _default;
@@ -1,15 +1,15 @@
1
1
  import type { User, UserWithoutPassword } from '../../types.js';
2
2
  export declare const useAuth: () => {
3
3
  user: Readonly<import("vue").Ref<{
4
- readonly email: string;
5
4
  readonly id: number;
5
+ readonly email: string;
6
6
  readonly name: string;
7
7
  readonly role: string;
8
8
  readonly created_at: string;
9
9
  readonly updated_at: string;
10
10
  } | null, {
11
- readonly email: string;
12
11
  readonly id: number;
12
+ readonly email: string;
13
13
  readonly name: string;
14
14
  readonly role: string;
15
15
  readonly created_at: string;
@@ -12,7 +12,7 @@ export const useAuth = () => {
12
12
  };
13
13
  const logout = async () => {
14
14
  try {
15
- await $fetch("/api/logout", {
15
+ await $fetch("/api/auth/logout", {
16
16
  method: "GET"
17
17
  });
18
18
  user.value = null;
@@ -26,7 +26,7 @@ export const useAuth = () => {
26
26
  };
27
27
  const fetchUser = async () => {
28
28
  try {
29
- const response = await $fetch("/api/profile", {
29
+ const response = await $fetch("/api/user/profile", {
30
30
  method: "GET"
31
31
  });
32
32
  user.value = response.user;
@@ -25,7 +25,7 @@ export declare const usePasswordValidation: (moduleOptions?: ModuleOptions, opti
25
25
  isValid: import("vue").ComputedRef<boolean>;
26
26
  errors: import("vue").ComputedRef<string[]>;
27
27
  hints: import("vue").ComputedRef<string[]>;
28
- strength: import("vue").ComputedRef<"strong" | "weak" | "medium">;
28
+ strength: import("vue").ComputedRef<"weak" | "medium" | "strong">;
29
29
  score: import("vue").ComputedRef<number>;
30
30
  strengthColor: import("vue").ComputedRef<string>;
31
31
  strengthText: import("vue").ComputedRef<string>;
@@ -11,10 +11,10 @@ export default defineNuxtRouteMiddleware((to) => {
11
11
  console.log(`[Nuxt Users] client.middleware.auth.global: Whitelisted: ${to.path}`);
12
12
  return;
13
13
  }
14
- const { user } = useAuth();
15
- if (!user.value?.id) {
16
- console.log("[Nuxt Users] client.middleware.auth.global: Unauthenticated, redirecting to /login");
14
+ const { isAuthenticated } = useAuth();
15
+ if (!isAuthenticated.value) {
16
+ console.log(`[Nuxt Users] client.middleware.auth.global: Unauthenticated ${to.path}, redirecting to /login`);
17
17
  return navigateTo("/login");
18
18
  }
19
- console.log("[Nuxt Users] client.middleware.auth.global", { user: user.value?.id, to: to.path });
19
+ console.log("[Nuxt Users] client.middleware.auth.global", { isAuthenticated: isAuthenticated.value, to: to.path });
20
20
  });
@@ -2,7 +2,7 @@ import { createError, defineEventHandler, readBody, setCookie } from "h3";
2
2
  import bcrypt from "bcrypt";
3
3
  import crypto from "node:crypto";
4
4
  import { useRuntimeConfig } from "#imports";
5
- import { useDb } from "../utils/index.js";
5
+ import { useDb } from "../../utils/index.js";
6
6
  export default defineEventHandler(async (event) => {
7
7
  const body = await readBody(event);
8
8
  const { email, password } = body;
@@ -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,6 +1,6 @@
1
1
  import { defineEventHandler, getCookie, setCookie } from "h3";
2
2
  import { useRuntimeConfig } from "#imports";
3
- import { useDb } from "../utils/index.js";
3
+ import { useDb } from "../../utils/index.js";
4
4
  export default defineEventHandler(async (event) => {
5
5
  const { nuxtUsers } = useRuntimeConfig();
6
6
  const options = nuxtUsers;
@@ -1,8 +1,8 @@
1
1
  import { createError, defineEventHandler, getCookie, readBody } from "h3";
2
2
  import bcrypt from "bcrypt";
3
3
  import { useRuntimeConfig } from "#imports";
4
- import { getCurrentUserFromToken, updateUserPassword } from "../utils/index.js";
5
- import { validatePassword, getPasswordValidationOptions } from "../../../utils";
4
+ import { getCurrentUserFromToken, updateUserPassword } from "../../utils/index.js";
5
+ import { validatePassword, getPasswordValidationOptions } from "../../../../utils";
6
6
  export default defineEventHandler(async (event) => {
7
7
  const { nuxtUsers } = useRuntimeConfig();
8
8
  const options = nuxtUsers;
@@ -0,0 +1,4 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
2
+ user: import("../../../../types.js").UserWithoutPassword;
3
+ }>>;
4
+ export default _default;
@@ -1,6 +1,6 @@
1
1
  import { createError, defineEventHandler, getCookie } from "h3";
2
2
  import { useRuntimeConfig } from "#imports";
3
- import { getCurrentUserFromToken } from "../utils/index.js";
3
+ import { getCurrentUserFromToken } from "../../utils/index.js";
4
4
  export default defineEventHandler(async (event) => {
5
5
  const { nuxtUsers } = useRuntimeConfig();
6
6
  const options = nuxtUsers;
@@ -18,5 +18,5 @@ export default defineEventHandler(async (event) => {
18
18
  statusMessage: "Unauthorized - Invalid authentication token"
19
19
  });
20
20
  }
21
- return user;
21
+ return { user };
22
22
  });
@@ -1,6 +1,7 @@
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
5
  export default defineEventHandler(async (event) => {
5
6
  const { nuxtUsers } = useRuntimeConfig();
6
7
  const options = nuxtUsers;
@@ -8,7 +9,7 @@ export default defineEventHandler(async (event) => {
8
9
  if (!isPageOrApiRoute) {
9
10
  return;
10
11
  }
11
- if (event.path === "/login" || event.path === "/api/login") {
12
+ if (noAuthPaths.includes(event.path)) {
12
13
  console.log("[Nuxt Users] server.middleware.auth.global: /login");
13
14
  return;
14
15
  }
@@ -26,6 +27,6 @@ export default defineEventHandler(async (event) => {
26
27
  console.log(`[Nuxt Users] server.middleware.auth.global: ${event.path} No user found redirecting to login`);
27
28
  return sendRedirect(event, "/login");
28
29
  }
29
- console.log(`[Nuxt Users] server.middleware.auth.global: Authenticated request for ${user.id}`);
30
+ console.log(`[Nuxt Users] server.middleware.auth.global: Authenticated request to ${event.path} for ${user.id}`);
30
31
  return;
31
32
  });
@@ -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
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-users",
3
- "version": "1.6.5",
3
+ "version": "1.6.7",
4
4
  "description": "A comprehensive user management module for Nuxt 3 applications with authentication, database support, and CLI tools",
5
5
  "keywords": [
6
6
  "nuxt",
@@ -1,77 +0,0 @@
1
- <script setup>
2
- import { ref, reactive } from "vue";
3
- const formData = reactive({
4
- email: ""
5
- });
6
- const message = ref("");
7
- const loading = ref(false);
8
- const isError = ref(false);
9
- const handleForgotPassword = async () => {
10
- loading.value = true;
11
- message.value = "";
12
- isError.value = false;
13
- try {
14
- const response = await fetch("/api/forgot-password", {
15
- method: "POST",
16
- headers: {
17
- "Content-Type": "application/json",
18
- "Accept": "application/json"
19
- },
20
- body: JSON.stringify({ email: formData.email })
21
- });
22
- const responseData = await response.json();
23
- if (!response.ok) {
24
- throw new Error(responseData.message || responseData.statusMessage || "An error occurred.");
25
- }
26
- message.value = responseData.message;
27
- isError.value = false;
28
- formData.email = "";
29
- } catch (err) {
30
- if (err instanceof Error) {
31
- message.value = err.message;
32
- } else {
33
- message.value = "Failed to send password reset link.";
34
- }
35
- console.error("[Nuxt Users] Forgot Password Form Error:", err);
36
- isError.value = true;
37
- } finally {
38
- loading.value = false;
39
- }
40
- };
41
- </script>
42
-
43
- <template>
44
- <div>
45
- <h2>Forgot Password</h2>
46
- <form @submit.prevent="handleForgotPassword">
47
- <div class="form-group">
48
- <label for="email">Email</label>
49
- <input
50
- id="email"
51
- v-model="formData.email"
52
- type="email"
53
- name="email"
54
- placeholder="your@email.com"
55
- required
56
- >
57
- </div>
58
- <button
59
- type="submit"
60
- :disabled="loading"
61
- >
62
- {{ loading ? "Sending..." : "Send Password Reset Link" }}
63
- </button>
64
- <p
65
- v-if="message"
66
- :class="{ error: isError, success: !isError }"
67
- class="form-message"
68
- >
69
- {{ message }}
70
- </p>
71
- </form>
72
- </div>
73
- </template>
74
-
75
- <style scoped>
76
- .error{color:red}.success{color:green}.form-message{font-size:.9rem;margin-top:1em}.form-group{margin-bottom:1em}label{display:block;font-weight:700;margin-bottom:.25em}input[type=email]{border:1px solid var(--color-border-dark);border-radius:4px;box-sizing:border-box;padding:.75em;width:100%}button[type=submit]{background-color:var(--color-gray-800);border:none;border-radius:4px;color:#fff;cursor:pointer;padding:.75em 1.5em}button[type=submit]:disabled{background-color:var(--color-gray-500);cursor:not-allowed}
77
- </style>