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.
Files changed (44) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/README.md +27 -59
  3. package/dist/cli.mjs +2 -3
  4. package/dist/module.d.mts +1 -1
  5. package/dist/module.d.ts +1 -1
  6. package/dist/module.json +1 -1
  7. package/dist/module.mjs +35 -9
  8. package/dist/runtime/assets/nuxt-users.css +1 -0
  9. package/dist/runtime/components/NUsersList.vue +136 -0
  10. package/dist/runtime/components/NUsersList.vue.d.ts +74 -0
  11. package/dist/runtime/components/{LoginForm.vue → NUsersLoginForm.vue} +20 -23
  12. package/dist/runtime/components/{LoginForm.vue.d.ts → NUsersLoginForm.vue.d.ts} +5 -9
  13. package/dist/runtime/components/{LogoutLink.vue → NUsersLogoutLink.vue} +2 -6
  14. package/dist/runtime/components/{PasswordStrengthIndicator.vue → NUsersPasswordStrengthIndicator.vue} +18 -22
  15. package/dist/runtime/components/{ProfileInfo.vue → NUsersProfileInfo.vue} +5 -7
  16. package/dist/runtime/components/{ResetPasswordForm.vue → NUsersResetPasswordForm.vue} +22 -26
  17. package/dist/runtime/components/{ResetPasswordForm.vue.d.ts → NUsersResetPasswordForm.vue.d.ts} +3 -6
  18. package/dist/runtime/components/NUsersUserCard.vue +93 -0
  19. package/dist/runtime/components/NUsersUserCard.vue.d.ts +33 -0
  20. package/dist/runtime/components/NUsersUserForm.vue +141 -0
  21. package/dist/runtime/components/NUsersUserForm.vue.d.ts +12 -0
  22. package/dist/runtime/composables/useAuthentication.d.ts +2 -0
  23. package/dist/runtime/composables/usePasswordValidation.d.ts +2 -2
  24. package/dist/runtime/middleware/authorization.client.js +1 -1
  25. package/dist/runtime/server/api/nuxt-users/index.get.d.ts +20 -0
  26. package/dist/runtime/server/api/nuxt-users/index.get.js +58 -0
  27. package/dist/runtime/server/api/nuxt-users/me.patch.d.ts +4 -0
  28. package/dist/runtime/server/api/nuxt-users/me.patch.js +33 -0
  29. package/dist/runtime/server/api/nuxt-users/session/index.post.d.ts +1 -0
  30. package/dist/runtime/server/middleware/authorization.server.js +24 -9
  31. package/dist/runtime/server/utils/user.js +20 -18
  32. package/dist/runtime/utils/permissions.d.ts +5 -3
  33. package/dist/runtime/utils/permissions.js +20 -4
  34. package/dist/shared/{nuxt-users.C7T1DKBV.d.ts → nuxt-users.u7N4meyI.d.mts} +8 -2
  35. package/dist/shared/{nuxt-users.C7T1DKBV.d.mts → nuxt-users.u7N4meyI.d.ts} +8 -2
  36. package/dist/utils.d.mts +1 -2
  37. package/dist/utils.d.ts +1 -2
  38. package/dist/utils.mjs +162 -2
  39. package/package.json +1 -1
  40. package/dist/runtime/assets/colors.css +0 -1
  41. package/dist/shared/nuxt-users.BnOQs2LI.mjs +0 -162
  42. package/dist/runtime/components/{LogoutLink.vue.d.ts → NUsersLogoutLink.vue.d.ts} +4 -4
  43. /package/dist/runtime/components/{PasswordStrengthIndicator.vue.d.ts → NUsersPasswordStrengthIndicator.vue.d.ts} +0 -0
  44. /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.length > 0);
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>Profile Information</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-info"
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 PasswordStrengthIndicator from "./PasswordStrengthIndicator.vue";
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, default: `${apiBasePath}/me` },
11
- updatePasswordEndpoint: { type: String, required: false, default: `${apiBasePath}/password` }
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>Change Password</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
- <PasswordStrengthIndicator
93
+ <NUsersPasswordStrengthIndicator
93
94
  :password="passwordForm.newPassword"
94
95
  :validation-result="passwordValidation.validationResult.value"
95
96
  />
96
97
 
97
- <!-- Password requirements -->
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>
@@ -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 { ModuleOptions } from '../../types.js';
3
- export declare const usePasswordValidation: (moduleOptions?: ModuleOptions, options?: PasswordValidationOptions) => {
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
  }