nuxt-users 1.3.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 (32) hide show
  1. package/CHANGELOG.md +61 -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 +38 -1
  7. package/dist/runtime/components/LoginForm.vue +51 -4
  8. package/dist/runtime/components/LoginForm.vue.d.ts +13 -4
  9. package/dist/runtime/components/PasswordStrengthIndicator.vue +132 -0
  10. package/dist/runtime/components/PasswordStrengthIndicator.vue.d.ts +12 -0
  11. package/dist/runtime/components/ProfileForm.vue +240 -0
  12. package/dist/runtime/components/ProfileForm.vue.d.ts +20 -0
  13. package/dist/runtime/components/ResetPasswordForm.vue +30 -4
  14. package/dist/runtime/composables/useAuth.d.ts +20 -2
  15. package/dist/runtime/composables/useAuth.js +53 -2
  16. package/dist/runtime/composables/usePasswordValidation.d.ts +33 -0
  17. package/dist/runtime/composables/usePasswordValidation.js +37 -0
  18. package/dist/runtime/plugin.js +46 -1
  19. package/dist/runtime/server/api/auth/reset-password.post.js +9 -3
  20. package/dist/runtime/server/api/profile/update-password.post.d.ts +4 -0
  21. package/dist/runtime/server/api/profile/update-password.post.js +60 -0
  22. package/dist/runtime/server/api/profile.get.d.ts +11 -0
  23. package/dist/runtime/server/api/profile.get.js +23 -0
  24. package/dist/shared/{nuxt-users.DZkU2AbH.mjs → nuxt-users.37k1R3_g.mjs} +172 -1
  25. package/dist/shared/{nuxt-users.DGRKT2Tq.d.mts → nuxt-users.CMEtv_Mw.d.mts} +64 -6
  26. package/dist/shared/{nuxt-users.DGRKT2Tq.d.ts → nuxt-users.CMEtv_Mw.d.ts} +64 -6
  27. package/dist/utils.d.mts +28 -2
  28. package/dist/utils.d.ts +28 -2
  29. package/dist/utils.mjs +1 -1
  30. package/package.json +3 -2
  31. package/dist/runtime/middleware/auth.global.d.ts +0 -2
  32. package/dist/runtime/middleware/auth.global.js +0 -12
@@ -0,0 +1,240 @@
1
+ <script setup>
2
+ import { ref, onMounted, watch } from "vue";
3
+ import { usePasswordValidation } from "../composables/usePasswordValidation";
4
+ import { useRuntimeConfig } from "#imports";
5
+ import PasswordStrengthIndicator from "./PasswordStrengthIndicator.vue";
6
+ const props = defineProps({
7
+ apiEndpoint: { type: String, required: false, default: "/api/profile" },
8
+ updatePasswordEndpoint: { type: String, required: false, default: "/api/profile/update-password" }
9
+ });
10
+ const emit = defineEmits(["success", "error", "password-error", "password-updated"]);
11
+ const isLoading = ref(false);
12
+ const isPasswordLoading = ref(false);
13
+ const error = ref("");
14
+ const passwordError = ref("");
15
+ const passwordSuccess = ref("");
16
+ const user = ref(null);
17
+ const passwordForm = ref({
18
+ currentPassword: "",
19
+ newPassword: "",
20
+ newPasswordConfirmation: ""
21
+ });
22
+ const { public: { nuxtUsers } } = useRuntimeConfig();
23
+ const moduleOptions = nuxtUsers;
24
+ const passwordValidation = usePasswordValidation(moduleOptions);
25
+ watch(() => passwordForm.value.newPassword, (newPassword) => {
26
+ if (newPassword) {
27
+ passwordValidation.validate(newPassword);
28
+ } else {
29
+ passwordValidation.clearValidation();
30
+ }
31
+ });
32
+ const loadProfile = async () => {
33
+ isLoading.value = true;
34
+ error.value = "";
35
+ try {
36
+ const response = await $fetch(props.apiEndpoint, {
37
+ method: "GET"
38
+ });
39
+ user.value = response.user;
40
+ emit("success", response.user);
41
+ } catch (err) {
42
+ 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 load profile";
43
+ error.value = errorMessage;
44
+ emit("error", errorMessage);
45
+ } finally {
46
+ isLoading.value = false;
47
+ }
48
+ };
49
+ const updatePassword = async () => {
50
+ isPasswordLoading.value = true;
51
+ passwordError.value = "";
52
+ passwordSuccess.value = "";
53
+ try {
54
+ await $fetch(props.updatePasswordEndpoint, {
55
+ method: "POST",
56
+ body: passwordForm.value
57
+ });
58
+ passwordSuccess.value = "Password updated successfully";
59
+ emit("password-updated");
60
+ passwordForm.value = {
61
+ currentPassword: "",
62
+ newPassword: "",
63
+ newPasswordConfirmation: ""
64
+ };
65
+ } catch (err) {
66
+ 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";
67
+ passwordError.value = errorMessage;
68
+ emit("password-error", errorMessage);
69
+ } finally {
70
+ isPasswordLoading.value = false;
71
+ }
72
+ };
73
+ const formatDate = (dateString) => {
74
+ return new Date(dateString).toLocaleDateString("en-US", {
75
+ year: "numeric",
76
+ month: "long",
77
+ day: "numeric",
78
+ hour: "2-digit",
79
+ minute: "2-digit"
80
+ });
81
+ };
82
+ onMounted(() => {
83
+ loadProfile();
84
+ });
85
+ </script>
86
+
87
+ <template>
88
+ <div class="profile-form">
89
+ <!-- Profile Information Section -->
90
+ <div class="profile-section">
91
+ <h2 class="section-title">
92
+ Profile Information
93
+ </h2>
94
+
95
+ <div
96
+ v-if="isLoading"
97
+ class="loading"
98
+ >
99
+ Loading profile...
100
+ </div>
101
+
102
+ <div
103
+ v-else-if="error"
104
+ class="error-message"
105
+ >
106
+ {{ error }}
107
+ </div>
108
+
109
+ <div
110
+ v-else-if="user"
111
+ class="profile-info"
112
+ >
113
+ <div class="info-row">
114
+ <label class="info-label">Name:</label>
115
+ <span class="info-value">{{ user.name }}</span>
116
+ </div>
117
+
118
+ <div class="info-row">
119
+ <label class="info-label">Email:</label>
120
+ <span class="info-value">{{ user.email }}</span>
121
+ </div>
122
+
123
+ <div class="info-row">
124
+ <label class="info-label">Role:</label>
125
+ <span class="info-value">{{ user.role }}</span>
126
+ </div>
127
+
128
+ <div class="info-row">
129
+ <label class="info-label">Member since:</label>
130
+ <span class="info-value">{{ formatDate(user.created_at) }}</span>
131
+ </div>
132
+
133
+ <div class="info-row">
134
+ <label class="info-label">Last updated:</label>
135
+ <span class="info-value">{{ formatDate(user.updated_at) }}</span>
136
+ </div>
137
+ </div>
138
+ </div>
139
+
140
+ <!-- Change Password Section -->
141
+ <div class="password-section">
142
+ <h2 class="section-title">
143
+ Change Password
144
+ </h2>
145
+
146
+ <form
147
+ class="password-form"
148
+ @submit.prevent="updatePassword"
149
+ >
150
+ <div class="form-group">
151
+ <label
152
+ for="currentPassword"
153
+ class="form-label"
154
+ >Current Password</label>
155
+ <input
156
+ id="currentPassword"
157
+ v-model="passwordForm.currentPassword"
158
+ type="password"
159
+ class="form-input"
160
+ required
161
+ :disabled="isPasswordLoading"
162
+ >
163
+ </div>
164
+
165
+ <div class="form-group">
166
+ <label
167
+ for="newPassword"
168
+ class="form-label"
169
+ >New Password</label>
170
+ <input
171
+ id="newPassword"
172
+ v-model="passwordForm.newPassword"
173
+ type="password"
174
+ class="form-input"
175
+ required
176
+ :minlength="moduleOptions.passwordValidation?.minLength || 8"
177
+ :class="{ error: passwordValidation.errors.value.length > 0 && passwordForm.newPassword }"
178
+ :disabled="isPasswordLoading"
179
+ >
180
+
181
+ <PasswordStrengthIndicator
182
+ :password="passwordForm.newPassword"
183
+ :validation-result="passwordValidation.validationResult.value"
184
+ />
185
+
186
+ <!-- Password requirements -->
187
+ <small class="form-help">
188
+ Password must contain at least {{ moduleOptions.passwordValidation?.minLength || 8 }} characters
189
+ <span v-if="moduleOptions.passwordValidation?.requireUppercase">, including uppercase letters</span>
190
+ <span v-if="moduleOptions.passwordValidation?.requireLowercase">, lowercase letters</span>
191
+ <span v-if="moduleOptions.passwordValidation?.requireNumbers">, numbers</span>
192
+ <span v-if="moduleOptions.passwordValidation?.requireSpecialChars">, and special characters</span>.
193
+ </small>
194
+ </div>
195
+
196
+ <div class="form-group">
197
+ <label
198
+ for="newPasswordConfirmation"
199
+ class="form-label"
200
+ >Confirm New Password</label>
201
+ <input
202
+ id="newPasswordConfirmation"
203
+ v-model="passwordForm.newPasswordConfirmation"
204
+ type="password"
205
+ class="form-input"
206
+ required
207
+ :disabled="isPasswordLoading"
208
+ >
209
+ </div>
210
+
211
+ <div
212
+ v-if="passwordError"
213
+ class="error-message"
214
+ >
215
+ {{ passwordError }}
216
+ </div>
217
+
218
+ <div
219
+ v-if="passwordSuccess"
220
+ class="success-message"
221
+ >
222
+ {{ passwordSuccess }}
223
+ </div>
224
+
225
+ <button
226
+ type="submit"
227
+ class="btn btn-primary"
228
+ :disabled="isPasswordLoading"
229
+ >
230
+ <span v-if="isPasswordLoading">Updating...</span>
231
+ <span v-else>Update Password</span>
232
+ </button>
233
+ </form>
234
+ </div>
235
+ </div>
236
+ </template>
237
+
238
+ <style scoped>
239
+ .profile-form{margin:0 auto;max-width:600px;padding:20px}.password-section,.profile-section{background:#fff;border:1px solid #e2e8f0;border-radius:8px;margin-bottom:24px;padding:24px}.section-title{border-bottom:2px solid #e2e8f0;color:#1a202c;font-size:1.5rem;font-weight:600;margin:0 0 20px;padding-bottom:12px}.loading{color:#718096;font-style:italic;text-align:center}.profile-info{display:flex;flex-direction:column;gap:16px}.info-row{align-items:center;border-bottom:1px solid #f7fafc;display:flex;padding:12px 0}.info-row:last-child{border-bottom:none}.info-label{color:#4a5568;font-weight:600;margin-right:16px;min-width:120px}.info-value{color:#2d3748;flex:1}.password-form{gap:20px}.form-group,.password-form{display:flex;flex-direction:column}.form-group{gap:8px}.form-label{color:#4a5568;font-size:.875rem;font-weight:500}.form-input{border:1px solid #e2e8f0;border-radius:6px;font-size:1rem;padding:12px 16px;transition:border-color .2s,box-shadow .2s}.form-input:focus{border-color:#4299e1;box-shadow:0 0 0 3px rgba(66,153,225,.1);outline:none}.form-input:disabled{background-color:#f7fafc;cursor:not-allowed}.form-help{color:#718096;font-size:.75rem;margin-top:4px}.btn{align-self:flex-start;border:none;border-radius:6px;cursor:pointer;font-size:1rem;font-weight:500;padding:12px 24px;transition:background-color .2s,transform .1s}.btn:disabled{cursor:not-allowed;opacity:.6;transform:none}.btn-primary{background-color:#4299e1;color:#fff}.btn-primary:hover:not(:disabled){background-color:#3182ce;transform:translateY(-1px)}.error-message{background-color:#fed7d7;border:1px solid #feb2b2;border-radius:6px;color:#e53e3e;font-size:.875rem;padding:12px}.success-message{background-color:#f0fff4;border:1px solid #9ae6b4;border-radius:6px;color:#38a169;font-size:.875rem;padding:12px}
240
+ </style>
@@ -0,0 +1,20 @@
1
+ import type { UserWithoutPassword } from '../../types.js';
2
+ interface ProfileFormProps {
3
+ apiEndpoint?: string;
4
+ updatePasswordEndpoint?: string;
5
+ }
6
+ declare const _default: import("vue").DefineComponent<ProfileFormProps, void, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
7
+ error: (error: string) => any;
8
+ success: (user: UserWithoutPassword) => any;
9
+ "password-error": (error: string) => any;
10
+ "password-updated": () => any;
11
+ }, string, import("vue").PublicProps, Readonly<ProfileFormProps> & Readonly<{
12
+ onError?: ((error: string) => any) | undefined;
13
+ onSuccess?: ((user: UserWithoutPassword) => any) | undefined;
14
+ "onPassword-error"?: ((error: string) => any) | undefined;
15
+ "onPassword-updated"?: (() => any) | undefined;
16
+ }>, {
17
+ apiEndpoint: string;
18
+ updatePasswordEndpoint: string;
19
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
20
+ export default _default;
@@ -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
+ };
@@ -1,10 +1,55 @@
1
- import { defineNuxtPlugin, useRuntimeConfig } from "#app";
1
+ import { addRouteMiddleware, defineNuxtPlugin, useRuntimeConfig, navigateTo } from "#app";
2
2
  import { checkTableExists } from "../utils";
3
+ import { useAuth } from "./composables/useAuth.js";
3
4
  export default defineNuxtPlugin(async (_nuxtApp) => {
4
5
  const { nuxtUsers } = useRuntimeConfig();
6
+ const { public: { nuxtUsers: publicNuxtUsers } } = useRuntimeConfig();
5
7
  const options = nuxtUsers;
8
+ const publicOptions = publicNuxtUsers;
6
9
  const hasMigrationsTable = await checkTableExists(options, options.tables.migrations);
7
10
  if (!hasMigrationsTable) {
8
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");
9
12
  }
13
+ const { initializeUser } = useAuth();
14
+ if (import.meta.client) {
15
+ await initializeUser();
16
+ }
17
+ addRouteMiddleware(
18
+ "auth.global",
19
+ (to, from) => {
20
+ if (to.path === "/login") {
21
+ return;
22
+ }
23
+ if (import.meta.server) {
24
+ console.log("[Nuxt Users] \u{1F510} Auth middleware: SSR - allowing request to proceed");
25
+ return;
26
+ }
27
+ const { user } = useAuth();
28
+ if (user.value || publicOptions.auth?.whitelist?.includes(to.path)) {
29
+ console.log("[Nuxt Users] \u{1F510} Auth middleware: Access allowed");
30
+ return;
31
+ }
32
+ console.log(`[Nuxt Users] \u{1F510} Auth middleware: Redirecting from ${from.path} to login - no user and not whitelisted`);
33
+ return navigateTo("/login");
34
+ },
35
+ { global: true }
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
+ }
10
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;