nuxt-users 1.4.0 → 1.5.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 (29) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/cli.mjs +12 -30
  3. package/dist/module.d.mts +1 -1
  4. package/dist/module.d.ts +1 -1
  5. package/dist/module.json +1 -1
  6. package/dist/module.mjs +37 -0
  7. package/dist/runtime/components/LoginForm.vue.d.ts +3 -3
  8. package/dist/runtime/components/PasswordStrengthIndicator.vue +132 -0
  9. package/dist/runtime/components/PasswordStrengthIndicator.vue.d.ts +12 -0
  10. package/dist/runtime/components/ProfileForm.vue +240 -0
  11. package/dist/runtime/components/ProfileForm.vue.d.ts +20 -0
  12. package/dist/runtime/components/ResetPasswordForm.vue +30 -4
  13. package/dist/runtime/composables/useAuth.d.ts +20 -2
  14. package/dist/runtime/composables/useAuth.js +53 -2
  15. package/dist/runtime/composables/usePasswordValidation.d.ts +33 -0
  16. package/dist/runtime/composables/usePasswordValidation.js +37 -0
  17. package/dist/runtime/plugin.js +29 -1
  18. package/dist/runtime/server/api/auth/reset-password.post.js +9 -3
  19. package/dist/runtime/server/api/profile/update-password.post.d.ts +4 -0
  20. package/dist/runtime/server/api/profile/update-password.post.js +60 -0
  21. package/dist/runtime/server/api/profile.get.d.ts +11 -0
  22. package/dist/runtime/server/api/profile.get.js +23 -0
  23. package/dist/shared/{nuxt-users.DZkU2AbH.mjs → nuxt-users.37k1R3_g.mjs} +172 -1
  24. package/dist/shared/{nuxt-users.DGRKT2Tq.d.mts → nuxt-users.CMEtv_Mw.d.mts} +64 -6
  25. package/dist/shared/{nuxt-users.DGRKT2Tq.d.ts → nuxt-users.CMEtv_Mw.d.ts} +64 -6
  26. package/dist/utils.d.mts +28 -2
  27. package/dist/utils.d.ts +28 -2
  28. package/dist/utils.mjs +1 -1
  29. package/package.json +3 -2
@@ -1,6 +1,9 @@
1
1
  <script setup>
2
- import { ref, reactive, onMounted, computed } from "vue";
2
+ import { ref, reactive, onMounted, computed, watch } from "vue";
3
3
  import { useRoute, useRouter } from "#app";
4
+ import { usePasswordValidation } from "../composables/usePasswordValidation";
5
+ import { useRuntimeConfig } from "#imports";
6
+ import PasswordStrengthIndicator from "./PasswordStrengthIndicator.vue";
4
7
  const formData = reactive({
5
8
  password: "",
6
9
  password_confirm: ""
@@ -27,6 +30,16 @@ onMounted(() => {
27
30
  }
28
31
  });
29
32
  const canSubmit = computed(() => !!tokenFromUrl.value && !!emailFromUrl.value && !formInvalidDueToToken.value);
33
+ const { public: { nuxtUsers } } = useRuntimeConfig();
34
+ const moduleOptions = nuxtUsers;
35
+ const passwordValidation = usePasswordValidation(moduleOptions);
36
+ watch(() => formData.password, (newPassword) => {
37
+ if (newPassword) {
38
+ passwordValidation.validate(newPassword);
39
+ } else {
40
+ passwordValidation.clearValidation();
41
+ }
42
+ });
30
43
  const handleResetPassword = async () => {
31
44
  if (!canSubmit.value) {
32
45
  message.value = "Token or email is missing or invalid. Cannot reset password.";
@@ -93,8 +106,21 @@ const handleResetPassword = async () => {
93
106
  name="password"
94
107
  placeholder="Enter new password"
95
108
  required
96
- minlength="8"
109
+ :minlength="moduleOptions.passwordValidation?.minLength || 8"
110
+ :class="{ error: passwordValidation.errors.value.length > 0 && formData.password }"
97
111
  >
112
+ <PasswordStrengthIndicator
113
+ :password="formData.password"
114
+ :validation-result="passwordValidation.validationResult.value"
115
+ />
116
+ <!-- Password requirements -->
117
+ <small class="form-help">
118
+ Password must contain at least {{ moduleOptions.passwordValidation?.minLength || 8 }} characters
119
+ <span v-if="moduleOptions.passwordValidation?.requireUppercase">, including uppercase letters</span>
120
+ <span v-if="moduleOptions.passwordValidation?.requireLowercase">, lowercase letters</span>
121
+ <span v-if="moduleOptions.passwordValidation?.requireNumbers">, numbers</span>
122
+ <span v-if="moduleOptions.passwordValidation?.requireSpecialChars">, and special characters</span>.
123
+ </small>
98
124
  </div>
99
125
  <div class="form-group">
100
126
  <label for="password_confirm">Confirm New Password</label>
@@ -105,7 +131,7 @@ const handleResetPassword = async () => {
105
131
  name="password_confirm"
106
132
  placeholder="Confirm new password"
107
133
  required
108
- minlength="8"
134
+ :minlength="moduleOptions.passwordValidation?.minLength || 8"
109
135
  >
110
136
  </div>
111
137
  <button
@@ -126,5 +152,5 @@ const handleResetPassword = async () => {
126
152
  </template>
127
153
 
128
154
  <style scoped>
129
- .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%}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}
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}
130
156
  </style>
@@ -1,5 +1,23 @@
1
- import type { User } from '../../types.js';
1
+ import type { User, UserWithoutPassword } from '../../types.js';
2
2
  export declare const useAuth: () => {
3
- user: import("vue").Ref<User | null, User | null>;
3
+ user: Readonly<import("vue").Ref<{
4
+ readonly email: string;
5
+ readonly id: number;
6
+ readonly name: string;
7
+ readonly role: string;
8
+ readonly created_at: string;
9
+ readonly updated_at: string;
10
+ } | null, {
11
+ readonly email: string;
12
+ readonly id: number;
13
+ readonly name: string;
14
+ readonly role: string;
15
+ readonly created_at: string;
16
+ readonly updated_at: string;
17
+ } | null>>;
18
+ isAuthenticated: import("vue").ComputedRef<boolean>;
19
+ login: (userData: User) => void;
4
20
  logout: () => Promise<void>;
21
+ fetchUser: () => Promise<UserWithoutPassword | null>;
22
+ initializeUser: () => Promise<void>;
5
23
  };
@@ -1,19 +1,70 @@
1
1
  import { useState } from "#app";
2
+ import { computed, readonly } from "vue";
2
3
  export const useAuth = () => {
3
4
  const user = useState("user", () => null);
5
+ const isAuthenticated = computed(() => !!user.value);
6
+ const login = (userData) => {
7
+ const { password: _, ...userWithoutPassword } = userData;
8
+ user.value = userWithoutPassword;
9
+ if (import.meta.client) {
10
+ localStorage.setItem("user", JSON.stringify(userWithoutPassword));
11
+ }
12
+ };
4
13
  const logout = async () => {
5
14
  try {
6
15
  await $fetch("/api/logout", {
7
16
  method: "GET"
8
17
  });
9
18
  user.value = null;
19
+ if (import.meta.client) {
20
+ localStorage.removeItem("user");
21
+ }
10
22
  } catch (error) {
11
23
  console.error("Logout failed:", error);
12
24
  user.value = null;
13
25
  }
14
26
  };
27
+ const fetchUser = async () => {
28
+ try {
29
+ const response = await $fetch("/api/profile", {
30
+ method: "GET"
31
+ });
32
+ user.value = response.user;
33
+ if (import.meta.client) {
34
+ localStorage.setItem("user", JSON.stringify(response.user));
35
+ }
36
+ return response.user;
37
+ } catch (error) {
38
+ console.error("Failed to fetch user:", error);
39
+ user.value = null;
40
+ if (import.meta.client) {
41
+ localStorage.removeItem("user");
42
+ }
43
+ return null;
44
+ }
45
+ };
46
+ const initializeUser = async () => {
47
+ if (!import.meta.client) {
48
+ return;
49
+ }
50
+ const storedUser = localStorage.getItem("user");
51
+ if (storedUser) {
52
+ try {
53
+ user.value = JSON.parse(storedUser);
54
+ await fetchUser();
55
+ } catch (error) {
56
+ console.error("Failed to parse stored user:", error);
57
+ localStorage.removeItem("user");
58
+ user.value = null;
59
+ }
60
+ }
61
+ };
15
62
  return {
16
- user,
17
- logout
63
+ user: readonly(user),
64
+ isAuthenticated,
65
+ login,
66
+ logout,
67
+ fetchUser,
68
+ initializeUser
18
69
  };
19
70
  };
@@ -0,0 +1,33 @@
1
+ import { type PasswordValidationResult, type PasswordValidationOptions } from '../../utils/password-validation.js';
2
+ import type { ModuleOptions } from '../../types.js';
3
+ export declare const usePasswordValidation: (moduleOptions?: ModuleOptions, options?: PasswordValidationOptions) => {
4
+ password: import("vue").Ref<string, string>;
5
+ validationResult: import("vue").Ref<{
6
+ isValid: boolean;
7
+ errors: string[];
8
+ strength: "weak" | "medium" | "strong";
9
+ score: number;
10
+ hints: string[];
11
+ } | null, PasswordValidationResult | {
12
+ isValid: boolean;
13
+ errors: string[];
14
+ strength: "weak" | "medium" | "strong";
15
+ score: number;
16
+ hints: string[];
17
+ } | null>;
18
+ validate: (passwordToValidate: string) => {
19
+ isValid: boolean;
20
+ errors: string[];
21
+ strength: "weak" | "medium" | "strong";
22
+ score: number;
23
+ hints: string[];
24
+ };
25
+ isValid: import("vue").ComputedRef<boolean>;
26
+ errors: import("vue").ComputedRef<string[]>;
27
+ hints: import("vue").ComputedRef<string[]>;
28
+ strength: import("vue").ComputedRef<"strong" | "weak" | "medium">;
29
+ score: import("vue").ComputedRef<number>;
30
+ strengthColor: import("vue").ComputedRef<string>;
31
+ strengthText: import("vue").ComputedRef<string>;
32
+ clearValidation: () => void;
33
+ };
@@ -0,0 +1,37 @@
1
+ import { ref, computed } from "vue";
2
+ import { validatePassword, getPasswordValidationOptions, getPasswordStrengthColor, getPasswordStrengthText } from "../../utils/password-validation";
3
+ export const usePasswordValidation = (moduleOptions, options) => {
4
+ const passwordOptions = getPasswordValidationOptions(moduleOptions);
5
+ const finalOptions = { ...passwordOptions, ...options };
6
+ const password = ref("");
7
+ const validationResult = ref(null);
8
+ const validate = (passwordToValidate) => {
9
+ password.value = passwordToValidate;
10
+ validationResult.value = validatePassword(passwordToValidate, finalOptions);
11
+ return validationResult.value;
12
+ };
13
+ const isValid = computed(() => validationResult.value?.isValid ?? false);
14
+ const errors = computed(() => validationResult.value?.errors ?? []);
15
+ const hints = computed(() => validationResult.value?.hints ?? []);
16
+ const strength = computed(() => validationResult.value?.strength ?? "weak");
17
+ const score = computed(() => validationResult.value?.score ?? 0);
18
+ const strengthColor = computed(() => getPasswordStrengthColor(strength.value));
19
+ const strengthText = computed(() => getPasswordStrengthText(strength.value));
20
+ const clearValidation = () => {
21
+ password.value = "";
22
+ validationResult.value = null;
23
+ };
24
+ return {
25
+ password,
26
+ validationResult,
27
+ validate,
28
+ isValid,
29
+ errors,
30
+ hints,
31
+ strength,
32
+ score,
33
+ strengthColor,
34
+ strengthText,
35
+ clearValidation
36
+ };
37
+ };
@@ -3,19 +3,29 @@ import { checkTableExists } from "../utils";
3
3
  import { useAuth } from "./composables/useAuth.js";
4
4
  export default defineNuxtPlugin(async (_nuxtApp) => {
5
5
  const { nuxtUsers } = useRuntimeConfig();
6
+ const { public: { nuxtUsers: publicNuxtUsers } } = useRuntimeConfig();
6
7
  const options = nuxtUsers;
8
+ const publicOptions = publicNuxtUsers;
7
9
  const hasMigrationsTable = await checkTableExists(options, options.tables.migrations);
8
10
  if (!hasMigrationsTable) {
9
11
  console.warn("[Nuxt Users] \u26A0\uFE0F Migrations table does not exist, you should run the migration script to create it by running: npx nuxt-users migrate");
10
12
  }
13
+ const { initializeUser } = useAuth();
14
+ if (import.meta.client) {
15
+ await initializeUser();
16
+ }
11
17
  addRouteMiddleware(
12
18
  "auth.global",
13
19
  (to, from) => {
14
20
  if (to.path === "/login") {
15
21
  return;
16
22
  }
23
+ if (import.meta.server) {
24
+ console.log("[Nuxt Users] \u{1F510} Auth middleware: SSR - allowing request to proceed");
25
+ return;
26
+ }
17
27
  const { user } = useAuth();
18
- if (user.value || nuxtUsers.auth?.whitelist?.includes(to.path)) {
28
+ if (user.value || publicOptions.auth?.whitelist?.includes(to.path)) {
19
29
  console.log("[Nuxt Users] \u{1F510} Auth middleware: Access allowed");
20
30
  return;
21
31
  }
@@ -24,4 +34,22 @@ export default defineNuxtPlugin(async (_nuxtApp) => {
24
34
  },
25
35
  { global: true }
26
36
  );
37
+ if (import.meta.client) {
38
+ addRouteMiddleware(
39
+ "auth.client",
40
+ (to, from) => {
41
+ if (to.path === "/login") {
42
+ return;
43
+ }
44
+ const { user } = useAuth();
45
+ if (user.value || publicOptions.auth?.whitelist?.includes(to.path)) {
46
+ console.log("[Nuxt Users] \u{1F510} Client auth middleware: Access allowed");
47
+ return;
48
+ }
49
+ console.log(`[Nuxt Users] \u{1F510} Client auth middleware: Redirecting from ${from.path} to login - no user and not whitelisted`);
50
+ return navigateTo("/login");
51
+ },
52
+ { global: true }
53
+ );
54
+ }
27
55
  });
@@ -1,6 +1,7 @@
1
1
  import { defineEventHandler, readBody, createError, H3Error } from "h3";
2
2
  import { resetPassword } from "../../services/password.js";
3
3
  import { useRuntimeConfig } from "#imports";
4
+ import { validatePassword, getPasswordValidationOptions } from "../../../../utils/password-validation";
4
5
  export default defineEventHandler(async (event) => {
5
6
  const body = await readBody(event);
6
7
  const { token, email, password, password_confirmation } = body;
@@ -16,11 +17,16 @@ export default defineEventHandler(async (event) => {
16
17
  if (password !== password_confirmation) {
17
18
  throw createError({ statusCode: 400, statusMessage: "Passwords do not match." });
18
19
  }
19
- if (password.length < 8) {
20
- throw createError({ statusCode: 400, statusMessage: "Password must be at least 8 characters long." });
21
- }
22
20
  try {
23
21
  const options = useRuntimeConfig().nuxtUsers;
22
+ const passwordOptions = getPasswordValidationOptions(options);
23
+ const passwordValidation = validatePassword(password, passwordOptions);
24
+ if (!passwordValidation.isValid) {
25
+ throw createError({
26
+ statusCode: 400,
27
+ statusMessage: `Password validation failed: ${passwordValidation.errors.join(", ")}`
28
+ });
29
+ }
24
30
  const success = await resetPassword(token, email, password, options);
25
31
  if (success) {
26
32
  return { message: "Password has been reset successfully. You can now log in with your new password." };
@@ -0,0 +1,4 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
2
+ message: string;
3
+ }>>;
4
+ export default _default;
@@ -0,0 +1,60 @@
1
+ import { createError, defineEventHandler, getCookie, readBody } from "h3";
2
+ import bcrypt from "bcrypt";
3
+ import { useRuntimeConfig } from "#imports";
4
+ import { getCurrentUserFromToken, updateUserPassword } from "../../../../utils/user";
5
+ import { validatePassword, getPasswordValidationOptions } from "../../../../utils/password-validation";
6
+ export default defineEventHandler(async (event) => {
7
+ const { nuxtUsers } = useRuntimeConfig();
8
+ const options = nuxtUsers;
9
+ const token = getCookie(event, "auth_token");
10
+ if (!token) {
11
+ throw createError({
12
+ statusCode: 401,
13
+ statusMessage: "Unauthorized - No authentication token found"
14
+ });
15
+ }
16
+ const user = await getCurrentUserFromToken(token, options);
17
+ if (!user) {
18
+ throw createError({
19
+ statusCode: 401,
20
+ statusMessage: "Unauthorized - Invalid authentication token"
21
+ });
22
+ }
23
+ const body = await readBody(event);
24
+ const { currentPassword, newPassword, newPasswordConfirmation } = body;
25
+ if (!currentPassword || typeof currentPassword !== "string") {
26
+ throw createError({
27
+ statusCode: 400,
28
+ statusMessage: "Current password is required"
29
+ });
30
+ }
31
+ if (!newPassword || typeof newPassword !== "string") {
32
+ throw createError({
33
+ statusCode: 400,
34
+ statusMessage: "New password is required"
35
+ });
36
+ }
37
+ if (newPassword !== newPasswordConfirmation) {
38
+ throw createError({
39
+ statusCode: 400,
40
+ statusMessage: "New password confirmation does not match"
41
+ });
42
+ }
43
+ const passwordOptions = getPasswordValidationOptions(options);
44
+ const passwordValidation = validatePassword(newPassword, passwordOptions);
45
+ if (!passwordValidation.isValid) {
46
+ throw createError({
47
+ statusCode: 400,
48
+ statusMessage: `Password validation failed: ${passwordValidation.errors.join(", ")}`
49
+ });
50
+ }
51
+ const currentPasswordMatch = await bcrypt.compare(currentPassword, user.password);
52
+ if (!currentPasswordMatch) {
53
+ throw createError({
54
+ statusCode: 400,
55
+ statusMessage: "Current password is incorrect"
56
+ });
57
+ }
58
+ await updateUserPassword(user.email, newPassword, options);
59
+ return { message: "Password updated successfully" };
60
+ });
@@ -0,0 +1,11 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
2
+ user: {
3
+ id: number;
4
+ email: string;
5
+ name: string;
6
+ role: string;
7
+ created_at: string;
8
+ updated_at: string;
9
+ };
10
+ }>>;
11
+ export default _default;
@@ -0,0 +1,23 @@
1
+ import { createError, defineEventHandler, getCookie } from "h3";
2
+ import { useRuntimeConfig } from "#imports";
3
+ import { getCurrentUserFromToken } from "../../../utils/user";
4
+ export default defineEventHandler(async (event) => {
5
+ const { nuxtUsers } = useRuntimeConfig();
6
+ const options = nuxtUsers;
7
+ const token = getCookie(event, "auth_token");
8
+ if (!token) {
9
+ throw createError({
10
+ statusCode: 401,
11
+ statusMessage: "Unauthorized - No authentication token found"
12
+ });
13
+ }
14
+ const user = await getCurrentUserFromToken(token, options);
15
+ if (!user) {
16
+ throw createError({
17
+ statusCode: 401,
18
+ statusMessage: "Unauthorized - Invalid authentication token"
19
+ });
20
+ }
21
+ const { password: _, ...userWithoutPassword } = user;
22
+ return { user: userWithoutPassword };
23
+ });
@@ -47,9 +47,175 @@ const checkTableExists = async (options, tableName) => {
47
47
  }
48
48
  };
49
49
 
50
+ const DEFAULT_OPTIONS = {
51
+ minLength: 8,
52
+ requireUppercase: true,
53
+ requireLowercase: true,
54
+ requireNumbers: true,
55
+ requireSpecialChars: true,
56
+ preventCommonPasswords: true
57
+ };
58
+ const getPasswordValidationOptions = (moduleOptions) => {
59
+ if (!moduleOptions?.passwordValidation) {
60
+ return DEFAULT_OPTIONS;
61
+ }
62
+ return {
63
+ ...DEFAULT_OPTIONS,
64
+ ...moduleOptions.passwordValidation
65
+ };
66
+ };
67
+ const COMMON_PASSWORDS = [
68
+ "password",
69
+ "123456",
70
+ "123456789",
71
+ "qwerty",
72
+ "abc123",
73
+ "password123",
74
+ "admin",
75
+ "letmein",
76
+ "welcome",
77
+ "monkey",
78
+ "dragon",
79
+ "master",
80
+ "hello",
81
+ "freedom",
82
+ "whatever",
83
+ "qazwsx",
84
+ "trustno1",
85
+ "jordan",
86
+ "harley",
87
+ "ranger",
88
+ "buster",
89
+ "thomas",
90
+ "tigger",
91
+ "robert",
92
+ "soccer",
93
+ "batman",
94
+ "test",
95
+ "pass",
96
+ "user",
97
+ "guest",
98
+ "login",
99
+ "secret",
100
+ "god",
101
+ "love",
102
+ "sex",
103
+ "money",
104
+ "password1",
105
+ "12345678",
106
+ "qwerty123",
107
+ "admin123",
108
+ "password!",
109
+ "password1!",
110
+ "password123",
111
+ "password123!"
112
+ ];
113
+ const validatePassword = (password, options = {}) => {
114
+ const config = { ...DEFAULT_OPTIONS, ...options };
115
+ const errors = [];
116
+ const hints = [];
117
+ let score = 0;
118
+ if (password.length < config.minLength) {
119
+ errors.push(`Password must be at least ${config.minLength} characters long`);
120
+ hints.push(`Use at least ${config.minLength} characters`);
121
+ } else {
122
+ score += 20;
123
+ if (password.length < 12) {
124
+ hints.push("Use 12 or more characters for extra security");
125
+ }
126
+ }
127
+ if (config.requireUppercase && !/[A-Z]/.test(password)) {
128
+ errors.push("Password must contain at least one uppercase letter");
129
+ hints.push("Add an uppercase letter (A-Z)");
130
+ } else if (/[A-Z]/.test(password)) {
131
+ score += 15;
132
+ }
133
+ if (config.requireLowercase && !/[a-z]/.test(password)) {
134
+ errors.push("Password must contain at least one lowercase letter");
135
+ hints.push("Add a lowercase letter (a-z)");
136
+ } else if (/[a-z]/.test(password)) {
137
+ score += 15;
138
+ }
139
+ if (config.requireNumbers && !/\d/.test(password)) {
140
+ errors.push("Password must contain at least one number");
141
+ hints.push("Add a number (0-9)");
142
+ } else if (/\d/.test(password)) {
143
+ score += 15;
144
+ }
145
+ if (config.requireSpecialChars && !/[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/.test(password)) {
146
+ errors.push("Password must contain at least one special character");
147
+ hints.push("Add a special character (e.g. !@#$%)");
148
+ } else if (/[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/.test(password)) {
149
+ score += 15;
150
+ }
151
+ if (config.preventCommonPasswords && COMMON_PASSWORDS.includes(password.toLowerCase())) {
152
+ errors.push("Password is too common. Please choose a more unique password");
153
+ hints.push("Avoid common passwords or names");
154
+ }
155
+ if (password.length >= 12) {
156
+ score += 10;
157
+ } else if (password.length >= 10) {
158
+ score += 5;
159
+ }
160
+ const hasUppercase = /[A-Z]/.test(password);
161
+ const hasLowercase = /[a-z]/.test(password);
162
+ const hasNumbers = /\d/.test(password);
163
+ const hasSpecial = /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/.test(password);
164
+ const complexityTypes = [hasUppercase, hasLowercase, hasNumbers, hasSpecial].filter(Boolean).length;
165
+ if (complexityTypes >= 4) {
166
+ score += 10;
167
+ } else if (complexityTypes >= 3) {
168
+ score += 5;
169
+ hints.push("Use a mix of uppercase, lowercase, numbers, and special characters");
170
+ }
171
+ let strength = "weak";
172
+ if (score >= 80) {
173
+ strength = "strong";
174
+ } else if (score >= 60) {
175
+ strength = "medium";
176
+ }
177
+ return {
178
+ isValid: errors.length === 0,
179
+ errors,
180
+ strength,
181
+ score: Math.min(score, 100),
182
+ hints: Array.from(new Set(hints))
183
+ // deduplicate
184
+ };
185
+ };
186
+ const getPasswordStrengthColor = (strength) => {
187
+ switch (strength) {
188
+ case "weak":
189
+ return "#dc3545";
190
+ case "medium":
191
+ return "#ffc107";
192
+ case "strong":
193
+ return "#28a745";
194
+ default:
195
+ return "#6c757d";
196
+ }
197
+ };
198
+ const getPasswordStrengthText = (strength) => {
199
+ switch (strength) {
200
+ case "weak":
201
+ return "Weak";
202
+ case "medium":
203
+ return "Medium";
204
+ case "strong":
205
+ return "Strong";
206
+ default:
207
+ return "Unknown";
208
+ }
209
+ };
210
+
50
211
  const createUser = async (userData, options) => {
51
212
  const db = await useDb(options);
52
213
  const usersTable = options.tables.users;
214
+ const passwordOptions = getPasswordValidationOptions(options);
215
+ const passwordValidation = validatePassword(userData.password, passwordOptions);
216
+ if (!passwordValidation.isValid) {
217
+ throw new Error(`Password validation failed: ${passwordValidation.errors.join(", ")}`);
218
+ }
53
219
  const hashedPassword = await bcrypt.hash(userData.password, 10);
54
220
  const role = userData.role || "user";
55
221
  await db.sql`
@@ -91,6 +257,11 @@ const findUserByEmail = async (email, options) => {
91
257
  const updateUserPassword = async (email, newPassword, options) => {
92
258
  const db = await useDb(options);
93
259
  const usersTable = options.tables.users;
260
+ const passwordOptions = getPasswordValidationOptions(options);
261
+ const passwordValidation = validatePassword(newPassword, passwordOptions);
262
+ if (!passwordValidation.isValid) {
263
+ throw new Error(`Password validation failed: ${passwordValidation.errors.join(", ")}`);
264
+ }
94
265
  const hashedPassword = await bcrypt.hash(newPassword, 10);
95
266
  await db.sql`
96
267
  UPDATE {${usersTable}}
@@ -336,4 +507,4 @@ const runMigrations = async (options) => {
336
507
  console.log("[Nuxt Users] All migrations successfull \u2705");
337
508
  };
338
509
 
339
- export { createUser as a, updateUserPassword as b, checkTableExists as c, createUsersTable as d, createPersonalAccessTokensTable as e, findUserByEmail as f, createPasswordResetTokensTable as g, createMigrationsTable as h, runMigrations as r, useDb as u };
510
+ export { createUser as a, updateUserPassword as b, checkTableExists as c, createUsersTable as d, createPersonalAccessTokensTable as e, findUserByEmail as f, createPasswordResetTokensTable as g, createMigrationsTable as h, getPasswordValidationOptions as i, getPasswordStrengthColor as j, getPasswordStrengthText as k, runMigrations as r, useDb as u, validatePassword as v };