nuxt-users 1.6.5 → 1.6.6
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 +31 -0
- package/dist/cli.mjs +1 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +16 -20
- package/dist/runtime/components/LoginForm.vue +6 -3
- package/dist/runtime/components/LoginForm.vue.d.ts +4 -4
- package/dist/runtime/components/LogoutLink.vue +21 -7
- package/dist/runtime/components/LogoutLink.vue.d.ts +22 -5
- package/dist/runtime/components/PasswordStrengthIndicator.vue +38 -27
- package/dist/runtime/components/ProfileInfo.vue +33 -0
- package/dist/runtime/components/ResetPasswordForm.vue +89 -96
- package/dist/runtime/components/ResetPasswordForm.vue.d.ts +15 -1
- package/dist/runtime/composables/useAuth.d.ts +2 -2
- package/dist/runtime/composables/useAuth.js +2 -2
- package/dist/runtime/composables/usePasswordValidation.d.ts +1 -1
- package/dist/runtime/middleware/auth.client.js +4 -4
- package/dist/runtime/server/api/{login.post.js → auth/login.post.js} +1 -1
- package/dist/runtime/server/api/{logout.get.js → auth/logout.get.js} +1 -1
- package/dist/runtime/server/api/{update-password.post.js → auth/update-password.post.js} +2 -2
- package/dist/runtime/server/api/user/profile.get.d.ts +4 -0
- package/dist/runtime/server/api/{profile.get.js → user/profile.get.js} +2 -2
- package/dist/runtime/server/middleware/auth.server.js +3 -2
- package/package.json +1 -1
- package/dist/runtime/components/ForgotPasswordForm.vue +0 -77
- package/dist/runtime/components/ProfileForm.vue +0 -240
- package/dist/runtime/components/ProfileForm.vue.d.ts +0 -20
- package/dist/runtime/server/api/profile.get.d.ts +0 -2
- /package/dist/runtime/components/{ForgotPasswordForm.vue.d.ts → ProfileInfo.vue.d.ts} +0 -0
- /package/dist/runtime/server/api/{login.post.d.ts → auth/login.post.d.ts} +0 -0
- /package/dist/runtime/server/api/{logout.get.d.ts → auth/logout.get.d.ts} +0 -0
- /package/dist/runtime/server/api/{update-password.post.d.ts → auth/update-password.post.d.ts} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
## v1.6.6
|
|
5
|
+
|
|
6
|
+
[compare changes](https://github.com/rrd108/nuxt-users/compare/v1.6.5...v1.6.6)
|
|
7
|
+
|
|
8
|
+
### 🩹 Fixes
|
|
9
|
+
|
|
10
|
+
- Authentication middleware ([c7de8a4](https://github.com/rrd108/nuxt-users/commit/c7de8a4))
|
|
11
|
+
- Lint and type errors ([dd6ee40](https://github.com/rrd108/nuxt-users/commit/dd6ee40))
|
|
12
|
+
- Type errors ([65c81fb](https://github.com/rrd108/nuxt-users/commit/65c81fb))
|
|
13
|
+
- **cli:** Updates CLI version dynamically ([1cca13a](https://github.com/rrd108/nuxt-users/commit/1cca13a))
|
|
14
|
+
- Authentication middleware logic ([18a3bb5](https://github.com/rrd108/nuxt-users/commit/18a3bb5))
|
|
15
|
+
- Password validation and UI ([07b44b3](https://github.com/rrd108/nuxt-users/commit/07b44b3))
|
|
16
|
+
|
|
17
|
+
### 💅 Refactors
|
|
18
|
+
|
|
19
|
+
- API routes ([3c7603f](https://github.com/rrd108/nuxt-users/commit/3c7603f))
|
|
20
|
+
- Authentication components and logic ([4da544f](https://github.com/rrd108/nuxt-users/commit/4da544f))
|
|
21
|
+
|
|
22
|
+
### 🏡 Chore
|
|
23
|
+
|
|
24
|
+
- **release:** V1.6.5 ([f35e526](https://github.com/rrd108/nuxt-users/commit/f35e526))
|
|
25
|
+
|
|
26
|
+
### ✅ Tests
|
|
27
|
+
|
|
28
|
+
- Fix mock ([791dc94](https://github.com/rrd108/nuxt-users/commit/791dc94))
|
|
29
|
+
- Adds unit tests for auth API endpoints ([d844630](https://github.com/rrd108/nuxt-users/commit/d844630))
|
|
30
|
+
|
|
31
|
+
### ❤️ Contributors
|
|
32
|
+
|
|
33
|
+
- Rrd108 ([@rrd108](https://github.com/rrd108))
|
|
34
|
+
|
|
4
35
|
## v1.6.5
|
|
5
36
|
|
|
6
37
|
[compare changes](https://github.com/rrd108/nuxt-users/compare/v1.6.4...v1.6.5)
|
package/dist/cli.mjs
CHANGED
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -93,54 +93,50 @@ const module = defineNuxtModule({
|
|
|
93
93
|
global: true
|
|
94
94
|
});
|
|
95
95
|
addServerHandler({
|
|
96
|
-
route: "/api/login",
|
|
96
|
+
route: "/api/auth/login",
|
|
97
97
|
method: "post",
|
|
98
|
-
handler: resolver.resolve("./runtime/server/api/login.post")
|
|
98
|
+
handler: resolver.resolve("./runtime/server/api/auth/login.post")
|
|
99
99
|
});
|
|
100
100
|
addServerHandler({
|
|
101
|
-
route: "/api/forgot-password",
|
|
101
|
+
route: "/api/auth/forgot-password",
|
|
102
102
|
method: "post",
|
|
103
103
|
handler: resolver.resolve("./runtime/server/api/auth/forgot-password.post")
|
|
104
104
|
});
|
|
105
105
|
addServerHandler({
|
|
106
|
-
route: "/api/reset-password",
|
|
106
|
+
route: "/api/auth/reset-password",
|
|
107
107
|
method: "post",
|
|
108
108
|
handler: resolver.resolve("./runtime/server/api/auth/reset-password.post")
|
|
109
109
|
});
|
|
110
110
|
addServerHandler({
|
|
111
|
-
route: "/api/logout",
|
|
111
|
+
route: "/api/auth/logout",
|
|
112
112
|
method: "get",
|
|
113
|
-
handler: resolver.resolve("./runtime/server/api/logout.get")
|
|
113
|
+
handler: resolver.resolve("./runtime/server/api/auth/logout.get")
|
|
114
114
|
});
|
|
115
115
|
addServerHandler({
|
|
116
|
-
route: "/api/profile",
|
|
116
|
+
route: "/api/user/profile",
|
|
117
117
|
method: "get",
|
|
118
|
-
handler: resolver.resolve("./runtime/server/api/profile.get")
|
|
118
|
+
handler: resolver.resolve("./runtime/server/api/user/profile.get")
|
|
119
119
|
});
|
|
120
120
|
addServerHandler({
|
|
121
|
-
route: "/api/update-password",
|
|
121
|
+
route: "/api/auth/update-password",
|
|
122
122
|
method: "post",
|
|
123
|
-
handler: resolver.resolve("./runtime/server/api/update-password.post")
|
|
123
|
+
handler: resolver.resolve("./runtime/server/api/auth/update-password.post")
|
|
124
124
|
});
|
|
125
125
|
addComponent({
|
|
126
126
|
name: "LoginForm",
|
|
127
127
|
filePath: resolver.resolve("./runtime/components/LoginForm.vue")
|
|
128
128
|
});
|
|
129
|
-
addComponent({
|
|
130
|
-
name: "ForgotPasswordForm",
|
|
131
|
-
filePath: resolver.resolve("./runtime/components/ForgotPasswordForm.vue")
|
|
132
|
-
});
|
|
133
|
-
addComponent({
|
|
134
|
-
name: "ResetPasswordForm",
|
|
135
|
-
filePath: resolver.resolve("./runtime/components/ResetPasswordForm.vue")
|
|
136
|
-
});
|
|
137
129
|
addComponent({
|
|
138
130
|
name: "LogoutLink",
|
|
139
131
|
filePath: resolver.resolve("./runtime/components/LogoutLink.vue")
|
|
140
132
|
});
|
|
141
133
|
addComponent({
|
|
142
|
-
name: "
|
|
143
|
-
filePath: resolver.resolve("./runtime/components/
|
|
134
|
+
name: "ProfileInfo",
|
|
135
|
+
filePath: resolver.resolve("./runtime/components/ProfileInfo.vue")
|
|
136
|
+
});
|
|
137
|
+
addComponent({
|
|
138
|
+
name: "ResetPasswordForm",
|
|
139
|
+
filePath: resolver.resolve("./runtime/components/ResetPasswordForm.vue")
|
|
144
140
|
});
|
|
145
141
|
nuxt.options.css = nuxt.options.css || [];
|
|
146
142
|
nuxt.options.css.push(resolver.resolve("./runtime/assets/colors.css"));
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { ref } from "vue";
|
|
3
3
|
import { navigateTo } from "#app";
|
|
4
|
+
import { useRuntimeConfig } from "#imports";
|
|
5
|
+
const { public: { nuxtUsers } } = useRuntimeConfig();
|
|
6
|
+
const { passwordValidation } = nuxtUsers;
|
|
4
7
|
const props = defineProps({
|
|
5
|
-
apiEndpoint: { type: String, required: false, default: "/api/login" },
|
|
8
|
+
apiEndpoint: { type: String, required: false, default: "/api/auth/login" },
|
|
6
9
|
redirectTo: { type: String, required: false, default: "/" },
|
|
7
10
|
forgotPasswordEndpoint: { type: String, required: false, default: "/api/auth/forgot-password" }
|
|
8
11
|
});
|
|
@@ -108,7 +111,7 @@ const handleForgotPassword = async () => {
|
|
|
108
111
|
name="password"
|
|
109
112
|
placeholder="Enter your password"
|
|
110
113
|
required
|
|
111
|
-
minlength="
|
|
114
|
+
:minlength="passwordValidation.minLength"
|
|
112
115
|
>
|
|
113
116
|
</div>
|
|
114
117
|
</slot>
|
|
@@ -190,5 +193,5 @@ const handleForgotPassword = async () => {
|
|
|
190
193
|
</template>
|
|
191
194
|
|
|
192
195
|
<style scoped>
|
|
193
|
-
section{background:linear-gradient(135deg,var(--color-bg-primary) 0,var(--color-bg-secondary) 100%);border:1px solid var(--color-border);border-radius:.75em;box-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -1px rgba(0,0,0,.06);margin:0 auto;max-width:25rem;padding:1em}form{gap:1.5rem}.form-group,form{display:flex;flex-direction:column}label{display:block;font-weight:500;margin-bottom:.5em}input[type=email],input[type=password],label{color:var(--color-gray-700);font-size:.875rem}input[type=email],input[type=password]{background-color:var(--color-bg-secondary);border:1px solid var(--color-border-light);border-radius
|
|
196
|
+
section{background:linear-gradient(135deg,var(--color-bg-primary) 0,var(--color-bg-secondary) 100%);border:1px solid var(--color-border);border-radius:.75em;box-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -1px rgba(0,0,0,.06);margin:0 auto;max-width:25rem;padding:1em}form{gap:1.5rem}.form-group,form{display:flex;flex-direction:column}label{display:block;font-weight:500;margin-bottom:.5em}input[type=email],input[type=password],label{color:var(--color-gray-700);font-size:.875rem}input[type=email],input[type=password]{background-color:var(--color-bg-secondary);border:1px solid var(--color-border-light);border-radius:.5em;box-sizing:border-box;padding:.75em 1em;transition:all .2s ease-in-out;width:100%}input[type=email]:focus,input[type=password]:focus{background-color:var(--color-bg-primary);border-color:var(--color-primary);box-shadow:0 0 0 3px rgba(59,130,246,.1);outline:none}.login-header{margin-bottom:1em;text-align:center}.login-title{color:var(--color-gray-800);font-size:1.875rem;font-weight:700;line-height:1.2;margin:0 0 .5em}.login-subtitle{color:var(--color-gray-500);font-size:.875rem;line-height:1.5;margin:0}.remember-me{align-items:center;display:flex;gap:.5rem}input[type=checkbox]{margin:0;width:auto}.remember-me label{color:var(--color-gray-700);font-size:.875rem;margin:0}button[type=submit]{align-items:center;background-color:var(--color-primary);border:1px solid var(--color-primary);border-radius:.5em;color:var(--color-white);cursor:pointer;display:flex;font-size:.875rem;font-weight:500;gap:.5rem;justify-content:center;min-height:44px;padding:.75em 1em;transition:all .2s ease-in-out;width:100%}button[type=submit]:hover:not(:disabled){background-color:var(--color-primary-dark);border-color:var(--color-primary-dark)}button[type=submit]:disabled{cursor:not-allowed;opacity:.6}.loading-spinner{animation:spin 1s linear infinite;border:2px solid var(--color-white);border-radius:50%;border-top:2px solid transparent;height:16px;width:16px}@keyframes spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}.login-footer{margin-top:1em;text-align:center}.forgot-password{margin:0}.forgot-link{align-items:center;color:var(--color-primary);display:inline-flex;font-size:.875rem;gap:.5rem;text-decoration:none;transition:color .2s ease-in-out}.forgot-link:hover:not(.loading){color:var(--color-primary-dark);text-decoration:underline}.forgot-link.loading{cursor:not-allowed;opacity:.6;pointer-events:none}.error-message{background-color:var(--color-error-light);border:1px solid var(--color-error-border);border-radius:.5em;color:var(--color-error)}.error-message,.success-message{font-size:.875rem;margin-top:1em;padding:.75em 1em;text-align:center}.success-message{background-color:var(--color-success-light);border:1px solid var(--color-success-border);border-radius:.5em;color:var(--color-success)}@media (max-width:480px){.login-form-container{margin:1em;padding:1.5em}.login-title{font-size:1.5rem}}
|
|
194
197
|
</style>
|
|
@@ -22,16 +22,16 @@ type __VLS_Slots = {} & {
|
|
|
22
22
|
'error-message'?: (props: typeof __VLS_15) => any;
|
|
23
23
|
};
|
|
24
24
|
declare const __VLS_component: import("vue").DefineComponent<LoginFormProps, void, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
25
|
-
error: (error: string) => any;
|
|
26
|
-
submit: (data: LoginFormData) => any;
|
|
27
25
|
success: (user: UserWithoutPassword) => any;
|
|
26
|
+
error: (error: string) => any;
|
|
28
27
|
"forgot-password-error": (error: string) => any;
|
|
28
|
+
submit: (data: LoginFormData) => any;
|
|
29
29
|
"forgot-password-success": () => any;
|
|
30
30
|
}, string, import("vue").PublicProps, Readonly<LoginFormProps> & Readonly<{
|
|
31
|
-
onError?: ((error: string) => any) | undefined;
|
|
32
|
-
onSubmit?: ((data: LoginFormData) => any) | undefined;
|
|
33
31
|
onSuccess?: ((user: UserWithoutPassword) => any) | undefined;
|
|
32
|
+
onError?: ((error: string) => any) | undefined;
|
|
34
33
|
"onForgot-password-error"?: ((error: string) => any) | undefined;
|
|
34
|
+
onSubmit?: ((data: LoginFormData) => any) | undefined;
|
|
35
35
|
"onForgot-password-success"?: (() => any) | undefined;
|
|
36
36
|
}>, {
|
|
37
37
|
apiEndpoint: string;
|
|
@@ -6,7 +6,14 @@ const props = defineProps({
|
|
|
6
6
|
redirectTo: { type: String, required: false, default: "/login" },
|
|
7
7
|
linkText: { type: String, required: false, default: "Logout" },
|
|
8
8
|
confirmMessage: { type: String, required: false, default: "Are you sure you want to logout?" },
|
|
9
|
-
class: { type: String, required: false }
|
|
9
|
+
class: { type: String, required: false },
|
|
10
|
+
style: { type: [String, Object], required: false },
|
|
11
|
+
linkClass: { type: String, required: false },
|
|
12
|
+
linkStyle: { type: [String, Object], required: false },
|
|
13
|
+
errorClass: { type: String, required: false },
|
|
14
|
+
errorStyle: { type: [String, Object], required: false },
|
|
15
|
+
containerClass: { type: String, required: false },
|
|
16
|
+
containerStyle: { type: [String, Object], required: false }
|
|
10
17
|
});
|
|
11
18
|
const emit = defineEmits(["success", "click", "error"]);
|
|
12
19
|
const { logout } = useAuth();
|
|
@@ -37,26 +44,33 @@ const handleLogout = async (event) => {
|
|
|
37
44
|
</script>
|
|
38
45
|
|
|
39
46
|
<template>
|
|
40
|
-
<div
|
|
47
|
+
<div
|
|
48
|
+
:class="['logout-container', props.containerClass]"
|
|
49
|
+
:style="props.containerStyle"
|
|
50
|
+
>
|
|
41
51
|
<div
|
|
42
52
|
v-if="error"
|
|
43
|
-
class="error-message"
|
|
53
|
+
:class="['error-message', props.errorClass]"
|
|
54
|
+
:style="props.errorStyle"
|
|
44
55
|
>
|
|
45
56
|
{{ error }}
|
|
46
57
|
</div>
|
|
47
58
|
|
|
48
59
|
<a
|
|
49
60
|
href="#"
|
|
50
|
-
:class="['logout-link', { loading: isLoading }, props.
|
|
61
|
+
:class="['logout-link', { loading: isLoading }, props.linkClass]"
|
|
62
|
+
:style="props.linkStyle"
|
|
51
63
|
:disabled="isLoading"
|
|
52
64
|
@click="handleLogout"
|
|
53
65
|
>
|
|
54
|
-
<
|
|
55
|
-
|
|
66
|
+
<slot>
|
|
67
|
+
<span v-if="isLoading">Logging out...</span>
|
|
68
|
+
<span v-else>{{ linkText }}</span>
|
|
69
|
+
</slot>
|
|
56
70
|
</a>
|
|
57
71
|
</div>
|
|
58
72
|
</template>
|
|
59
73
|
|
|
60
74
|
<style scoped>
|
|
61
|
-
.logout-link{color
|
|
75
|
+
.logout-link{background:var(--color-error);border:2px solid transparent;border-color:var(--color-error-dark);border-radius:.5em;color:#fff;display:inline-block;font-weight:500;padding:.5em 1em;text-decoration:none;transition:all .2s}.logout-link:hover{border-color:var(--color-primary)}.error-message{color:var(--color-error);font-size:1rem;margin-bottom:.5em}
|
|
62
76
|
</style>
|
|
@@ -3,18 +3,35 @@ interface LogoutLinkProps {
|
|
|
3
3
|
linkText?: string;
|
|
4
4
|
confirmMessage?: string;
|
|
5
5
|
class?: string;
|
|
6
|
+
style?: string | Record<string, string | number>;
|
|
7
|
+
linkClass?: string;
|
|
8
|
+
linkStyle?: string | Record<string, string | number>;
|
|
9
|
+
errorClass?: string;
|
|
10
|
+
errorStyle?: string | Record<string, string | number>;
|
|
11
|
+
containerClass?: string;
|
|
12
|
+
containerStyle?: string | Record<string, string | number>;
|
|
6
13
|
}
|
|
7
|
-
declare
|
|
8
|
-
|
|
9
|
-
|
|
14
|
+
declare var __VLS_1: {};
|
|
15
|
+
type __VLS_Slots = {} & {
|
|
16
|
+
default?: (props: typeof __VLS_1) => any;
|
|
17
|
+
};
|
|
18
|
+
declare const __VLS_component: import("vue").DefineComponent<LogoutLinkProps, void, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
10
19
|
success: () => any;
|
|
20
|
+
error: (error: string) => any;
|
|
21
|
+
click: () => any;
|
|
11
22
|
}, string, import("vue").PublicProps, Readonly<LogoutLinkProps> & Readonly<{
|
|
12
|
-
onClick?: (() => any) | undefined;
|
|
13
|
-
onError?: ((error: string) => any) | undefined;
|
|
14
23
|
onSuccess?: (() => any) | undefined;
|
|
24
|
+
onError?: ((error: string) => any) | undefined;
|
|
25
|
+
onClick?: (() => any) | undefined;
|
|
15
26
|
}>, {
|
|
16
27
|
redirectTo: string;
|
|
17
28
|
linkText: string;
|
|
18
29
|
confirmMessage: string;
|
|
19
30
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
31
|
+
declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
|
|
20
32
|
export default _default;
|
|
33
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
34
|
+
new (): {
|
|
35
|
+
$slots: S;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
@@ -1,49 +1,60 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { computed } from "vue";
|
|
3
|
+
import { useRuntimeConfig } from "#imports";
|
|
3
4
|
const props = defineProps({
|
|
4
5
|
password: { type: String, required: true },
|
|
5
6
|
validationResult: { type: [Object, null], required: true },
|
|
6
7
|
showHints: { type: Boolean, required: false, default: true },
|
|
7
8
|
showRules: { type: Boolean, required: false, default: true }
|
|
8
9
|
});
|
|
10
|
+
const { public: { nuxtUsers } } = useRuntimeConfig();
|
|
11
|
+
const moduleOptions = nuxtUsers;
|
|
9
12
|
const hasErrors = computed(() => (props.validationResult?.errors?.length || 0) > 0);
|
|
10
13
|
const hasHints = computed(() => (props.validationResult?.hints?.length || 0) > 0);
|
|
11
14
|
const hasPassword = computed(() => props.password.length > 0);
|
|
12
15
|
const validationRules = computed(() => {
|
|
13
16
|
if (!props.password) return [];
|
|
14
17
|
const rules = [];
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
18
|
+
if (moduleOptions.passwordValidation.minLength) {
|
|
19
|
+
const hasMinLength = props.password.length >= moduleOptions.passwordValidation.minLength;
|
|
20
|
+
rules.push({
|
|
21
|
+
text: `At least ${moduleOptions.passwordValidation.minLength} characters`,
|
|
22
|
+
passed: hasMinLength
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
if (moduleOptions.passwordValidation.requireUppercase) {
|
|
26
|
+
const hasUppercase = /[A-Z]/.test(props.password);
|
|
27
|
+
rules.push({
|
|
28
|
+
text: "Contains uppercase letter (A-Z)",
|
|
29
|
+
passed: hasUppercase
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
if (moduleOptions.passwordValidation.requireLowercase) {
|
|
33
|
+
const hasLowercase = /[a-z]/.test(props.password);
|
|
34
|
+
rules.push({
|
|
35
|
+
text: "Contains lowercase letter (a-z)",
|
|
36
|
+
passed: hasLowercase
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
if (moduleOptions.passwordValidation.requireNumbers) {
|
|
40
|
+
const hasNumbers = /\d/.test(props.password);
|
|
41
|
+
rules.push({
|
|
42
|
+
text: "Contains number (0-9)",
|
|
43
|
+
passed: hasNumbers
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
if (moduleOptions.passwordValidation.requireSpecialChars) {
|
|
47
|
+
const hasSpecialChars = /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/.test(props.password);
|
|
48
|
+
rules.push({
|
|
49
|
+
text: "Contains special character (!@#$%^&*)",
|
|
50
|
+
passed: hasSpecialChars
|
|
51
|
+
});
|
|
52
|
+
}
|
|
41
53
|
return rules;
|
|
42
54
|
});
|
|
43
55
|
</script>
|
|
44
56
|
|
|
45
57
|
<template>
|
|
46
|
-
<!-- Password strength indicator -->
|
|
47
58
|
<div
|
|
48
59
|
v-if="hasPassword"
|
|
49
60
|
class="password-strength"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { useAuth } from "../composables/useAuth";
|
|
3
|
+
const { user } = useAuth();
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<template>
|
|
7
|
+
<div>
|
|
8
|
+
<h2>Profile Information</h2>
|
|
9
|
+
<dl
|
|
10
|
+
v-if="user"
|
|
11
|
+
class="profile-info"
|
|
12
|
+
>
|
|
13
|
+
<dt>Name:</dt>
|
|
14
|
+
<dd>{{ user.name }}</dd>
|
|
15
|
+
|
|
16
|
+
<dt>Email:</dt>
|
|
17
|
+
<dd>{{ user.email }}</dd>
|
|
18
|
+
|
|
19
|
+
<dt>Role:</dt>
|
|
20
|
+
<dd>{{ user.role }}</dd>
|
|
21
|
+
|
|
22
|
+
<dt>Registered:</dt>
|
|
23
|
+
<dd>{{ Intl.DateTimeFormat().format(new Date(user.created_at)) }}</dd>
|
|
24
|
+
|
|
25
|
+
<dt>Last updated:</dt>
|
|
26
|
+
<dd>{{ Intl.DateTimeFormat().format(new Date(user.updated_at)) }}</dd>
|
|
27
|
+
</dl>
|
|
28
|
+
</div>
|
|
29
|
+
</template>
|
|
30
|
+
|
|
31
|
+
<style scoped>
|
|
32
|
+
div{background-color:var(--color-bg-primary);border-top-left-radius:.5em;border-top-right-radius:.5em;margin:0 auto;max-width:42em;padding:1em}h2{border-bottom:.1em solid var(--color-border)}dl{display:grid;font-size:1rem;gap:16px;grid-template-columns:1fr 5fr;list-style:none;margin:0;padding:0}
|
|
33
|
+
</style>
|
|
@@ -1,118 +1,98 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { ref,
|
|
3
|
-
import { useRoute, useRouter } from "#app";
|
|
2
|
+
import { ref, watch } from "vue";
|
|
4
3
|
import { usePasswordValidation } from "../composables/usePasswordValidation";
|
|
5
4
|
import { useRuntimeConfig } from "#imports";
|
|
6
5
|
import PasswordStrengthIndicator from "./PasswordStrengthIndicator.vue";
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
const props = defineProps({
|
|
7
|
+
apiEndpoint: { type: String, required: false, default: "/api/user/profile" },
|
|
8
|
+
updatePasswordEndpoint: { type: String, required: false, default: "/api/auth/update-password" }
|
|
10
9
|
});
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
onMounted(() => {
|
|
20
|
-
if (typeof route.query.token === "string") {
|
|
21
|
-
tokenFromUrl.value = route.query.token;
|
|
22
|
-
}
|
|
23
|
-
if (typeof route.query.email === "string") {
|
|
24
|
-
emailFromUrl.value = decodeURIComponent(route.query.email);
|
|
25
|
-
}
|
|
26
|
-
if (!tokenFromUrl.value || !emailFromUrl.value) {
|
|
27
|
-
message.value = "Invalid or missing password reset token or email in URL. Cannot reset password.";
|
|
28
|
-
isError.value = true;
|
|
29
|
-
formInvalidDueToToken.value = true;
|
|
30
|
-
}
|
|
10
|
+
const emit = defineEmits(["success", "error", "password-error", "password-updated"]);
|
|
11
|
+
const isPasswordLoading = ref(false);
|
|
12
|
+
const passwordError = ref("");
|
|
13
|
+
const passwordSuccess = ref("");
|
|
14
|
+
const passwordForm = ref({
|
|
15
|
+
currentPassword: "",
|
|
16
|
+
newPassword: "",
|
|
17
|
+
newPasswordConfirmation: ""
|
|
31
18
|
});
|
|
32
|
-
const canSubmit = computed(() => !!tokenFromUrl.value && !!emailFromUrl.value && !formInvalidDueToToken.value);
|
|
33
19
|
const { public: { nuxtUsers } } = useRuntimeConfig();
|
|
34
20
|
const moduleOptions = nuxtUsers;
|
|
35
21
|
const passwordValidation = usePasswordValidation(moduleOptions);
|
|
36
|
-
watch(() =>
|
|
22
|
+
watch(() => passwordForm.value.newPassword, (newPassword) => {
|
|
37
23
|
if (newPassword) {
|
|
38
24
|
passwordValidation.validate(newPassword);
|
|
39
25
|
} else {
|
|
40
26
|
passwordValidation.clearValidation();
|
|
41
27
|
}
|
|
42
28
|
});
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
if (formData.password !== formData.password_confirm) {
|
|
50
|
-
message.value = "Passwords do not match.";
|
|
51
|
-
isError.value = true;
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
loading.value = true;
|
|
55
|
-
message.value = "";
|
|
56
|
-
isError.value = false;
|
|
29
|
+
const updatePassword = async () => {
|
|
30
|
+
isPasswordLoading.value = true;
|
|
31
|
+
passwordError.value = "";
|
|
32
|
+
passwordSuccess.value = "";
|
|
57
33
|
try {
|
|
58
|
-
|
|
34
|
+
await $fetch(props.updatePasswordEndpoint, {
|
|
59
35
|
method: "POST",
|
|
60
|
-
|
|
61
|
-
"Content-Type": "application/json",
|
|
62
|
-
"Accept": "application/json"
|
|
63
|
-
},
|
|
64
|
-
body: JSON.stringify({
|
|
65
|
-
token: tokenFromUrl.value,
|
|
66
|
-
email: emailFromUrl.value,
|
|
67
|
-
password: formData.password,
|
|
68
|
-
password_confirmation: formData.password_confirm
|
|
69
|
-
})
|
|
36
|
+
body: passwordForm.value
|
|
70
37
|
});
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
formData.password_confirm = "";
|
|
79
|
-
setTimeout(() => {
|
|
80
|
-
router.push("/login");
|
|
81
|
-
}, 3e3);
|
|
38
|
+
passwordSuccess.value = "Password updated successfully";
|
|
39
|
+
emit("password-updated");
|
|
40
|
+
passwordForm.value = {
|
|
41
|
+
currentPassword: "",
|
|
42
|
+
newPassword: "",
|
|
43
|
+
newPasswordConfirmation: ""
|
|
44
|
+
};
|
|
82
45
|
} catch (err) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
message.value = "Failed to reset password.";
|
|
87
|
-
}
|
|
88
|
-
console.error("[Nuxt Users] Reset Password Form Error:", err);
|
|
89
|
-
isError.value = true;
|
|
46
|
+
const errorMessage = err && typeof err === "object" && "data" in err && err.data && typeof err.data === "object" && "statusMessage" in err.data ? String(err.data.statusMessage) : err instanceof Error ? err.message : "Failed to update password";
|
|
47
|
+
passwordError.value = errorMessage;
|
|
48
|
+
emit("password-error", errorMessage);
|
|
90
49
|
} finally {
|
|
91
|
-
|
|
50
|
+
isPasswordLoading.value = false;
|
|
92
51
|
}
|
|
93
52
|
};
|
|
94
53
|
</script>
|
|
95
54
|
|
|
96
55
|
<template>
|
|
97
56
|
<div>
|
|
98
|
-
<h2>
|
|
99
|
-
|
|
57
|
+
<h2>Change Password</h2>
|
|
58
|
+
|
|
59
|
+
<form @submit.prevent="updatePassword">
|
|
60
|
+
<div class="form-group">
|
|
61
|
+
<label
|
|
62
|
+
for="currentPassword"
|
|
63
|
+
class="form-label"
|
|
64
|
+
>Current Password</label>
|
|
65
|
+
<input
|
|
66
|
+
id="currentPassword"
|
|
67
|
+
v-model="passwordForm.currentPassword"
|
|
68
|
+
type="password"
|
|
69
|
+
class="form-input"
|
|
70
|
+
required
|
|
71
|
+
:disabled="isPasswordLoading"
|
|
72
|
+
>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
100
75
|
<div class="form-group">
|
|
101
|
-
<label
|
|
76
|
+
<label
|
|
77
|
+
for="newPassword"
|
|
78
|
+
class="form-label"
|
|
79
|
+
>New Password</label>
|
|
102
80
|
<input
|
|
103
|
-
id="
|
|
104
|
-
v-model="
|
|
81
|
+
id="newPassword"
|
|
82
|
+
v-model="passwordForm.newPassword"
|
|
105
83
|
type="password"
|
|
106
|
-
|
|
107
|
-
placeholder="Enter new password"
|
|
84
|
+
class="form-input"
|
|
108
85
|
required
|
|
109
86
|
:minlength="moduleOptions.passwordValidation?.minLength || 8"
|
|
110
|
-
:class="{ error: passwordValidation.errors.value.length > 0 &&
|
|
87
|
+
:class="{ error: passwordValidation.errors.value.length > 0 && passwordForm.newPassword }"
|
|
88
|
+
:disabled="isPasswordLoading"
|
|
111
89
|
>
|
|
90
|
+
|
|
112
91
|
<PasswordStrengthIndicator
|
|
113
|
-
:password="
|
|
92
|
+
:password="passwordForm.newPassword"
|
|
114
93
|
:validation-result="passwordValidation.validationResult.value"
|
|
115
94
|
/>
|
|
95
|
+
|
|
116
96
|
<!-- Password requirements -->
|
|
117
97
|
<small class="form-help">
|
|
118
98
|
Password must contain at least {{ moduleOptions.passwordValidation?.minLength || 8 }} characters
|
|
@@ -122,35 +102,48 @@ const handleResetPassword = async () => {
|
|
|
122
102
|
<span v-if="moduleOptions.passwordValidation?.requireSpecialChars">, and special characters</span>.
|
|
123
103
|
</small>
|
|
124
104
|
</div>
|
|
105
|
+
|
|
125
106
|
<div class="form-group">
|
|
126
|
-
<label
|
|
107
|
+
<label
|
|
108
|
+
for="newPasswordConfirmation"
|
|
109
|
+
class="form-label"
|
|
110
|
+
>Confirm New Password</label>
|
|
127
111
|
<input
|
|
128
|
-
id="
|
|
129
|
-
v-model="
|
|
112
|
+
id="newPasswordConfirmation"
|
|
113
|
+
v-model="passwordForm.newPasswordConfirmation"
|
|
130
114
|
type="password"
|
|
131
|
-
|
|
132
|
-
placeholder="Confirm new password"
|
|
115
|
+
class="form-input"
|
|
133
116
|
required
|
|
134
|
-
:
|
|
117
|
+
:disabled="isPasswordLoading"
|
|
135
118
|
>
|
|
136
119
|
</div>
|
|
120
|
+
|
|
121
|
+
<div
|
|
122
|
+
v-if="passwordError"
|
|
123
|
+
class="error-message"
|
|
124
|
+
>
|
|
125
|
+
{{ passwordError }}
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
<div
|
|
129
|
+
v-if="passwordSuccess"
|
|
130
|
+
class="success-message"
|
|
131
|
+
>
|
|
132
|
+
{{ passwordSuccess }}
|
|
133
|
+
</div>
|
|
134
|
+
|
|
137
135
|
<button
|
|
138
136
|
type="submit"
|
|
139
|
-
|
|
137
|
+
class="btn btn-primary"
|
|
138
|
+
:disabled="isPasswordLoading"
|
|
140
139
|
>
|
|
141
|
-
|
|
140
|
+
<span v-if="isPasswordLoading">Updating...</span>
|
|
141
|
+
<span v-else>Update Password</span>
|
|
142
142
|
</button>
|
|
143
|
-
<p
|
|
144
|
-
v-if="message"
|
|
145
|
-
:class="{ error: isError, success: !isError }"
|
|
146
|
-
class="form-message"
|
|
147
|
-
>
|
|
148
|
-
{{ message }}
|
|
149
|
-
</p>
|
|
150
143
|
</form>
|
|
151
144
|
</div>
|
|
152
145
|
</template>
|
|
153
146
|
|
|
154
147
|
<style scoped>
|
|
155
|
-
|
|
148
|
+
div{background-color:var(--color-bg-primary);border-bottom-left-radius:.5em;border-bottom-right-radius:.5em;margin:0 auto;max-width:42em;padding:1em}h2{border-bottom:.1em solid var(--color-border)}form{gap:1em}.form-group,form{display:flex;flex-direction:column}.form-group{gap:.5em;width:90%}.form-label{color:var(--color-text-primary);font-size:1rem;font-weight:500}.form-input{border:1px solid var(--color-border);border-radius:.5em;font-size:1rem;padding:.5em 1em;transition:border-color .2s,box-shadow .2s}.form-input:focus{border-color:var(--color-primary);box-shadow:0 0 0 .1em rgba(var(--color-primary-rgb),.1);outline:none}.form-input:disabled{background-color:var(--color-bg-secondary);cursor:not-allowed}.form-help{color:var(--color-text-secondary);font-size:.75rem;margin-top:.25em}.btn{align-self:flex-start;border:none;border-radius:.5em;cursor:pointer;font-size:1rem;font-weight:500;padding:.5em 1em;transition:background-color .2s,transform .1s}.btn:disabled{cursor:not-allowed;opacity:.6;transform:none}.btn-primary{background-color:var(--color-primary);color:#fff}.btn-primary:hover:not(:disabled){background-color:var(--color-primary-dark);transform:translateY(-1px)}.error-message{background-color:var(--color-error-bg);border:1px solid var(--color-error-border);border-radius:.5em;color:var(--color-error);font-size:.875rem;padding:12px}.success-message{background-color:#f0fff4;border:1px solid #9ae6b4;border-radius:6px;color:#38a169;font-size:.875rem;padding:12px}
|
|
156
149
|
</style>
|
|
@@ -1,2 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
import type { UserWithoutPassword, ResetPasswordFormProps } from '../../types.js';
|
|
2
|
+
declare const _default: import("vue").DefineComponent<ResetPasswordFormProps, void, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
3
|
+
success: (user: UserWithoutPassword) => any;
|
|
4
|
+
error: (error: string) => any;
|
|
5
|
+
"password-error": (error: string) => any;
|
|
6
|
+
"password-updated": () => any;
|
|
7
|
+
}, string, import("vue").PublicProps, Readonly<ResetPasswordFormProps> & Readonly<{
|
|
8
|
+
onSuccess?: ((user: UserWithoutPassword) => any) | undefined;
|
|
9
|
+
onError?: ((error: string) => any) | undefined;
|
|
10
|
+
"onPassword-error"?: ((error: string) => any) | undefined;
|
|
11
|
+
"onPassword-updated"?: (() => any) | undefined;
|
|
12
|
+
}>, {
|
|
13
|
+
apiEndpoint: string;
|
|
14
|
+
updatePasswordEndpoint: string;
|
|
15
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
2
16
|
export default _default;
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import type { User, UserWithoutPassword } from '../../types.js';
|
|
2
2
|
export declare const useAuth: () => {
|
|
3
3
|
user: Readonly<import("vue").Ref<{
|
|
4
|
-
readonly email: string;
|
|
5
4
|
readonly id: number;
|
|
5
|
+
readonly email: string;
|
|
6
6
|
readonly name: string;
|
|
7
7
|
readonly role: string;
|
|
8
8
|
readonly created_at: string;
|
|
9
9
|
readonly updated_at: string;
|
|
10
10
|
} | null, {
|
|
11
|
-
readonly email: string;
|
|
12
11
|
readonly id: number;
|
|
12
|
+
readonly email: string;
|
|
13
13
|
readonly name: string;
|
|
14
14
|
readonly role: string;
|
|
15
15
|
readonly created_at: string;
|
|
@@ -12,7 +12,7 @@ export const useAuth = () => {
|
|
|
12
12
|
};
|
|
13
13
|
const logout = async () => {
|
|
14
14
|
try {
|
|
15
|
-
await $fetch("/api/logout", {
|
|
15
|
+
await $fetch("/api/auth/logout", {
|
|
16
16
|
method: "GET"
|
|
17
17
|
});
|
|
18
18
|
user.value = null;
|
|
@@ -26,7 +26,7 @@ export const useAuth = () => {
|
|
|
26
26
|
};
|
|
27
27
|
const fetchUser = async () => {
|
|
28
28
|
try {
|
|
29
|
-
const response = await $fetch("/api/profile", {
|
|
29
|
+
const response = await $fetch("/api/user/profile", {
|
|
30
30
|
method: "GET"
|
|
31
31
|
});
|
|
32
32
|
user.value = response.user;
|
|
@@ -25,7 +25,7 @@ export declare const usePasswordValidation: (moduleOptions?: ModuleOptions, opti
|
|
|
25
25
|
isValid: import("vue").ComputedRef<boolean>;
|
|
26
26
|
errors: import("vue").ComputedRef<string[]>;
|
|
27
27
|
hints: import("vue").ComputedRef<string[]>;
|
|
28
|
-
strength: import("vue").ComputedRef<"
|
|
28
|
+
strength: import("vue").ComputedRef<"weak" | "medium" | "strong">;
|
|
29
29
|
score: import("vue").ComputedRef<number>;
|
|
30
30
|
strengthColor: import("vue").ComputedRef<string>;
|
|
31
31
|
strengthText: import("vue").ComputedRef<string>;
|
|
@@ -11,10 +11,10 @@ export default defineNuxtRouteMiddleware((to) => {
|
|
|
11
11
|
console.log(`[Nuxt Users] client.middleware.auth.global: Whitelisted: ${to.path}`);
|
|
12
12
|
return;
|
|
13
13
|
}
|
|
14
|
-
const {
|
|
15
|
-
if (!
|
|
16
|
-
console.log(
|
|
14
|
+
const { isAuthenticated } = useAuth();
|
|
15
|
+
if (!isAuthenticated.value) {
|
|
16
|
+
console.log(`[Nuxt Users] client.middleware.auth.global: Unauthenticated ${to.path}, redirecting to /login`);
|
|
17
17
|
return navigateTo("/login");
|
|
18
18
|
}
|
|
19
|
-
console.log("[Nuxt Users] client.middleware.auth.global", {
|
|
19
|
+
console.log("[Nuxt Users] client.middleware.auth.global", { isAuthenticated: isAuthenticated.value, to: to.path });
|
|
20
20
|
});
|
|
@@ -2,7 +2,7 @@ import { createError, defineEventHandler, readBody, setCookie } from "h3";
|
|
|
2
2
|
import bcrypt from "bcrypt";
|
|
3
3
|
import crypto from "node:crypto";
|
|
4
4
|
import { useRuntimeConfig } from "#imports";
|
|
5
|
-
import { useDb } from "
|
|
5
|
+
import { useDb } from "../../utils/index.js";
|
|
6
6
|
export default defineEventHandler(async (event) => {
|
|
7
7
|
const body = await readBody(event);
|
|
8
8
|
const { email, password } = body;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { defineEventHandler, getCookie, setCookie } from "h3";
|
|
2
2
|
import { useRuntimeConfig } from "#imports";
|
|
3
|
-
import { useDb } from "
|
|
3
|
+
import { useDb } from "../../utils/index.js";
|
|
4
4
|
export default defineEventHandler(async (event) => {
|
|
5
5
|
const { nuxtUsers } = useRuntimeConfig();
|
|
6
6
|
const options = nuxtUsers;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createError, defineEventHandler, getCookie, readBody } from "h3";
|
|
2
2
|
import bcrypt from "bcrypt";
|
|
3
3
|
import { useRuntimeConfig } from "#imports";
|
|
4
|
-
import { getCurrentUserFromToken, updateUserPassword } from "
|
|
5
|
-
import { validatePassword, getPasswordValidationOptions } from "
|
|
4
|
+
import { getCurrentUserFromToken, updateUserPassword } from "../../utils/index.js";
|
|
5
|
+
import { validatePassword, getPasswordValidationOptions } from "../../../../utils";
|
|
6
6
|
export default defineEventHandler(async (event) => {
|
|
7
7
|
const { nuxtUsers } = useRuntimeConfig();
|
|
8
8
|
const options = nuxtUsers;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createError, defineEventHandler, getCookie } from "h3";
|
|
2
2
|
import { useRuntimeConfig } from "#imports";
|
|
3
|
-
import { getCurrentUserFromToken } from "
|
|
3
|
+
import { getCurrentUserFromToken } from "../../utils/index.js";
|
|
4
4
|
export default defineEventHandler(async (event) => {
|
|
5
5
|
const { nuxtUsers } = useRuntimeConfig();
|
|
6
6
|
const options = nuxtUsers;
|
|
@@ -18,5 +18,5 @@ export default defineEventHandler(async (event) => {
|
|
|
18
18
|
statusMessage: "Unauthorized - Invalid authentication token"
|
|
19
19
|
});
|
|
20
20
|
}
|
|
21
|
-
return user;
|
|
21
|
+
return { user };
|
|
22
22
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { defineEventHandler, getCookie, sendRedirect } from "h3";
|
|
2
2
|
import { useRuntimeConfig } from "#imports";
|
|
3
3
|
import { getCurrentUserFromToken } from "../utils/index.js";
|
|
4
|
+
const noAuthPaths = ["/login", "/api/auth/login", "/api/auth/forgot-password"];
|
|
4
5
|
export default defineEventHandler(async (event) => {
|
|
5
6
|
const { nuxtUsers } = useRuntimeConfig();
|
|
6
7
|
const options = nuxtUsers;
|
|
@@ -8,7 +9,7 @@ export default defineEventHandler(async (event) => {
|
|
|
8
9
|
if (!isPageOrApiRoute) {
|
|
9
10
|
return;
|
|
10
11
|
}
|
|
11
|
-
if (
|
|
12
|
+
if (noAuthPaths.includes(event.path)) {
|
|
12
13
|
console.log("[Nuxt Users] server.middleware.auth.global: /login");
|
|
13
14
|
return;
|
|
14
15
|
}
|
|
@@ -26,6 +27,6 @@ export default defineEventHandler(async (event) => {
|
|
|
26
27
|
console.log(`[Nuxt Users] server.middleware.auth.global: ${event.path} No user found redirecting to login`);
|
|
27
28
|
return sendRedirect(event, "/login");
|
|
28
29
|
}
|
|
29
|
-
console.log(`[Nuxt Users] server.middleware.auth.global: Authenticated request for ${user.id}`);
|
|
30
|
+
console.log(`[Nuxt Users] server.middleware.auth.global: Authenticated request to ${event.path} for ${user.id}`);
|
|
30
31
|
return;
|
|
31
32
|
});
|
package/package.json
CHANGED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
<script setup>
|
|
2
|
-
import { ref, reactive } from "vue";
|
|
3
|
-
const formData = reactive({
|
|
4
|
-
email: ""
|
|
5
|
-
});
|
|
6
|
-
const message = ref("");
|
|
7
|
-
const loading = ref(false);
|
|
8
|
-
const isError = ref(false);
|
|
9
|
-
const handleForgotPassword = async () => {
|
|
10
|
-
loading.value = true;
|
|
11
|
-
message.value = "";
|
|
12
|
-
isError.value = false;
|
|
13
|
-
try {
|
|
14
|
-
const response = await fetch("/api/forgot-password", {
|
|
15
|
-
method: "POST",
|
|
16
|
-
headers: {
|
|
17
|
-
"Content-Type": "application/json",
|
|
18
|
-
"Accept": "application/json"
|
|
19
|
-
},
|
|
20
|
-
body: JSON.stringify({ email: formData.email })
|
|
21
|
-
});
|
|
22
|
-
const responseData = await response.json();
|
|
23
|
-
if (!response.ok) {
|
|
24
|
-
throw new Error(responseData.message || responseData.statusMessage || "An error occurred.");
|
|
25
|
-
}
|
|
26
|
-
message.value = responseData.message;
|
|
27
|
-
isError.value = false;
|
|
28
|
-
formData.email = "";
|
|
29
|
-
} catch (err) {
|
|
30
|
-
if (err instanceof Error) {
|
|
31
|
-
message.value = err.message;
|
|
32
|
-
} else {
|
|
33
|
-
message.value = "Failed to send password reset link.";
|
|
34
|
-
}
|
|
35
|
-
console.error("[Nuxt Users] Forgot Password Form Error:", err);
|
|
36
|
-
isError.value = true;
|
|
37
|
-
} finally {
|
|
38
|
-
loading.value = false;
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
</script>
|
|
42
|
-
|
|
43
|
-
<template>
|
|
44
|
-
<div>
|
|
45
|
-
<h2>Forgot Password</h2>
|
|
46
|
-
<form @submit.prevent="handleForgotPassword">
|
|
47
|
-
<div class="form-group">
|
|
48
|
-
<label for="email">Email</label>
|
|
49
|
-
<input
|
|
50
|
-
id="email"
|
|
51
|
-
v-model="formData.email"
|
|
52
|
-
type="email"
|
|
53
|
-
name="email"
|
|
54
|
-
placeholder="your@email.com"
|
|
55
|
-
required
|
|
56
|
-
>
|
|
57
|
-
</div>
|
|
58
|
-
<button
|
|
59
|
-
type="submit"
|
|
60
|
-
:disabled="loading"
|
|
61
|
-
>
|
|
62
|
-
{{ loading ? "Sending..." : "Send Password Reset Link" }}
|
|
63
|
-
</button>
|
|
64
|
-
<p
|
|
65
|
-
v-if="message"
|
|
66
|
-
:class="{ error: isError, success: !isError }"
|
|
67
|
-
class="form-message"
|
|
68
|
-
>
|
|
69
|
-
{{ message }}
|
|
70
|
-
</p>
|
|
71
|
-
</form>
|
|
72
|
-
</div>
|
|
73
|
-
</template>
|
|
74
|
-
|
|
75
|
-
<style scoped>
|
|
76
|
-
.error{color:red}.success{color:green}.form-message{font-size:.9rem;margin-top:1em}.form-group{margin-bottom:1em}label{display:block;font-weight:700;margin-bottom:.25em}input[type=email]{border:1px solid var(--color-border-dark);border-radius:4px;box-sizing:border-box;padding:.75em;width:100%}button[type=submit]{background-color:var(--color-gray-800);border:none;border-radius:4px;color:#fff;cursor:pointer;padding:.75em 1.5em}button[type=submit]:disabled{background-color:var(--color-gray-500);cursor:not-allowed}
|
|
77
|
-
</style>
|
|
@@ -1,240 +0,0 @@
|
|
|
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/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>
|
|
@@ -1,20 +0,0 @@
|
|
|
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;
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/dist/runtime/server/api/{update-password.post.d.ts → auth/update-password.post.d.ts}
RENAMED
|
File without changes
|