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.
- package/CHANGELOG.md +34 -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 +37 -0
- package/dist/runtime/components/LoginForm.vue.d.ts +3 -3
- 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 +29 -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
|
@@ -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
|
@@ -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 ||
|
|
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,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,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 };
|