nuxt-users 1.11.0 → 1.13.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 +68 -0
- package/README.md +27 -59
- package/dist/cli.mjs +2 -3
- package/dist/module.d.mts +1 -1
- package/dist/module.d.ts +1 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +35 -9
- package/dist/runtime/assets/nuxt-users.css +1 -0
- package/dist/runtime/components/NUsersList.vue +136 -0
- package/dist/runtime/components/NUsersList.vue.d.ts +74 -0
- package/dist/runtime/components/{LoginForm.vue → NUsersLoginForm.vue} +20 -23
- package/dist/runtime/components/{LoginForm.vue.d.ts → NUsersLoginForm.vue.d.ts} +5 -9
- package/dist/runtime/components/{LogoutLink.vue → NUsersLogoutLink.vue} +2 -6
- package/dist/runtime/components/{PasswordStrengthIndicator.vue → NUsersPasswordStrengthIndicator.vue} +18 -22
- package/dist/runtime/components/{ProfileInfo.vue → NUsersProfileInfo.vue} +5 -7
- package/dist/runtime/components/{ResetPasswordForm.vue → NUsersResetPasswordForm.vue} +22 -26
- package/dist/runtime/components/{ResetPasswordForm.vue.d.ts → NUsersResetPasswordForm.vue.d.ts} +3 -6
- package/dist/runtime/components/NUsersUserCard.vue +93 -0
- package/dist/runtime/components/NUsersUserCard.vue.d.ts +33 -0
- package/dist/runtime/components/NUsersUserForm.vue +141 -0
- package/dist/runtime/components/NUsersUserForm.vue.d.ts +12 -0
- package/dist/runtime/composables/useAuthentication.d.ts +2 -0
- package/dist/runtime/composables/usePasswordValidation.d.ts +2 -2
- package/dist/runtime/middleware/authorization.client.js +1 -1
- package/dist/runtime/server/api/nuxt-users/index.get.d.ts +20 -0
- package/dist/runtime/server/api/nuxt-users/index.get.js +58 -0
- package/dist/runtime/server/api/nuxt-users/me.patch.d.ts +4 -0
- package/dist/runtime/server/api/nuxt-users/me.patch.js +33 -0
- package/dist/runtime/server/api/nuxt-users/session/index.post.d.ts +1 -0
- package/dist/runtime/server/middleware/authorization.server.js +24 -9
- package/dist/runtime/server/utils/user.js +20 -18
- package/dist/runtime/utils/permissions.d.ts +5 -3
- package/dist/runtime/utils/permissions.js +20 -4
- package/dist/shared/{nuxt-users.C7T1DKBV.d.ts → nuxt-users.u7N4meyI.d.mts} +8 -2
- package/dist/shared/{nuxt-users.C7T1DKBV.d.mts → nuxt-users.u7N4meyI.d.ts} +8 -2
- package/dist/utils.d.mts +1 -2
- package/dist/utils.d.ts +1 -2
- package/dist/utils.mjs +162 -2
- package/package.json +1 -1
- package/dist/runtime/assets/colors.css +0 -1
- package/dist/shared/nuxt-users.BnOQs2LI.mjs +0 -162
- package/dist/runtime/components/{LogoutLink.vue.d.ts → NUsersLogoutLink.vue.d.ts} +4 -4
- /package/dist/runtime/components/{PasswordStrengthIndicator.vue.d.ts → NUsersPasswordStrengthIndicator.vue.d.ts} +0 -0
- /package/dist/runtime/components/{ProfileInfo.vue.d.ts → NUsersProfileInfo.vue.d.ts} +0 -0
|
@@ -22,22 +22,18 @@ 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
|
-
success: (user: UserWithoutPassword) => any;
|
|
26
25
|
error: (error: string) => any;
|
|
27
|
-
"forgot-password-error": (error: string) => any;
|
|
28
26
|
submit: (data: LoginFormData) => any;
|
|
27
|
+
success: (user: UserWithoutPassword) => any;
|
|
28
|
+
"forgot-password-error": (error: string) => any;
|
|
29
29
|
"forgot-password-success": () => any;
|
|
30
30
|
}, string, import("vue").PublicProps, Readonly<LoginFormProps> & Readonly<{
|
|
31
|
-
onSuccess?: ((user: UserWithoutPassword) => any) | undefined;
|
|
32
31
|
onError?: ((error: string) => any) | undefined;
|
|
33
|
-
"onForgot-password-error"?: ((error: string) => any) | undefined;
|
|
34
32
|
onSubmit?: ((data: LoginFormData) => any) | undefined;
|
|
33
|
+
onSuccess?: ((user: UserWithoutPassword) => any) | undefined;
|
|
34
|
+
"onForgot-password-error"?: ((error: string) => any) | undefined;
|
|
35
35
|
"onForgot-password-success"?: (() => any) | undefined;
|
|
36
|
-
}>, {
|
|
37
|
-
apiEndpoint: string;
|
|
38
|
-
redirectTo: string;
|
|
39
|
-
forgotPasswordEndpoint: string;
|
|
40
|
-
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
36
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
41
37
|
declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
|
|
42
38
|
export default _default;
|
|
43
39
|
type __VLS_WithSlots<T, S> = T & {
|
|
@@ -50,7 +50,7 @@ const handleLogout = async (event) => {
|
|
|
50
50
|
>
|
|
51
51
|
<div
|
|
52
52
|
v-if="error"
|
|
53
|
-
:class="['error-message', props.errorClass]"
|
|
53
|
+
:class="['n-users-error-message', props.errorClass]"
|
|
54
54
|
:style="props.errorStyle"
|
|
55
55
|
>
|
|
56
56
|
{{ error }}
|
|
@@ -58,7 +58,7 @@ const handleLogout = async (event) => {
|
|
|
58
58
|
|
|
59
59
|
<a
|
|
60
60
|
href="#"
|
|
61
|
-
:class="['logout-link', { loading: isLoading }, props.linkClass]"
|
|
61
|
+
:class="['n-users-logout-link', { loading: isLoading }, props.linkClass]"
|
|
62
62
|
:style="props.linkStyle"
|
|
63
63
|
:disabled="isLoading"
|
|
64
64
|
@click="handleLogout"
|
|
@@ -70,7 +70,3 @@ const handleLogout = async (event) => {
|
|
|
70
70
|
</a>
|
|
71
71
|
</div>
|
|
72
72
|
</template>
|
|
73
|
-
|
|
74
|
-
<style scoped>
|
|
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}
|
|
76
|
-
</style>
|
|
@@ -11,7 +11,7 @@ const { public: { nuxtUsers } } = useRuntimeConfig();
|
|
|
11
11
|
const moduleOptions = nuxtUsers;
|
|
12
12
|
const hasErrors = computed(() => (props.validationResult?.errors?.length || 0) > 0);
|
|
13
13
|
const hasHints = computed(() => (props.validationResult?.hints?.length || 0) > 0);
|
|
14
|
-
const hasPassword = computed(() => props.password
|
|
14
|
+
const hasPassword = computed(() => props.password?.length > 0);
|
|
15
15
|
const validationRules = computed(() => {
|
|
16
16
|
if (!props.password) return [];
|
|
17
17
|
const rules = [];
|
|
@@ -57,11 +57,11 @@ const validationRules = computed(() => {
|
|
|
57
57
|
<template>
|
|
58
58
|
<div
|
|
59
59
|
v-if="hasPassword"
|
|
60
|
-
class="password-strength"
|
|
60
|
+
class="n-users-password-strength"
|
|
61
61
|
>
|
|
62
|
-
<div class="strength-bar">
|
|
62
|
+
<div class="n-users-strength-bar">
|
|
63
63
|
<div
|
|
64
|
-
class="strength-fill"
|
|
64
|
+
class="n-users-strength-fill"
|
|
65
65
|
:style="{
|
|
66
66
|
width: `${validationResult?.score || 0}%`,
|
|
67
67
|
backgroundColor: validationResult?.strength === 'weak' ? '#dc3545' : validationResult?.strength === 'medium' ? '#ffc107' : validationResult?.strength === 'strong' ? '#28a745' : '#6c757d'
|
|
@@ -69,7 +69,7 @@ const validationRules = computed(() => {
|
|
|
69
69
|
/>
|
|
70
70
|
</div>
|
|
71
71
|
<span
|
|
72
|
-
class="strength-text"
|
|
72
|
+
class="n-users-strength-text"
|
|
73
73
|
:style="{
|
|
74
74
|
color: validationResult?.strength === 'weak' ? '#dc3545' : validationResult?.strength === 'medium' ? '#ffc107' : validationResult?.strength === 'strong' ? '#28a745' : '#6c757d'
|
|
75
75
|
}"
|
|
@@ -82,22 +82,22 @@ const validationRules = computed(() => {
|
|
|
82
82
|
<!-- Validation rules -->
|
|
83
83
|
<div
|
|
84
84
|
v-if="showRules && hasPassword"
|
|
85
|
-
class="validation-rules"
|
|
85
|
+
class="n-users-validation-rules n-users-validation-container"
|
|
86
86
|
>
|
|
87
|
-
<div class="rules-title">
|
|
87
|
+
<div class="n-users-rules-title n-users-container-title">
|
|
88
88
|
Password Requirements:
|
|
89
89
|
</div>
|
|
90
|
-
<ul class="rules-list">
|
|
90
|
+
<ul class="n-users-rules-list n-users-list-unstyled">
|
|
91
91
|
<li
|
|
92
92
|
v-for="rule in validationRules"
|
|
93
93
|
:key="rule.text"
|
|
94
|
-
class="rule-item"
|
|
95
|
-
:class="{ 'rule-passed': rule.passed, 'rule-failed': !rule.passed }"
|
|
94
|
+
class="n-users-rule-item n-users-list-item"
|
|
95
|
+
:class="{ 'n-users-rule-passed': rule.passed, 'n-users-rule-failed': !rule.passed }"
|
|
96
96
|
>
|
|
97
|
-
<span class="rule-icon">
|
|
97
|
+
<span class="n-users-rule-icon n-users-icon">
|
|
98
98
|
{{ rule.passed ? "\u2713" : "\u2717" }}
|
|
99
99
|
</span>
|
|
100
|
-
<span class="rule-text">
|
|
100
|
+
<span class="n-users-rule-text n-users-text">
|
|
101
101
|
{{ rule.text }}
|
|
102
102
|
</span>
|
|
103
103
|
</li>
|
|
@@ -107,12 +107,12 @@ const validationRules = computed(() => {
|
|
|
107
107
|
<!-- Validation errors -->
|
|
108
108
|
<div
|
|
109
109
|
v-if="hasErrors"
|
|
110
|
-
class="validation-errors"
|
|
110
|
+
class="n-users-validation-errors"
|
|
111
111
|
>
|
|
112
112
|
<small
|
|
113
113
|
v-for="error in validationResult?.errors || []"
|
|
114
114
|
:key="String(error)"
|
|
115
|
-
class="error-text"
|
|
115
|
+
class="n-users-error-text"
|
|
116
116
|
>
|
|
117
117
|
{{ error }}
|
|
118
118
|
</small>
|
|
@@ -121,23 +121,19 @@ const validationRules = computed(() => {
|
|
|
121
121
|
<!-- Password improvement hints -->
|
|
122
122
|
<div
|
|
123
123
|
v-if="showHints && hasPassword && hasHints"
|
|
124
|
-
class="password-hints"
|
|
124
|
+
class="n-users-password-hints n-users-validation-container"
|
|
125
125
|
>
|
|
126
|
-
<div class="hint-title">
|
|
126
|
+
<div class="n-users-hint-title n-users-container-title">
|
|
127
127
|
How to make your password stronger:
|
|
128
128
|
</div>
|
|
129
|
-
<ul class="hint-list">
|
|
129
|
+
<ul class="n-users-hint-list n-users-list-unstyled">
|
|
130
130
|
<li
|
|
131
131
|
v-for="hint in validationResult?.hints || []"
|
|
132
132
|
:key="'hint-' + hint"
|
|
133
|
-
class="hint-item"
|
|
133
|
+
class="n-users-hint-item n-users-list-item"
|
|
134
134
|
>
|
|
135
135
|
{{ hint }}
|
|
136
136
|
</li>
|
|
137
137
|
</ul>
|
|
138
138
|
</div>
|
|
139
139
|
</template>
|
|
140
|
-
|
|
141
|
-
<style scoped>
|
|
142
|
-
.password-strength{margin-top:8px}.strength-bar{background-color:#e9ecef;border-radius:2px;height:4px;margin-bottom:4px;overflow:hidden;width:100%}.strength-fill{height:100%;transition:width .3s ease,background-color .3s ease}.strength-text{font-size:.75rem;font-weight:500}.validation-rules{background-color:#f8f9fa;border:1px solid #e9ecef;border-radius:4px;margin-top:12px;padding:12px}.rules-title{color:#495057;font-size:.875rem;font-weight:600;margin-bottom:8px}.rules-list{list-style:none;margin:0;padding-left:0}.rule-item{align-items:center;display:flex;font-size:.875rem;margin-bottom:6px;transition:color .2s ease}.rule-item:last-child{margin-bottom:0}.rule-passed{color:#28a745}.rule-failed{color:#dc3545}.rule-icon{font-weight:700;margin-right:8px;min-width:16px;text-align:center}.rule-text{flex:1}.validation-errors{margin-top:8px}.error-text{color:#dc3545;display:block;font-size:.875rem;margin-bottom:4px}.error-text:last-child{margin-bottom:0}.password-hints{background-color:#f8f9fa;border:1px solid #e9ecef;border-radius:4px;margin-top:8px;padding:12px}.hint-title{color:#495057;font-size:.875rem;font-weight:600;margin-bottom:8px}.hint-list{margin:0;padding-left:16px}.hint-item{color:#6c757d;font-size:.875rem;margin-bottom:4px}.hint-item:last-child{margin-bottom:0}
|
|
143
|
-
</style>
|
|
@@ -4,11 +4,13 @@ const { user } = useAuthentication();
|
|
|
4
4
|
</script>
|
|
5
5
|
|
|
6
6
|
<template>
|
|
7
|
-
<div>
|
|
8
|
-
<h2
|
|
7
|
+
<div class="n-users-profile-info n-users-section">
|
|
8
|
+
<h2 class="n-users-section-header">
|
|
9
|
+
Profile Information
|
|
10
|
+
</h2>
|
|
9
11
|
<dl
|
|
10
12
|
v-if="user"
|
|
11
|
-
class="profile-
|
|
13
|
+
class="n-users-profile-details n-users-grid n-users-grid-2"
|
|
12
14
|
>
|
|
13
15
|
<dt>Name:</dt>
|
|
14
16
|
<dd>{{ user.name }}</dd>
|
|
@@ -27,7 +29,3 @@ const { user } = useAuthentication();
|
|
|
27
29
|
</dl>
|
|
28
30
|
</div>
|
|
29
31
|
</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>
|
|
@@ -2,13 +2,12 @@
|
|
|
2
2
|
import { ref, watch } from "vue";
|
|
3
3
|
import { usePasswordValidation } from "../composables/usePasswordValidation";
|
|
4
4
|
import { useRuntimeConfig } from "#imports";
|
|
5
|
-
import
|
|
5
|
+
import NUsersPasswordStrengthIndicator from "./NUsersPasswordStrengthIndicator.vue";
|
|
6
6
|
const { public: { nuxtUsers } } = useRuntimeConfig();
|
|
7
|
-
const apiBasePath = nuxtUsers?.apiBasePath;
|
|
8
7
|
const moduleOptions = nuxtUsers;
|
|
9
8
|
const props = defineProps({
|
|
10
|
-
apiEndpoint: { type: String, required: false
|
|
11
|
-
updatePasswordEndpoint: { type: String, required: false
|
|
9
|
+
apiEndpoint: { type: String, required: false },
|
|
10
|
+
updatePasswordEndpoint: { type: String, required: false }
|
|
12
11
|
});
|
|
13
12
|
const emit = defineEmits(["success", "error", "password-error", "password-updated"]);
|
|
14
13
|
const isPasswordLoading = ref(false);
|
|
@@ -32,7 +31,7 @@ const updatePassword = async () => {
|
|
|
32
31
|
passwordError.value = "";
|
|
33
32
|
passwordSuccess.value = "";
|
|
34
33
|
try {
|
|
35
|
-
await $fetch(props.updatePasswordEndpoint, {
|
|
34
|
+
await $fetch(props.updatePasswordEndpoint || nuxtUsers.apiBasePath + "/password", {
|
|
36
35
|
method: "PATCH",
|
|
37
36
|
body: passwordForm.value
|
|
38
37
|
});
|
|
@@ -54,48 +53,49 @@ const updatePassword = async () => {
|
|
|
54
53
|
</script>
|
|
55
54
|
|
|
56
55
|
<template>
|
|
57
|
-
<div>
|
|
58
|
-
<h2
|
|
56
|
+
<div class="n-users-section">
|
|
57
|
+
<h2 class="n-users-section-header">
|
|
58
|
+
Change Password
|
|
59
|
+
</h2>
|
|
59
60
|
|
|
60
61
|
<form @submit.prevent="updatePassword">
|
|
61
|
-
<div class="form-group">
|
|
62
|
+
<div class="n-users-form-group">
|
|
62
63
|
<label
|
|
63
64
|
for="currentPassword"
|
|
64
|
-
class="form-label"
|
|
65
|
+
class="n-users-form-label"
|
|
65
66
|
>Current Password</label>
|
|
66
67
|
<input
|
|
67
68
|
id="currentPassword"
|
|
68
69
|
v-model="passwordForm.currentPassword"
|
|
69
70
|
type="password"
|
|
70
|
-
class="form-input"
|
|
71
|
+
class="n-users-form-input"
|
|
71
72
|
required
|
|
72
73
|
:disabled="isPasswordLoading"
|
|
73
74
|
>
|
|
74
75
|
</div>
|
|
75
76
|
|
|
76
|
-
<div class="form-group">
|
|
77
|
+
<div class="n-users-form-group">
|
|
77
78
|
<label
|
|
78
79
|
for="newPassword"
|
|
79
|
-
class="form-label"
|
|
80
|
+
class="n-users-form-label"
|
|
80
81
|
>New Password</label>
|
|
81
82
|
<input
|
|
82
83
|
id="newPassword"
|
|
83
84
|
v-model="passwordForm.newPassword"
|
|
84
85
|
type="password"
|
|
85
|
-
class="form-input"
|
|
86
|
+
class="n-users-form-input"
|
|
86
87
|
required
|
|
87
88
|
:minlength="moduleOptions.passwordValidation?.minLength || 8"
|
|
88
89
|
:class="{ error: passwordValidation.errors.value.length > 0 && passwordForm.newPassword }"
|
|
89
90
|
:disabled="isPasswordLoading"
|
|
90
91
|
>
|
|
91
92
|
|
|
92
|
-
<
|
|
93
|
+
<NUsersPasswordStrengthIndicator
|
|
93
94
|
:password="passwordForm.newPassword"
|
|
94
95
|
:validation-result="passwordValidation.validationResult.value"
|
|
95
96
|
/>
|
|
96
97
|
|
|
97
|
-
|
|
98
|
-
<small class="form-help">
|
|
98
|
+
<small class="n-users-form-help">
|
|
99
99
|
Password must contain at least {{ moduleOptions.passwordValidation?.minLength || 8 }} characters
|
|
100
100
|
<span v-if="moduleOptions.passwordValidation?.requireUppercase">, including uppercase letters</span>
|
|
101
101
|
<span v-if="moduleOptions.passwordValidation?.requireLowercase">, lowercase letters</span>
|
|
@@ -104,16 +104,16 @@ const updatePassword = async () => {
|
|
|
104
104
|
</small>
|
|
105
105
|
</div>
|
|
106
106
|
|
|
107
|
-
<div class="form-group">
|
|
107
|
+
<div class="n-users-form-group">
|
|
108
108
|
<label
|
|
109
109
|
for="newPasswordConfirmation"
|
|
110
|
-
class="form-label"
|
|
110
|
+
class="n-users-form-label"
|
|
111
111
|
>Confirm New Password</label>
|
|
112
112
|
<input
|
|
113
113
|
id="newPasswordConfirmation"
|
|
114
114
|
v-model="passwordForm.newPasswordConfirmation"
|
|
115
115
|
type="password"
|
|
116
|
-
class="form-input"
|
|
116
|
+
class="n-users-form-input"
|
|
117
117
|
required
|
|
118
118
|
:disabled="isPasswordLoading"
|
|
119
119
|
>
|
|
@@ -121,21 +121,21 @@ const updatePassword = async () => {
|
|
|
121
121
|
|
|
122
122
|
<div
|
|
123
123
|
v-if="passwordError"
|
|
124
|
-
class="error-message"
|
|
124
|
+
class="n-users-error-message"
|
|
125
125
|
>
|
|
126
126
|
{{ passwordError }}
|
|
127
127
|
</div>
|
|
128
128
|
|
|
129
129
|
<div
|
|
130
130
|
v-if="passwordSuccess"
|
|
131
|
-
class="success-message"
|
|
131
|
+
class="n-users-success-message"
|
|
132
132
|
>
|
|
133
133
|
{{ passwordSuccess }}
|
|
134
134
|
</div>
|
|
135
135
|
|
|
136
136
|
<button
|
|
137
137
|
type="submit"
|
|
138
|
-
class="btn btn-primary"
|
|
138
|
+
class="n-users-btn n-users-btn-primary"
|
|
139
139
|
:disabled="isPasswordLoading"
|
|
140
140
|
>
|
|
141
141
|
<span v-if="isPasswordLoading">Updating...</span>
|
|
@@ -144,7 +144,3 @@ const updatePassword = async () => {
|
|
|
144
144
|
</form>
|
|
145
145
|
</div>
|
|
146
146
|
</template>
|
|
147
|
-
|
|
148
|
-
<style scoped>
|
|
149
|
-
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}
|
|
150
|
-
</style>
|
package/dist/runtime/components/{ResetPasswordForm.vue.d.ts → NUsersResetPasswordForm.vue.d.ts}
RENAMED
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
import type { UserWithoutPassword, ResetPasswordFormProps } from '../../types.js';
|
|
2
2
|
declare const _default: import("vue").DefineComponent<ResetPasswordFormProps, void, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
3
|
-
success: (user: UserWithoutPassword) => any;
|
|
4
3
|
error: (error: string) => any;
|
|
4
|
+
success: (user: UserWithoutPassword) => any;
|
|
5
5
|
"password-error": (error: string) => any;
|
|
6
6
|
"password-updated": () => any;
|
|
7
7
|
}, string, import("vue").PublicProps, Readonly<ResetPasswordFormProps> & Readonly<{
|
|
8
|
-
onSuccess?: ((user: UserWithoutPassword) => any) | undefined;
|
|
9
8
|
onError?: ((error: string) => any) | undefined;
|
|
9
|
+
onSuccess?: ((user: UserWithoutPassword) => any) | undefined;
|
|
10
10
|
"onPassword-error"?: ((error: string) => any) | undefined;
|
|
11
11
|
"onPassword-updated"?: (() => any) | undefined;
|
|
12
|
-
}>, {
|
|
13
|
-
apiEndpoint: string;
|
|
14
|
-
updatePasswordEndpoint: string;
|
|
15
|
-
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
12
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
16
13
|
export default _default;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
import { useAuthentication } from "../composables/useAuthentication";
|
|
4
|
+
import { useRuntimeConfig } from "#imports";
|
|
5
|
+
import { defaultDisplayFields, defaultFieldLabels } from "../../types";
|
|
6
|
+
const props = defineProps({
|
|
7
|
+
user: { type: Object, required: true },
|
|
8
|
+
index: { type: Number, required: true },
|
|
9
|
+
displayFields: { type: Array, required: false, default: () => defaultDisplayFields },
|
|
10
|
+
fieldLabels: { type: Object, required: false, default: () => defaultFieldLabels }
|
|
11
|
+
});
|
|
12
|
+
const emit = defineEmits(["delete", "editClick"]);
|
|
13
|
+
const { user: currentUser } = useAuthentication();
|
|
14
|
+
const { public: { nuxtUsers } } = useRuntimeConfig();
|
|
15
|
+
const canEdit = computed(() => {
|
|
16
|
+
if (!currentUser.value) return false;
|
|
17
|
+
if (nuxtUsers.auth?.permissions) {
|
|
18
|
+
const userPermissions = nuxtUsers.auth.permissions[currentUser.value.role] || [];
|
|
19
|
+
const apiBasePath = nuxtUsers.apiBasePath;
|
|
20
|
+
return userPermissions.some(
|
|
21
|
+
(permission) => permission === "*" || permission === `${apiBasePath}/*` || permission === `${apiBasePath}/[id].delete`
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
});
|
|
26
|
+
const canDelete = computed(() => {
|
|
27
|
+
if (!currentUser.value) return false;
|
|
28
|
+
if (nuxtUsers.auth?.permissions) {
|
|
29
|
+
const userPermissions = nuxtUsers.auth.permissions[currentUser.value.role] || [];
|
|
30
|
+
const apiBasePath = nuxtUsers.apiBasePath;
|
|
31
|
+
return userPermissions.some(
|
|
32
|
+
(permission) => permission === "*" || permission === `${apiBasePath}/*` || permission === `${apiBasePath}/[id].delete`
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
});
|
|
37
|
+
const editUser = async (user) => {
|
|
38
|
+
emit("editClick", user);
|
|
39
|
+
};
|
|
40
|
+
const deleteUser = async (user) => {
|
|
41
|
+
if (!confirm(`Are you sure you want to delete user ${user.name}?`)) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
await $fetch(`${nuxtUsers.apiBasePath}/${user.id}`, { method: "DELETE" });
|
|
46
|
+
console.log(`User ${user.name} deleted successfully`);
|
|
47
|
+
emit("delete", user);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.error("Failed to delete user:", error);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
</script>
|
|
53
|
+
|
|
54
|
+
<template>
|
|
55
|
+
<slot
|
|
56
|
+
name="userCard"
|
|
57
|
+
:user="user"
|
|
58
|
+
:index="index"
|
|
59
|
+
>
|
|
60
|
+
<dl class="n-users-user-card n-users-grid n-users-grid-2">
|
|
61
|
+
<template
|
|
62
|
+
v-for="field in props.displayFields"
|
|
63
|
+
:key="field"
|
|
64
|
+
>
|
|
65
|
+
<dt>{{ props.fieldLabels[field] || field }}:</dt>
|
|
66
|
+
<dd>{{ user[field] }}</dd>
|
|
67
|
+
</template>
|
|
68
|
+
</dl>
|
|
69
|
+
|
|
70
|
+
<div
|
|
71
|
+
class="n-users-user-card-actions"
|
|
72
|
+
>
|
|
73
|
+
<button
|
|
74
|
+
v-if="canEdit"
|
|
75
|
+
class="n-users-edit-btn"
|
|
76
|
+
@click="editUser(user)"
|
|
77
|
+
>
|
|
78
|
+
<slot name="editButton">
|
|
79
|
+
Edit
|
|
80
|
+
</slot>
|
|
81
|
+
</button>
|
|
82
|
+
<button
|
|
83
|
+
v-if="canDelete"
|
|
84
|
+
class="n-users-delete-btn"
|
|
85
|
+
@click="deleteUser(user)"
|
|
86
|
+
>
|
|
87
|
+
<slot name="deleteButton">
|
|
88
|
+
Delete
|
|
89
|
+
</slot>
|
|
90
|
+
</button>
|
|
91
|
+
</div>
|
|
92
|
+
</slot>
|
|
93
|
+
</template>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type DisplayFieldsProps, type User } from '../../types.js';
|
|
2
|
+
interface Props extends DisplayFieldsProps {
|
|
3
|
+
user: User;
|
|
4
|
+
index: number;
|
|
5
|
+
}
|
|
6
|
+
declare var __VLS_1: {
|
|
7
|
+
user: User;
|
|
8
|
+
index: number;
|
|
9
|
+
}, __VLS_3: {}, __VLS_5: {};
|
|
10
|
+
type __VLS_Slots = {} & {
|
|
11
|
+
userCard?: (props: typeof __VLS_1) => any;
|
|
12
|
+
} & {
|
|
13
|
+
editButton?: (props: typeof __VLS_3) => any;
|
|
14
|
+
} & {
|
|
15
|
+
deleteButton?: (props: typeof __VLS_5) => any;
|
|
16
|
+
};
|
|
17
|
+
declare const __VLS_component: import("vue").DefineComponent<Props, void, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
18
|
+
delete: (user: User) => void;
|
|
19
|
+
editClick: (user: User) => void;
|
|
20
|
+
}, string, import("vue").PublicProps, Readonly<Props> & Readonly<{
|
|
21
|
+
onDelete?: ((user: User) => any) | undefined;
|
|
22
|
+
onEditClick?: ((user: User) => any) | undefined;
|
|
23
|
+
}>, {
|
|
24
|
+
displayFields: string[];
|
|
25
|
+
fieldLabels: Record<string, string>;
|
|
26
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
27
|
+
declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
|
|
28
|
+
export default _default;
|
|
29
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
30
|
+
new (): {
|
|
31
|
+
$slots: S;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, computed, watch } from "vue";
|
|
3
|
+
import { usePasswordValidation } from "../composables/usePasswordValidation";
|
|
4
|
+
import { useRuntimeConfig } from "#imports";
|
|
5
|
+
const props = defineProps({
|
|
6
|
+
user: { type: [Object, null], required: false }
|
|
7
|
+
});
|
|
8
|
+
const emit = defineEmits(["submit", "cancel"]);
|
|
9
|
+
const emptyFormData = {
|
|
10
|
+
name: "",
|
|
11
|
+
email: "",
|
|
12
|
+
role: "user",
|
|
13
|
+
password: ""
|
|
14
|
+
};
|
|
15
|
+
const formData = ref(props.user ?? emptyFormData);
|
|
16
|
+
const { public: { nuxtUsers } } = useRuntimeConfig();
|
|
17
|
+
const moduleOptions = nuxtUsers;
|
|
18
|
+
const passwordValidation = usePasswordValidation(moduleOptions);
|
|
19
|
+
const nameError = ref("");
|
|
20
|
+
const isEditMode = computed(() => !!props.user);
|
|
21
|
+
const title = computed(() => isEditMode.value ? "Edit User" : "Create User");
|
|
22
|
+
const submitText = computed(() => isEditMode.value ? "Update" : "Create");
|
|
23
|
+
watch(() => props.user, (newUser) => {
|
|
24
|
+
if (newUser) {
|
|
25
|
+
formData.value = { ...newUser };
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
watch(() => formData.value.name, (name) => {
|
|
29
|
+
if (!name || name.trim().length < 3) {
|
|
30
|
+
nameError.value = "Name must be at least 3 characters long";
|
|
31
|
+
} else {
|
|
32
|
+
nameError.value = "";
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
watch(() => formData.value.password, (newPassword) => {
|
|
36
|
+
if (newPassword) {
|
|
37
|
+
passwordValidation.validate(newPassword);
|
|
38
|
+
} else {
|
|
39
|
+
passwordValidation.clearValidation();
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
const handleSubmit = async () => {
|
|
43
|
+
const userData = { ...formData.value };
|
|
44
|
+
if (!userData.name || userData.name.trim().length < 3) {
|
|
45
|
+
console.log("name is too short");
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (isEditMode.value && !userData.password) {
|
|
49
|
+
delete userData.password;
|
|
50
|
+
}
|
|
51
|
+
if (userData.password && !passwordValidation.validationResult.value?.isValid) {
|
|
52
|
+
console.log("password validation failed", passwordValidation.validationResult.value);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
if (isEditMode.value) {
|
|
57
|
+
await $fetch(`${nuxtUsers.apiBasePath}/${props.user.id}`, {
|
|
58
|
+
method: "PATCH",
|
|
59
|
+
body: userData
|
|
60
|
+
});
|
|
61
|
+
} else {
|
|
62
|
+
await $fetch(nuxtUsers.apiBasePath, {
|
|
63
|
+
method: "POST",
|
|
64
|
+
body: userData
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
emit("submit", userData);
|
|
68
|
+
formData.value = { ...emptyFormData };
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error("Failed to save user:", error);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
</script>
|
|
74
|
+
|
|
75
|
+
<template>
|
|
76
|
+
<div class="n-users-user-form">
|
|
77
|
+
<h3>{{ title }}</h3>
|
|
78
|
+
|
|
79
|
+
<form @submit.prevent="handleSubmit">
|
|
80
|
+
<div class="n-users-form-group">
|
|
81
|
+
<label for="name">Name:</label>
|
|
82
|
+
<input
|
|
83
|
+
v-model="formData.name"
|
|
84
|
+
name="name"
|
|
85
|
+
type="text"
|
|
86
|
+
minlength="3"
|
|
87
|
+
required
|
|
88
|
+
:class="{ error: nameError }"
|
|
89
|
+
>
|
|
90
|
+
<small
|
|
91
|
+
v-if="nameError"
|
|
92
|
+
class="n-users-error-text"
|
|
93
|
+
>
|
|
94
|
+
{{ nameError }}
|
|
95
|
+
</small>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<div class="n-users-form-group">
|
|
99
|
+
<label for="email">Email:</label>
|
|
100
|
+
<input
|
|
101
|
+
v-model="formData.email"
|
|
102
|
+
type="email"
|
|
103
|
+
required
|
|
104
|
+
>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<div class="n-users-form-group">
|
|
108
|
+
<label for="role">Role:</label>
|
|
109
|
+
<input
|
|
110
|
+
v-model="formData.role"
|
|
111
|
+
type="text"
|
|
112
|
+
>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<div class="n-users-form-group">
|
|
116
|
+
<label for="password">
|
|
117
|
+
{{ isEditMode ? "Password (leave blank to keep current)" : "Password:" }}
|
|
118
|
+
</label>
|
|
119
|
+
<input
|
|
120
|
+
v-model="formData.password"
|
|
121
|
+
type="password"
|
|
122
|
+
:required="!isEditMode"
|
|
123
|
+
>
|
|
124
|
+
|
|
125
|
+
<NUsersPasswordStrengthIndicator
|
|
126
|
+
:password="formData.password"
|
|
127
|
+
:validation-result="passwordValidation.validationResult.value"
|
|
128
|
+
/>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<div class="n-users-form-actions">
|
|
132
|
+
<button
|
|
133
|
+
type="submit"
|
|
134
|
+
class="n-users-submit-btn"
|
|
135
|
+
>
|
|
136
|
+
{{ submitText }}
|
|
137
|
+
</button>
|
|
138
|
+
</div>
|
|
139
|
+
</form>
|
|
140
|
+
</div>
|
|
141
|
+
</template>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { User } from '../../types.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
user?: User | null;
|
|
4
|
+
};
|
|
5
|
+
declare const _default: import("vue").DefineComponent<__VLS_Props, void, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
6
|
+
cancel: () => any;
|
|
7
|
+
submit: (userData: Partial<User>) => any;
|
|
8
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
9
|
+
onCancel?: (() => any) | undefined;
|
|
10
|
+
onSubmit?: ((userData: Partial<User>) => any) | undefined;
|
|
11
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
12
|
+
export default _default;
|
|
@@ -7,6 +7,7 @@ export declare const useAuthentication: () => {
|
|
|
7
7
|
readonly role: string;
|
|
8
8
|
readonly created_at: string;
|
|
9
9
|
readonly updated_at: string;
|
|
10
|
+
readonly last_login_at?: string | undefined;
|
|
10
11
|
} | null, {
|
|
11
12
|
readonly id: number;
|
|
12
13
|
readonly email: string;
|
|
@@ -14,6 +15,7 @@ export declare const useAuthentication: () => {
|
|
|
14
15
|
readonly role: string;
|
|
15
16
|
readonly created_at: string;
|
|
16
17
|
readonly updated_at: string;
|
|
18
|
+
readonly last_login_at?: string | undefined;
|
|
17
19
|
} | null>>;
|
|
18
20
|
isAuthenticated: import("vue").ComputedRef<boolean>;
|
|
19
21
|
login: (userData: User) => void;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type PasswordValidationResult, type PasswordValidationOptions } from '../../utils/index.js';
|
|
2
|
-
import type {
|
|
3
|
-
export declare const usePasswordValidation: (moduleOptions?:
|
|
2
|
+
import type { RuntimeModuleOptions } from '../../types.js';
|
|
3
|
+
export declare const usePasswordValidation: (moduleOptions?: RuntimeModuleOptions, options?: PasswordValidationOptions) => {
|
|
4
4
|
password: import("vue").Ref<string, string>;
|
|
5
5
|
validationResult: import("vue").Ref<{
|
|
6
6
|
isValid: boolean;
|
|
@@ -23,7 +23,7 @@ export default defineNuxtRouteMiddleware((to) => {
|
|
|
23
23
|
console.log(`[Nuxt Users] client.middleware.auth.global: Unauthenticated ${to.path}, redirecting to /login`);
|
|
24
24
|
return navigateTo("/login");
|
|
25
25
|
}
|
|
26
|
-
if (!user.value || !hasPermission(user.value.role, to.path, publicOptions.auth.permissions)) {
|
|
26
|
+
if (!user.value || !hasPermission(user.value.role, to.path, "GET", publicOptions.auth.permissions)) {
|
|
27
27
|
console.log(`[Nuxt Users] client.middleware.auth.global: User with role ${user.value?.role} denied access to ${to.path}`);
|
|
28
28
|
return navigateTo("/login");
|
|
29
29
|
}
|