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.
- package/CHANGELOG.md +61 -0
- package/dist/cli.mjs +12 -30
- package/dist/module.d.mts +1 -1
- package/dist/module.d.ts +1 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +38 -1
- package/dist/runtime/components/LoginForm.vue +51 -4
- package/dist/runtime/components/LoginForm.vue.d.ts +13 -4
- package/dist/runtime/components/PasswordStrengthIndicator.vue +132 -0
- package/dist/runtime/components/PasswordStrengthIndicator.vue.d.ts +12 -0
- package/dist/runtime/components/ProfileForm.vue +240 -0
- package/dist/runtime/components/ProfileForm.vue.d.ts +20 -0
- package/dist/runtime/components/ResetPasswordForm.vue +30 -4
- package/dist/runtime/composables/useAuth.d.ts +20 -2
- package/dist/runtime/composables/useAuth.js +53 -2
- package/dist/runtime/composables/usePasswordValidation.d.ts +33 -0
- package/dist/runtime/composables/usePasswordValidation.js +37 -0
- package/dist/runtime/plugin.js +46 -1
- package/dist/runtime/server/api/auth/reset-password.post.js +9 -3
- package/dist/runtime/server/api/profile/update-password.post.d.ts +4 -0
- package/dist/runtime/server/api/profile/update-password.post.js +60 -0
- package/dist/runtime/server/api/profile.get.d.ts +11 -0
- package/dist/runtime/server/api/profile.get.js +23 -0
- package/dist/shared/{nuxt-users.DZkU2AbH.mjs → nuxt-users.37k1R3_g.mjs} +172 -1
- package/dist/shared/{nuxt-users.DGRKT2Tq.d.mts → nuxt-users.CMEtv_Mw.d.mts} +64 -6
- package/dist/shared/{nuxt-users.DGRKT2Tq.d.ts → nuxt-users.CMEtv_Mw.d.ts} +64 -6
- package/dist/utils.d.mts +28 -2
- package/dist/utils.d.ts +28 -2
- package/dist/utils.mjs +1 -1
- package/package.json +3 -2
- package/dist/runtime/middleware/auth.global.d.ts +0 -2
- 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<
|
|
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
|
-
|
|
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
|
+
};
|
package/dist/runtime/plugin.js
CHANGED
|
@@ -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,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
|
+
});
|