nuxt-users 1.10.0 → 1.12.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 (53) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/dist/cli.mjs +2 -3
  3. package/dist/module.d.mts +1 -1
  4. package/dist/module.d.ts +1 -1
  5. package/dist/module.json +2 -2
  6. package/dist/module.mjs +55 -30
  7. package/dist/runtime/assets/nuxt-users.css +1 -0
  8. package/dist/runtime/components/NUsersList.vue +136 -0
  9. package/dist/runtime/components/NUsersList.vue.d.ts +74 -0
  10. package/dist/runtime/components/{LoginForm.vue → NUsersLoginForm.vue} +20 -22
  11. package/dist/runtime/components/{LoginForm.vue.d.ts → NUsersLoginForm.vue.d.ts} +5 -9
  12. package/dist/runtime/components/{LogoutLink.vue → NUsersLogoutLink.vue} +2 -6
  13. package/dist/runtime/components/{PasswordStrengthIndicator.vue → NUsersPasswordStrengthIndicator.vue} +18 -22
  14. package/dist/runtime/components/{ProfileInfo.vue → NUsersProfileInfo.vue} +5 -7
  15. package/dist/runtime/components/{ResetPasswordForm.vue → NUsersResetPasswordForm.vue} +25 -28
  16. package/dist/runtime/components/{ResetPasswordForm.vue.d.ts → NUsersResetPasswordForm.vue.d.ts} +3 -6
  17. package/dist/runtime/components/NUsersUserCard.vue +93 -0
  18. package/dist/runtime/components/NUsersUserCard.vue.d.ts +33 -0
  19. package/dist/runtime/components/NUsersUserForm.vue +141 -0
  20. package/dist/runtime/components/NUsersUserForm.vue.d.ts +12 -0
  21. package/dist/runtime/composables/useAuthentication.d.ts +2 -0
  22. package/dist/runtime/composables/useAuthentication.js +5 -7
  23. package/dist/runtime/composables/usePasswordValidation.d.ts +2 -2
  24. package/dist/runtime/constants.d.ts +1 -0
  25. package/dist/runtime/constants.js +2 -1
  26. package/dist/runtime/middleware/authorization.client.js +6 -1
  27. package/dist/runtime/server/api/nuxt-users/index.get.d.ts +20 -0
  28. package/dist/runtime/server/api/nuxt-users/index.get.js +58 -0
  29. package/dist/runtime/server/api/{auth/forgot-password.post.js → nuxt-users/password/forgot.post.js} +1 -1
  30. package/dist/runtime/server/api/{auth/update-password.post.js → nuxt-users/password/index.patch.js} +2 -2
  31. package/dist/runtime/server/api/{auth/reset-password.post.js → nuxt-users/password/reset.post.js} +2 -2
  32. package/dist/runtime/server/api/{auth/logout.get.js → nuxt-users/session/index.delete.js} +1 -1
  33. package/dist/runtime/server/api/{auth/login.post.d.ts → nuxt-users/session/index.post.d.ts} +1 -0
  34. package/dist/runtime/server/api/{auth/login.post.js → nuxt-users/session/index.post.js} +1 -1
  35. package/dist/runtime/server/middleware/authorization.server.js +29 -9
  36. package/dist/runtime/server/utils/user.js +8 -5
  37. package/dist/shared/{nuxt-users.CXezgQLO.d.mts → nuxt-users.B5ttEpIO.d.mts} +8 -1
  38. package/dist/shared/{nuxt-users.CXezgQLO.d.ts → nuxt-users.B5ttEpIO.d.ts} +8 -1
  39. package/dist/utils.d.mts +1 -2
  40. package/dist/utils.d.ts +1 -2
  41. package/dist/utils.mjs +162 -2
  42. package/package.json +2 -2
  43. package/dist/runtime/assets/colors.css +0 -1
  44. package/dist/shared/nuxt-users.BnOQs2LI.mjs +0 -162
  45. package/dist/runtime/components/{LogoutLink.vue.d.ts → NUsersLogoutLink.vue.d.ts} +4 -4
  46. /package/dist/runtime/components/{PasswordStrengthIndicator.vue.d.ts → NUsersPasswordStrengthIndicator.vue.d.ts} +0 -0
  47. /package/dist/runtime/components/{ProfileInfo.vue.d.ts → NUsersProfileInfo.vue.d.ts} +0 -0
  48. /package/dist/runtime/server/api/nuxt-users/{profile.get.d.ts → me.get.d.ts} +0 -0
  49. /package/dist/runtime/server/api/nuxt-users/{profile.get.js → me.get.js} +0 -0
  50. /package/dist/runtime/server/api/{auth/forgot-password.post.d.ts → nuxt-users/password/forgot.post.d.ts} +0 -0
  51. /package/dist/runtime/server/api/{auth/logout.get.d.ts → nuxt-users/password/index.patch.d.ts} +0 -0
  52. /package/dist/runtime/server/api/{auth/reset-password.post.d.ts → nuxt-users/password/reset.post.d.ts} +0 -0
  53. /package/dist/runtime/server/api/{auth/update-password.post.d.ts → nuxt-users/session/index.delete.d.ts} +0 -0
@@ -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,10 +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
+ const { public: { nuxtUsers } } = useRuntimeConfig();
7
+ const moduleOptions = nuxtUsers;
6
8
  const props = defineProps({
7
- apiEndpoint: { type: String, required: false, default: "/api/user/profile" },
8
- updatePasswordEndpoint: { type: String, required: false, default: "/api/auth/update-password" }
9
+ apiEndpoint: { type: String, required: false },
10
+ updatePasswordEndpoint: { type: String, required: false }
9
11
  });
10
12
  const emit = defineEmits(["success", "error", "password-error", "password-updated"]);
11
13
  const isPasswordLoading = ref(false);
@@ -16,8 +18,6 @@ const passwordForm = ref({
16
18
  newPassword: "",
17
19
  newPasswordConfirmation: ""
18
20
  });
19
- const { public: { nuxtUsers } } = useRuntimeConfig();
20
- const moduleOptions = nuxtUsers;
21
21
  const passwordValidation = usePasswordValidation(moduleOptions);
22
22
  watch(() => passwordForm.value.newPassword, (newPassword) => {
23
23
  if (newPassword) {
@@ -31,8 +31,8 @@ const updatePassword = async () => {
31
31
  passwordError.value = "";
32
32
  passwordSuccess.value = "";
33
33
  try {
34
- await $fetch(props.updatePasswordEndpoint, {
35
- method: "POST",
34
+ await $fetch(props.updatePasswordEndpoint || nuxtUsers.apiBasePath + "/password", {
35
+ method: "PATCH",
36
36
  body: passwordForm.value
37
37
  });
38
38
  passwordSuccess.value = "Password updated successfully";
@@ -53,48 +53,49 @@ const updatePassword = async () => {
53
53
  </script>
54
54
 
55
55
  <template>
56
- <div>
57
- <h2>Change Password</h2>
56
+ <div class="n-users-section">
57
+ <h2 class="n-users-section-header">
58
+ Change Password
59
+ </h2>
58
60
 
59
61
  <form @submit.prevent="updatePassword">
60
- <div class="form-group">
62
+ <div class="n-users-form-group">
61
63
  <label
62
64
  for="currentPassword"
63
- class="form-label"
65
+ class="n-users-form-label"
64
66
  >Current Password</label>
65
67
  <input
66
68
  id="currentPassword"
67
69
  v-model="passwordForm.currentPassword"
68
70
  type="password"
69
- class="form-input"
71
+ class="n-users-form-input"
70
72
  required
71
73
  :disabled="isPasswordLoading"
72
74
  >
73
75
  </div>
74
76
 
75
- <div class="form-group">
77
+ <div class="n-users-form-group">
76
78
  <label
77
79
  for="newPassword"
78
- class="form-label"
80
+ class="n-users-form-label"
79
81
  >New Password</label>
80
82
  <input
81
83
  id="newPassword"
82
84
  v-model="passwordForm.newPassword"
83
85
  type="password"
84
- class="form-input"
86
+ class="n-users-form-input"
85
87
  required
86
88
  :minlength="moduleOptions.passwordValidation?.minLength || 8"
87
89
  :class="{ error: passwordValidation.errors.value.length > 0 && passwordForm.newPassword }"
88
90
  :disabled="isPasswordLoading"
89
91
  >
90
92
 
91
- <PasswordStrengthIndicator
93
+ <NUsersPasswordStrengthIndicator
92
94
  :password="passwordForm.newPassword"
93
95
  :validation-result="passwordValidation.validationResult.value"
94
96
  />
95
97
 
96
- <!-- Password requirements -->
97
- <small class="form-help">
98
+ <small class="n-users-form-help">
98
99
  Password must contain at least {{ moduleOptions.passwordValidation?.minLength || 8 }} characters
99
100
  <span v-if="moduleOptions.passwordValidation?.requireUppercase">, including uppercase letters</span>
100
101
  <span v-if="moduleOptions.passwordValidation?.requireLowercase">, lowercase letters</span>
@@ -103,16 +104,16 @@ const updatePassword = async () => {
103
104
  </small>
104
105
  </div>
105
106
 
106
- <div class="form-group">
107
+ <div class="n-users-form-group">
107
108
  <label
108
109
  for="newPasswordConfirmation"
109
- class="form-label"
110
+ class="n-users-form-label"
110
111
  >Confirm New Password</label>
111
112
  <input
112
113
  id="newPasswordConfirmation"
113
114
  v-model="passwordForm.newPasswordConfirmation"
114
115
  type="password"
115
- class="form-input"
116
+ class="n-users-form-input"
116
117
  required
117
118
  :disabled="isPasswordLoading"
118
119
  >
@@ -120,21 +121,21 @@ const updatePassword = async () => {
120
121
 
121
122
  <div
122
123
  v-if="passwordError"
123
- class="error-message"
124
+ class="n-users-error-message"
124
125
  >
125
126
  {{ passwordError }}
126
127
  </div>
127
128
 
128
129
  <div
129
130
  v-if="passwordSuccess"
130
- class="success-message"
131
+ class="n-users-success-message"
131
132
  >
132
133
  {{ passwordSuccess }}
133
134
  </div>
134
135
 
135
136
  <button
136
137
  type="submit"
137
- class="btn btn-primary"
138
+ class="n-users-btn n-users-btn-primary"
138
139
  :disabled="isPasswordLoading"
139
140
  >
140
141
  <span v-if="isPasswordLoading">Updating...</span>
@@ -143,7 +144,3 @@ const updatePassword = async () => {
143
144
  </form>
144
145
  </div>
145
146
  </template>
146
-
147
- <style scoped>
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}
149
- </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,7 +1,9 @@
1
- import { useState } from "#app";
1
+ import { useState, useRuntimeConfig } from "#app";
2
2
  import { computed, readonly } from "vue";
3
3
  export const useAuthentication = () => {
4
4
  const user = useState("user", () => null);
5
+ const { public: { nuxtUsers } } = useRuntimeConfig();
6
+ const apiBasePath = nuxtUsers?.apiBasePath;
5
7
  const isAuthenticated = computed(() => !!user.value);
6
8
  const login = (userData) => {
7
9
  const { password: _, ...userWithoutPassword } = userData;
@@ -12,9 +14,7 @@ export const useAuthentication = () => {
12
14
  };
13
15
  const logout = async () => {
14
16
  try {
15
- await $fetch("/api/auth/logout", {
16
- method: "GET"
17
- });
17
+ await $fetch(`${apiBasePath}/session`, { method: "DELETE" });
18
18
  user.value = null;
19
19
  if (import.meta.client) {
20
20
  localStorage.removeItem("user");
@@ -26,9 +26,7 @@ export const useAuthentication = () => {
26
26
  };
27
27
  const fetchUser = async () => {
28
28
  try {
29
- const response = await $fetch("/api/user/profile", {
30
- method: "GET"
31
- });
29
+ const response = await $fetch(`${apiBasePath}/me`, { method: "GET" });
32
30
  user.value = response.user;
33
31
  if (import.meta.client) {
34
32
  localStorage.setItem("user", JSON.stringify(response.user));
@@ -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;