nuxt-users 1.6.5 → 1.6.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/cli.mjs +1 -1
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +16 -20
  5. package/dist/runtime/components/LoginForm.vue +6 -3
  6. package/dist/runtime/components/LoginForm.vue.d.ts +4 -4
  7. package/dist/runtime/components/LogoutLink.vue +21 -7
  8. package/dist/runtime/components/LogoutLink.vue.d.ts +22 -5
  9. package/dist/runtime/components/PasswordStrengthIndicator.vue +38 -27
  10. package/dist/runtime/components/ProfileInfo.vue +33 -0
  11. package/dist/runtime/components/ResetPasswordForm.vue +89 -96
  12. package/dist/runtime/components/ResetPasswordForm.vue.d.ts +15 -1
  13. package/dist/runtime/composables/useAuth.d.ts +2 -2
  14. package/dist/runtime/composables/useAuth.js +2 -2
  15. package/dist/runtime/composables/usePasswordValidation.d.ts +1 -1
  16. package/dist/runtime/middleware/auth.client.js +4 -4
  17. package/dist/runtime/server/api/{login.post.js → auth/login.post.js} +1 -1
  18. package/dist/runtime/server/api/{logout.get.js → auth/logout.get.js} +1 -1
  19. package/dist/runtime/server/api/{update-password.post.js → auth/update-password.post.js} +2 -2
  20. package/dist/runtime/server/api/user/profile.get.d.ts +4 -0
  21. package/dist/runtime/server/api/{profile.get.js → user/profile.get.js} +2 -2
  22. package/dist/runtime/server/middleware/auth.server.js +3 -2
  23. package/package.json +1 -1
  24. package/dist/runtime/components/ForgotPasswordForm.vue +0 -77
  25. package/dist/runtime/components/ProfileForm.vue +0 -240
  26. package/dist/runtime/components/ProfileForm.vue.d.ts +0 -20
  27. package/dist/runtime/server/api/profile.get.d.ts +0 -2
  28. /package/dist/runtime/components/{ForgotPasswordForm.vue.d.ts → ProfileInfo.vue.d.ts} +0 -0
  29. /package/dist/runtime/server/api/{login.post.d.ts → auth/login.post.d.ts} +0 -0
  30. /package/dist/runtime/server/api/{logout.get.d.ts → auth/logout.get.d.ts} +0 -0
  31. /package/dist/runtime/server/api/{update-password.post.d.ts → auth/update-password.post.d.ts} +0 -0
package/CHANGELOG.md CHANGED
@@ -1,6 +1,37 @@
1
1
  # Changelog
2
2
 
3
3
 
4
+ ## v1.6.6
5
+
6
+ [compare changes](https://github.com/rrd108/nuxt-users/compare/v1.6.5...v1.6.6)
7
+
8
+ ### 🩹 Fixes
9
+
10
+ - Authentication middleware ([c7de8a4](https://github.com/rrd108/nuxt-users/commit/c7de8a4))
11
+ - Lint and type errors ([dd6ee40](https://github.com/rrd108/nuxt-users/commit/dd6ee40))
12
+ - Type errors ([65c81fb](https://github.com/rrd108/nuxt-users/commit/65c81fb))
13
+ - **cli:** Updates CLI version dynamically ([1cca13a](https://github.com/rrd108/nuxt-users/commit/1cca13a))
14
+ - Authentication middleware logic ([18a3bb5](https://github.com/rrd108/nuxt-users/commit/18a3bb5))
15
+ - Password validation and UI ([07b44b3](https://github.com/rrd108/nuxt-users/commit/07b44b3))
16
+
17
+ ### 💅 Refactors
18
+
19
+ - API routes ([3c7603f](https://github.com/rrd108/nuxt-users/commit/3c7603f))
20
+ - Authentication components and logic ([4da544f](https://github.com/rrd108/nuxt-users/commit/4da544f))
21
+
22
+ ### 🏡 Chore
23
+
24
+ - **release:** V1.6.5 ([f35e526](https://github.com/rrd108/nuxt-users/commit/f35e526))
25
+
26
+ ### ✅ Tests
27
+
28
+ - Fix mock ([791dc94](https://github.com/rrd108/nuxt-users/commit/791dc94))
29
+ - Adds unit tests for auth API endpoints ([d844630](https://github.com/rrd108/nuxt-users/commit/d844630))
30
+
31
+ ### ❤️ Contributors
32
+
33
+ - Rrd108 ([@rrd108](https://github.com/rrd108))
34
+
4
35
  ## v1.6.5
5
36
 
6
37
  [compare changes](https://github.com/rrd108/nuxt-users/compare/v1.6.4...v1.6.5)
package/dist/cli.mjs CHANGED
@@ -247,7 +247,7 @@ const main = defineCommand({
247
247
  meta: {
248
248
  name: "nuxt-users",
249
249
  description: "CLI for Nuxt Users Module - Manage users, migrations, and database operations",
250
- version: "1.6.3"
250
+ version: "1.6.5"
251
251
  },
252
252
  subCommands: {
253
253
  migrate,
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-users",
3
3
  "configKey": "nuxtUsers",
4
- "version": "1.6.5",
4
+ "version": "1.6.6",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.1",
7
7
  "unbuild": "3.6.0"
package/dist/module.mjs CHANGED
@@ -93,54 +93,50 @@ const module = defineNuxtModule({
93
93
  global: true
94
94
  });
95
95
  addServerHandler({
96
- route: "/api/login",
96
+ route: "/api/auth/login",
97
97
  method: "post",
98
- handler: resolver.resolve("./runtime/server/api/login.post")
98
+ handler: resolver.resolve("./runtime/server/api/auth/login.post")
99
99
  });
100
100
  addServerHandler({
101
- route: "/api/forgot-password",
101
+ route: "/api/auth/forgot-password",
102
102
  method: "post",
103
103
  handler: resolver.resolve("./runtime/server/api/auth/forgot-password.post")
104
104
  });
105
105
  addServerHandler({
106
- route: "/api/reset-password",
106
+ route: "/api/auth/reset-password",
107
107
  method: "post",
108
108
  handler: resolver.resolve("./runtime/server/api/auth/reset-password.post")
109
109
  });
110
110
  addServerHandler({
111
- route: "/api/logout",
111
+ route: "/api/auth/logout",
112
112
  method: "get",
113
- handler: resolver.resolve("./runtime/server/api/logout.get")
113
+ handler: resolver.resolve("./runtime/server/api/auth/logout.get")
114
114
  });
115
115
  addServerHandler({
116
- route: "/api/profile",
116
+ route: "/api/user/profile",
117
117
  method: "get",
118
- handler: resolver.resolve("./runtime/server/api/profile.get")
118
+ handler: resolver.resolve("./runtime/server/api/user/profile.get")
119
119
  });
120
120
  addServerHandler({
121
- route: "/api/update-password",
121
+ route: "/api/auth/update-password",
122
122
  method: "post",
123
- handler: resolver.resolve("./runtime/server/api/update-password.post")
123
+ handler: resolver.resolve("./runtime/server/api/auth/update-password.post")
124
124
  });
125
125
  addComponent({
126
126
  name: "LoginForm",
127
127
  filePath: resolver.resolve("./runtime/components/LoginForm.vue")
128
128
  });
129
- addComponent({
130
- name: "ForgotPasswordForm",
131
- filePath: resolver.resolve("./runtime/components/ForgotPasswordForm.vue")
132
- });
133
- addComponent({
134
- name: "ResetPasswordForm",
135
- filePath: resolver.resolve("./runtime/components/ResetPasswordForm.vue")
136
- });
137
129
  addComponent({
138
130
  name: "LogoutLink",
139
131
  filePath: resolver.resolve("./runtime/components/LogoutLink.vue")
140
132
  });
141
133
  addComponent({
142
- name: "ProfileForm",
143
- filePath: resolver.resolve("./runtime/components/ProfileForm.vue")
134
+ name: "ProfileInfo",
135
+ filePath: resolver.resolve("./runtime/components/ProfileInfo.vue")
136
+ });
137
+ addComponent({
138
+ name: "ResetPasswordForm",
139
+ filePath: resolver.resolve("./runtime/components/ResetPasswordForm.vue")
144
140
  });
145
141
  nuxt.options.css = nuxt.options.css || [];
146
142
  nuxt.options.css.push(resolver.resolve("./runtime/assets/colors.css"));
@@ -1,8 +1,11 @@
1
1
  <script setup>
2
2
  import { ref } from "vue";
3
3
  import { navigateTo } from "#app";
4
+ import { useRuntimeConfig } from "#imports";
5
+ const { public: { nuxtUsers } } = useRuntimeConfig();
6
+ const { passwordValidation } = nuxtUsers;
4
7
  const props = defineProps({
5
- apiEndpoint: { type: String, required: false, default: "/api/login" },
8
+ apiEndpoint: { type: String, required: false, default: "/api/auth/login" },
6
9
  redirectTo: { type: String, required: false, default: "/" },
7
10
  forgotPasswordEndpoint: { type: String, required: false, default: "/api/auth/forgot-password" }
8
11
  });
@@ -108,7 +111,7 @@ const handleForgotPassword = async () => {
108
111
  name="password"
109
112
  placeholder="Enter your password"
110
113
  required
111
- minlength="6"
114
+ :minlength="passwordValidation.minLength"
112
115
  >
113
116
  </div>
114
117
  </slot>
@@ -190,5 +193,5 @@ const handleForgotPassword = async () => {
190
193
  </template>
191
194
 
192
195
  <style scoped>
193
- section{background:linear-gradient(135deg,var(--color-bg-primary) 0,var(--color-bg-secondary) 100%);border:1px solid var(--color-border);border-radius:.75em;box-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -1px rgba(0,0,0,.06);margin:0 auto;max-width:25rem;padding:1em}form{gap:1.5rem}.form-group,form{display:flex;flex-direction:column}label{display:block;font-weight:500;margin-bottom:.5em}input[type=email],input[type=password],label{color:var(--color-gray-700);font-size:.875rem}input[type=email],input[type=password]{background-color:var(--color-bg-secondary);border:1px solid var(--color-border-light);border-radius:8px;box-sizing:border-box;padding:.75em 1em;transition:all .2s ease-in-out;width:100%}input[type=email]:focus,input[type=password]:focus{background-color:var(--color-bg-primary);border-color:var(--color-primary);box-shadow:0 0 0 3px rgba(59,130,246,.1);outline:none}.login-header{margin-bottom:1em;text-align:center}.login-title{color:var(--color-gray-800);font-size:1.875rem;font-weight:700;line-height:1.2;margin:0 0 .5em}.login-subtitle{color:var(--color-gray-500);font-size:.875rem;line-height:1.5;margin:0}.remember-me{align-items:center;display:flex;gap:.5rem}input[type=checkbox]{margin:0;width:auto}.remember-me label{color:var(--color-gray-700);font-size:.875rem;margin:0}button[type=submit]{align-items:center;background-color:var(--color-primary);border:1px solid var(--color-primary);border-radius:8px;color:var(--color-white);cursor:pointer;display:flex;font-size:.875rem;font-weight:500;gap:.5rem;justify-content:center;min-height:44px;padding:.75em 1em;transition:all .2s ease-in-out;width:100%}button[type=submit]:hover:not(:disabled){background-color:var(--color-primary-dark);border-color:var(--color-primary-dark)}button[type=submit]:disabled{cursor:not-allowed;opacity:.6}.loading-spinner{animation:spin 1s linear infinite;border:2px solid var(--color-white);border-radius:50%;border-top:2px solid transparent;height:16px;width:16px}@keyframes spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}.login-footer{margin-top:1em;text-align:center}.forgot-password{margin:0}.forgot-link{align-items:center;color:var(--color-primary);display:inline-flex;font-size:.875rem;gap:.5rem;text-decoration:none;transition:color .2s ease-in-out}.forgot-link:hover:not(.loading){color:var(--color-primary-dark);text-decoration:underline}.forgot-link.loading{cursor:not-allowed;opacity:.6;pointer-events:none}.error-message{background-color:var(--color-error-light);border:1px solid var(--color-error-border);border-radius:8px;color:var(--color-error)}.error-message,.success-message{font-size:.875rem;margin-top:1em;padding:.75em 1em;text-align:center}.success-message{background-color:var(--color-success-light);border:1px solid var(--color-success-border);border-radius:8px;color:var(--color-success)}@media (max-width:480px){.login-form-container{margin:1em;padding:1.5em}.login-title{font-size:1.5rem}}
196
+ section{background:linear-gradient(135deg,var(--color-bg-primary) 0,var(--color-bg-secondary) 100%);border:1px solid var(--color-border);border-radius:.75em;box-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -1px rgba(0,0,0,.06);margin:0 auto;max-width:25rem;padding:1em}form{gap:1.5rem}.form-group,form{display:flex;flex-direction:column}label{display:block;font-weight:500;margin-bottom:.5em}input[type=email],input[type=password],label{color:var(--color-gray-700);font-size:.875rem}input[type=email],input[type=password]{background-color:var(--color-bg-secondary);border:1px solid var(--color-border-light);border-radius:.5em;box-sizing:border-box;padding:.75em 1em;transition:all .2s ease-in-out;width:100%}input[type=email]:focus,input[type=password]:focus{background-color:var(--color-bg-primary);border-color:var(--color-primary);box-shadow:0 0 0 3px rgba(59,130,246,.1);outline:none}.login-header{margin-bottom:1em;text-align:center}.login-title{color:var(--color-gray-800);font-size:1.875rem;font-weight:700;line-height:1.2;margin:0 0 .5em}.login-subtitle{color:var(--color-gray-500);font-size:.875rem;line-height:1.5;margin:0}.remember-me{align-items:center;display:flex;gap:.5rem}input[type=checkbox]{margin:0;width:auto}.remember-me label{color:var(--color-gray-700);font-size:.875rem;margin:0}button[type=submit]{align-items:center;background-color:var(--color-primary);border:1px solid var(--color-primary);border-radius:.5em;color:var(--color-white);cursor:pointer;display:flex;font-size:.875rem;font-weight:500;gap:.5rem;justify-content:center;min-height:44px;padding:.75em 1em;transition:all .2s ease-in-out;width:100%}button[type=submit]:hover:not(:disabled){background-color:var(--color-primary-dark);border-color:var(--color-primary-dark)}button[type=submit]:disabled{cursor:not-allowed;opacity:.6}.loading-spinner{animation:spin 1s linear infinite;border:2px solid var(--color-white);border-radius:50%;border-top:2px solid transparent;height:16px;width:16px}@keyframes spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}.login-footer{margin-top:1em;text-align:center}.forgot-password{margin:0}.forgot-link{align-items:center;color:var(--color-primary);display:inline-flex;font-size:.875rem;gap:.5rem;text-decoration:none;transition:color .2s ease-in-out}.forgot-link:hover:not(.loading){color:var(--color-primary-dark);text-decoration:underline}.forgot-link.loading{cursor:not-allowed;opacity:.6;pointer-events:none}.error-message{background-color:var(--color-error-light);border:1px solid var(--color-error-border);border-radius:.5em;color:var(--color-error)}.error-message,.success-message{font-size:.875rem;margin-top:1em;padding:.75em 1em;text-align:center}.success-message{background-color:var(--color-success-light);border:1px solid var(--color-success-border);border-radius:.5em;color:var(--color-success)}@media (max-width:480px){.login-form-container{margin:1em;padding:1.5em}.login-title{font-size:1.5rem}}
194
197
  </style>
@@ -22,16 +22,16 @@ type __VLS_Slots = {} & {
22
22
  'error-message'?: (props: typeof __VLS_15) => any;
23
23
  };
24
24
  declare const __VLS_component: import("vue").DefineComponent<LoginFormProps, void, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
25
- error: (error: string) => any;
26
- submit: (data: LoginFormData) => any;
27
25
  success: (user: UserWithoutPassword) => any;
26
+ error: (error: string) => any;
28
27
  "forgot-password-error": (error: string) => any;
28
+ submit: (data: LoginFormData) => any;
29
29
  "forgot-password-success": () => any;
30
30
  }, string, import("vue").PublicProps, Readonly<LoginFormProps> & Readonly<{
31
- onError?: ((error: string) => any) | undefined;
32
- onSubmit?: ((data: LoginFormData) => any) | undefined;
33
31
  onSuccess?: ((user: UserWithoutPassword) => any) | undefined;
32
+ onError?: ((error: string) => any) | undefined;
34
33
  "onForgot-password-error"?: ((error: string) => any) | undefined;
34
+ onSubmit?: ((data: LoginFormData) => any) | undefined;
35
35
  "onForgot-password-success"?: (() => any) | undefined;
36
36
  }>, {
37
37
  apiEndpoint: string;
@@ -6,7 +6,14 @@ const props = defineProps({
6
6
  redirectTo: { type: String, required: false, default: "/login" },
7
7
  linkText: { type: String, required: false, default: "Logout" },
8
8
  confirmMessage: { type: String, required: false, default: "Are you sure you want to logout?" },
9
- class: { type: String, required: false }
9
+ class: { type: String, required: false },
10
+ style: { type: [String, Object], required: false },
11
+ linkClass: { type: String, required: false },
12
+ linkStyle: { type: [String, Object], required: false },
13
+ errorClass: { type: String, required: false },
14
+ errorStyle: { type: [String, Object], required: false },
15
+ containerClass: { type: String, required: false },
16
+ containerStyle: { type: [String, Object], required: false }
10
17
  });
11
18
  const emit = defineEmits(["success", "click", "error"]);
12
19
  const { logout } = useAuth();
@@ -37,26 +44,33 @@ const handleLogout = async (event) => {
37
44
  </script>
38
45
 
39
46
  <template>
40
- <div>
47
+ <div
48
+ :class="['logout-container', props.containerClass]"
49
+ :style="props.containerStyle"
50
+ >
41
51
  <div
42
52
  v-if="error"
43
- class="error-message"
53
+ :class="['error-message', props.errorClass]"
54
+ :style="props.errorStyle"
44
55
  >
45
56
  {{ error }}
46
57
  </div>
47
58
 
48
59
  <a
49
60
  href="#"
50
- :class="['logout-link', { loading: isLoading }, props.class]"
61
+ :class="['logout-link', { loading: isLoading }, props.linkClass]"
62
+ :style="props.linkStyle"
51
63
  :disabled="isLoading"
52
64
  @click="handleLogout"
53
65
  >
54
- <span v-if="isLoading">Logging out...</span>
55
- <span v-else>{{ linkText }}</span>
66
+ <slot>
67
+ <span v-if="isLoading">Logging out...</span>
68
+ <span v-else>{{ linkText }}</span>
69
+ </slot>
56
70
  </a>
57
71
  </div>
58
72
  </template>
59
73
 
60
74
  <style scoped>
61
- .logout-link{color:#dc3545;cursor:pointer;font-size:.875rem;font-weight:500;text-decoration:none;transition:color .2s}.logout-link:hover:not(.loading){color:#c82333;text-decoration:underline}.logout-link.loading{cursor:not-allowed;opacity:.6;pointer-events:none}.error-message{color:#dc3545;font-size:.875rem;margin-bottom:.5rem}
75
+ .logout-link{background:var(--color-error);border:2px solid transparent;border-color:var(--color-error-dark);border-radius:.5em;color:#fff;display:inline-block;font-weight:500;padding:.5em 1em;text-decoration:none;transition:all .2s}.logout-link:hover{border-color:var(--color-primary)}.error-message{color:var(--color-error);font-size:1rem;margin-bottom:.5em}
62
76
  </style>
@@ -3,18 +3,35 @@ interface LogoutLinkProps {
3
3
  linkText?: string;
4
4
  confirmMessage?: string;
5
5
  class?: string;
6
+ style?: string | Record<string, string | number>;
7
+ linkClass?: string;
8
+ linkStyle?: string | Record<string, string | number>;
9
+ errorClass?: string;
10
+ errorStyle?: string | Record<string, string | number>;
11
+ containerClass?: string;
12
+ containerStyle?: string | Record<string, string | number>;
6
13
  }
7
- declare const _default: import("vue").DefineComponent<LogoutLinkProps, void, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
8
- click: () => any;
9
- error: (error: string) => any;
14
+ declare var __VLS_1: {};
15
+ type __VLS_Slots = {} & {
16
+ default?: (props: typeof __VLS_1) => any;
17
+ };
18
+ declare const __VLS_component: import("vue").DefineComponent<LogoutLinkProps, void, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
10
19
  success: () => any;
20
+ error: (error: string) => any;
21
+ click: () => any;
11
22
  }, string, import("vue").PublicProps, Readonly<LogoutLinkProps> & Readonly<{
12
- onClick?: (() => any) | undefined;
13
- onError?: ((error: string) => any) | undefined;
14
23
  onSuccess?: (() => any) | undefined;
24
+ onError?: ((error: string) => any) | undefined;
25
+ onClick?: (() => any) | undefined;
15
26
  }>, {
16
27
  redirectTo: string;
17
28
  linkText: string;
18
29
  confirmMessage: string;
19
30
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
31
+ declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
20
32
  export default _default;
33
+ type __VLS_WithSlots<T, S> = T & {
34
+ new (): {
35
+ $slots: S;
36
+ };
37
+ };
@@ -1,49 +1,60 @@
1
1
  <script setup>
2
2
  import { computed } from "vue";
3
+ import { useRuntimeConfig } from "#imports";
3
4
  const props = defineProps({
4
5
  password: { type: String, required: true },
5
6
  validationResult: { type: [Object, null], required: true },
6
7
  showHints: { type: Boolean, required: false, default: true },
7
8
  showRules: { type: Boolean, required: false, default: true }
8
9
  });
10
+ const { public: { nuxtUsers } } = useRuntimeConfig();
11
+ const moduleOptions = nuxtUsers;
9
12
  const hasErrors = computed(() => (props.validationResult?.errors?.length || 0) > 0);
10
13
  const hasHints = computed(() => (props.validationResult?.hints?.length || 0) > 0);
11
14
  const hasPassword = computed(() => props.password.length > 0);
12
15
  const validationRules = computed(() => {
13
16
  if (!props.password) return [];
14
17
  const rules = [];
15
- const minLength = 8;
16
- const hasMinLength = props.password.length >= minLength;
17
- rules.push({
18
- text: `At least ${minLength} characters`,
19
- passed: hasMinLength
20
- });
21
- const hasUppercase = /[A-Z]/.test(props.password);
22
- rules.push({
23
- text: "Contains uppercase letter (A-Z)",
24
- passed: hasUppercase
25
- });
26
- const hasLowercase = /[a-z]/.test(props.password);
27
- rules.push({
28
- text: "Contains lowercase letter (a-z)",
29
- passed: hasLowercase
30
- });
31
- const hasNumbers = /\d/.test(props.password);
32
- rules.push({
33
- text: "Contains number (0-9)",
34
- passed: hasNumbers
35
- });
36
- const hasSpecialChars = /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/.test(props.password);
37
- rules.push({
38
- text: "Contains special character (!@#$%^&*)",
39
- passed: hasSpecialChars
40
- });
18
+ if (moduleOptions.passwordValidation.minLength) {
19
+ const hasMinLength = props.password.length >= moduleOptions.passwordValidation.minLength;
20
+ rules.push({
21
+ text: `At least ${moduleOptions.passwordValidation.minLength} characters`,
22
+ passed: hasMinLength
23
+ });
24
+ }
25
+ if (moduleOptions.passwordValidation.requireUppercase) {
26
+ const hasUppercase = /[A-Z]/.test(props.password);
27
+ rules.push({
28
+ text: "Contains uppercase letter (A-Z)",
29
+ passed: hasUppercase
30
+ });
31
+ }
32
+ if (moduleOptions.passwordValidation.requireLowercase) {
33
+ const hasLowercase = /[a-z]/.test(props.password);
34
+ rules.push({
35
+ text: "Contains lowercase letter (a-z)",
36
+ passed: hasLowercase
37
+ });
38
+ }
39
+ if (moduleOptions.passwordValidation.requireNumbers) {
40
+ const hasNumbers = /\d/.test(props.password);
41
+ rules.push({
42
+ text: "Contains number (0-9)",
43
+ passed: hasNumbers
44
+ });
45
+ }
46
+ if (moduleOptions.passwordValidation.requireSpecialChars) {
47
+ const hasSpecialChars = /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/.test(props.password);
48
+ rules.push({
49
+ text: "Contains special character (!@#$%^&*)",
50
+ passed: hasSpecialChars
51
+ });
52
+ }
41
53
  return rules;
42
54
  });
43
55
  </script>
44
56
 
45
57
  <template>
46
- <!-- Password strength indicator -->
47
58
  <div
48
59
  v-if="hasPassword"
49
60
  class="password-strength"
@@ -0,0 +1,33 @@
1
+ <script setup>
2
+ import { useAuth } from "../composables/useAuth";
3
+ const { user } = useAuth();
4
+ </script>
5
+
6
+ <template>
7
+ <div>
8
+ <h2>Profile Information</h2>
9
+ <dl
10
+ v-if="user"
11
+ class="profile-info"
12
+ >
13
+ <dt>Name:</dt>
14
+ <dd>{{ user.name }}</dd>
15
+
16
+ <dt>Email:</dt>
17
+ <dd>{{ user.email }}</dd>
18
+
19
+ <dt>Role:</dt>
20
+ <dd>{{ user.role }}</dd>
21
+
22
+ <dt>Registered:</dt>
23
+ <dd>{{ Intl.DateTimeFormat().format(new Date(user.created_at)) }}</dd>
24
+
25
+ <dt>Last updated:</dt>
26
+ <dd>{{ Intl.DateTimeFormat().format(new Date(user.updated_at)) }}</dd>
27
+ </dl>
28
+ </div>
29
+ </template>
30
+
31
+ <style scoped>
32
+ div{background-color:var(--color-bg-primary);border-top-left-radius:.5em;border-top-right-radius:.5em;margin:0 auto;max-width:42em;padding:1em}h2{border-bottom:.1em solid var(--color-border)}dl{display:grid;font-size:1rem;gap:16px;grid-template-columns:1fr 5fr;list-style:none;margin:0;padding:0}
33
+ </style>
@@ -1,118 +1,98 @@
1
1
  <script setup>
2
- import { ref, reactive, onMounted, computed, watch } from "vue";
3
- import { useRoute, useRouter } from "#app";
2
+ import { ref, watch } from "vue";
4
3
  import { usePasswordValidation } from "../composables/usePasswordValidation";
5
4
  import { useRuntimeConfig } from "#imports";
6
5
  import PasswordStrengthIndicator from "./PasswordStrengthIndicator.vue";
7
- const formData = reactive({
8
- password: "",
9
- password_confirm: ""
6
+ const props = defineProps({
7
+ apiEndpoint: { type: String, required: false, default: "/api/user/profile" },
8
+ updatePasswordEndpoint: { type: String, required: false, default: "/api/auth/update-password" }
10
9
  });
11
- const message = ref("");
12
- const loading = ref(false);
13
- const isError = ref(false);
14
- const formInvalidDueToToken = ref(false);
15
- const route = useRoute();
16
- const router = useRouter();
17
- const tokenFromUrl = ref(null);
18
- const emailFromUrl = ref(null);
19
- onMounted(() => {
20
- if (typeof route.query.token === "string") {
21
- tokenFromUrl.value = route.query.token;
22
- }
23
- if (typeof route.query.email === "string") {
24
- emailFromUrl.value = decodeURIComponent(route.query.email);
25
- }
26
- if (!tokenFromUrl.value || !emailFromUrl.value) {
27
- message.value = "Invalid or missing password reset token or email in URL. Cannot reset password.";
28
- isError.value = true;
29
- formInvalidDueToToken.value = true;
30
- }
10
+ const emit = defineEmits(["success", "error", "password-error", "password-updated"]);
11
+ const isPasswordLoading = ref(false);
12
+ const passwordError = ref("");
13
+ const passwordSuccess = ref("");
14
+ const passwordForm = ref({
15
+ currentPassword: "",
16
+ newPassword: "",
17
+ newPasswordConfirmation: ""
31
18
  });
32
- const canSubmit = computed(() => !!tokenFromUrl.value && !!emailFromUrl.value && !formInvalidDueToToken.value);
33
19
  const { public: { nuxtUsers } } = useRuntimeConfig();
34
20
  const moduleOptions = nuxtUsers;
35
21
  const passwordValidation = usePasswordValidation(moduleOptions);
36
- watch(() => formData.password, (newPassword) => {
22
+ watch(() => passwordForm.value.newPassword, (newPassword) => {
37
23
  if (newPassword) {
38
24
  passwordValidation.validate(newPassword);
39
25
  } else {
40
26
  passwordValidation.clearValidation();
41
27
  }
42
28
  });
43
- const handleResetPassword = async () => {
44
- if (!canSubmit.value) {
45
- message.value = "Token or email is missing or invalid. Cannot reset password.";
46
- isError.value = true;
47
- return;
48
- }
49
- if (formData.password !== formData.password_confirm) {
50
- message.value = "Passwords do not match.";
51
- isError.value = true;
52
- return;
53
- }
54
- loading.value = true;
55
- message.value = "";
56
- isError.value = false;
29
+ const updatePassword = async () => {
30
+ isPasswordLoading.value = true;
31
+ passwordError.value = "";
32
+ passwordSuccess.value = "";
57
33
  try {
58
- const response = await fetch("/api/reset-password", {
34
+ await $fetch(props.updatePasswordEndpoint, {
59
35
  method: "POST",
60
- headers: {
61
- "Content-Type": "application/json",
62
- "Accept": "application/json"
63
- },
64
- body: JSON.stringify({
65
- token: tokenFromUrl.value,
66
- email: emailFromUrl.value,
67
- password: formData.password,
68
- password_confirmation: formData.password_confirm
69
- })
36
+ body: passwordForm.value
70
37
  });
71
- const responseData = await response.json();
72
- if (!response.ok) {
73
- throw new Error(responseData.message || responseData.statusMessage || "An error occurred.");
74
- }
75
- message.value = responseData.message + " Redirecting to login...";
76
- isError.value = false;
77
- formData.password = "";
78
- formData.password_confirm = "";
79
- setTimeout(() => {
80
- router.push("/login");
81
- }, 3e3);
38
+ passwordSuccess.value = "Password updated successfully";
39
+ emit("password-updated");
40
+ passwordForm.value = {
41
+ currentPassword: "",
42
+ newPassword: "",
43
+ newPasswordConfirmation: ""
44
+ };
82
45
  } catch (err) {
83
- if (err instanceof Error) {
84
- message.value = err.message;
85
- } else {
86
- message.value = "Failed to reset password.";
87
- }
88
- console.error("[Nuxt Users] Reset Password Form Error:", err);
89
- isError.value = true;
46
+ const errorMessage = err && typeof err === "object" && "data" in err && err.data && typeof err.data === "object" && "statusMessage" in err.data ? String(err.data.statusMessage) : err instanceof Error ? err.message : "Failed to update password";
47
+ passwordError.value = errorMessage;
48
+ emit("password-error", errorMessage);
90
49
  } finally {
91
- loading.value = false;
50
+ isPasswordLoading.value = false;
92
51
  }
93
52
  };
94
53
  </script>
95
54
 
96
55
  <template>
97
56
  <div>
98
- <h2>Reset Password</h2>
99
- <form @submit.prevent="handleResetPassword">
57
+ <h2>Change Password</h2>
58
+
59
+ <form @submit.prevent="updatePassword">
60
+ <div class="form-group">
61
+ <label
62
+ for="currentPassword"
63
+ class="form-label"
64
+ >Current Password</label>
65
+ <input
66
+ id="currentPassword"
67
+ v-model="passwordForm.currentPassword"
68
+ type="password"
69
+ class="form-input"
70
+ required
71
+ :disabled="isPasswordLoading"
72
+ >
73
+ </div>
74
+
100
75
  <div class="form-group">
101
- <label for="password">New Password</label>
76
+ <label
77
+ for="newPassword"
78
+ class="form-label"
79
+ >New Password</label>
102
80
  <input
103
- id="password"
104
- v-model="formData.password"
81
+ id="newPassword"
82
+ v-model="passwordForm.newPassword"
105
83
  type="password"
106
- name="password"
107
- placeholder="Enter new password"
84
+ class="form-input"
108
85
  required
109
86
  :minlength="moduleOptions.passwordValidation?.minLength || 8"
110
- :class="{ error: passwordValidation.errors.value.length > 0 && formData.password }"
87
+ :class="{ error: passwordValidation.errors.value.length > 0 && passwordForm.newPassword }"
88
+ :disabled="isPasswordLoading"
111
89
  >
90
+
112
91
  <PasswordStrengthIndicator
113
- :password="formData.password"
92
+ :password="passwordForm.newPassword"
114
93
  :validation-result="passwordValidation.validationResult.value"
115
94
  />
95
+
116
96
  <!-- Password requirements -->
117
97
  <small class="form-help">
118
98
  Password must contain at least {{ moduleOptions.passwordValidation?.minLength || 8 }} characters
@@ -122,35 +102,48 @@ const handleResetPassword = async () => {
122
102
  <span v-if="moduleOptions.passwordValidation?.requireSpecialChars">, and special characters</span>.
123
103
  </small>
124
104
  </div>
105
+
125
106
  <div class="form-group">
126
- <label for="password_confirm">Confirm New Password</label>
107
+ <label
108
+ for="newPasswordConfirmation"
109
+ class="form-label"
110
+ >Confirm New Password</label>
127
111
  <input
128
- id="password_confirm"
129
- v-model="formData.password_confirm"
112
+ id="newPasswordConfirmation"
113
+ v-model="passwordForm.newPasswordConfirmation"
130
114
  type="password"
131
- name="password_confirm"
132
- placeholder="Confirm new password"
115
+ class="form-input"
133
116
  required
134
- :minlength="moduleOptions.passwordValidation?.minLength || 8"
117
+ :disabled="isPasswordLoading"
135
118
  >
136
119
  </div>
120
+
121
+ <div
122
+ v-if="passwordError"
123
+ class="error-message"
124
+ >
125
+ {{ passwordError }}
126
+ </div>
127
+
128
+ <div
129
+ v-if="passwordSuccess"
130
+ class="success-message"
131
+ >
132
+ {{ passwordSuccess }}
133
+ </div>
134
+
137
135
  <button
138
136
  type="submit"
139
- :disabled="loading || formInvalidDueToToken"
137
+ class="btn btn-primary"
138
+ :disabled="isPasswordLoading"
140
139
  >
141
- {{ loading ? "Resetting..." : "Reset Password" }}
140
+ <span v-if="isPasswordLoading">Updating...</span>
141
+ <span v-else>Update Password</span>
142
142
  </button>
143
- <p
144
- v-if="message"
145
- :class="{ error: isError, success: !isError }"
146
- class="form-message"
147
- >
148
- {{ message }}
149
- </p>
150
143
  </form>
151
144
  </div>
152
145
  </template>
153
146
 
154
147
  <style scoped>
155
- .error{color:red}.success{color:green}.form-message{font-size:.9rem;margin-top:1em}.form-group{margin-bottom:1em}label{display:block;font-weight:700;margin-bottom:.25em}input[type=password]{border:1px solid var(--color-border-dark);border-radius:4px;box-sizing:border-box;padding:.75em;width:100%}input[type=password].error{border-color:#dc3545}.form-help{color:#6c757d;display:block;font-size:.875rem;margin-top:4px}button[type=submit]{background-color:var(--color-gray-800);border:none;border-radius:4px;color:#fff;cursor:pointer;padding:.75em 1.5em}button[type=submit]:disabled{background-color:var(--color-gray-500);cursor:not-allowed}
148
+ div{background-color:var(--color-bg-primary);border-bottom-left-radius:.5em;border-bottom-right-radius:.5em;margin:0 auto;max-width:42em;padding:1em}h2{border-bottom:.1em solid var(--color-border)}form{gap:1em}.form-group,form{display:flex;flex-direction:column}.form-group{gap:.5em;width:90%}.form-label{color:var(--color-text-primary);font-size:1rem;font-weight:500}.form-input{border:1px solid var(--color-border);border-radius:.5em;font-size:1rem;padding:.5em 1em;transition:border-color .2s,box-shadow .2s}.form-input:focus{border-color:var(--color-primary);box-shadow:0 0 0 .1em rgba(var(--color-primary-rgb),.1);outline:none}.form-input:disabled{background-color:var(--color-bg-secondary);cursor:not-allowed}.form-help{color:var(--color-text-secondary);font-size:.75rem;margin-top:.25em}.btn{align-self:flex-start;border:none;border-radius:.5em;cursor:pointer;font-size:1rem;font-weight:500;padding:.5em 1em;transition:background-color .2s,transform .1s}.btn:disabled{cursor:not-allowed;opacity:.6;transform:none}.btn-primary{background-color:var(--color-primary);color:#fff}.btn-primary:hover:not(:disabled){background-color:var(--color-primary-dark);transform:translateY(-1px)}.error-message{background-color:var(--color-error-bg);border:1px solid var(--color-error-border);border-radius:.5em;color:var(--color-error);font-size:.875rem;padding:12px}.success-message{background-color:#f0fff4;border:1px solid #9ae6b4;border-radius:6px;color:#38a169;font-size:.875rem;padding:12px}
156
149
  </style>
@@ -1,2 +1,16 @@
1
- declare const _default: import("vue").DefineComponent<{}, void, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
1
+ import type { UserWithoutPassword, ResetPasswordFormProps } from '../../types.js';
2
+ declare const _default: import("vue").DefineComponent<ResetPasswordFormProps, void, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
3
+ success: (user: UserWithoutPassword) => any;
4
+ error: (error: string) => any;
5
+ "password-error": (error: string) => any;
6
+ "password-updated": () => any;
7
+ }, string, import("vue").PublicProps, Readonly<ResetPasswordFormProps> & Readonly<{
8
+ onSuccess?: ((user: UserWithoutPassword) => any) | undefined;
9
+ onError?: ((error: string) => any) | undefined;
10
+ "onPassword-error"?: ((error: string) => any) | undefined;
11
+ "onPassword-updated"?: (() => any) | undefined;
12
+ }>, {
13
+ apiEndpoint: string;
14
+ updatePasswordEndpoint: string;
15
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
2
16
  export default _default;
@@ -1,15 +1,15 @@
1
1
  import type { User, UserWithoutPassword } from '../../types.js';
2
2
  export declare const useAuth: () => {
3
3
  user: Readonly<import("vue").Ref<{
4
- readonly email: string;
5
4
  readonly id: number;
5
+ readonly email: string;
6
6
  readonly name: string;
7
7
  readonly role: string;
8
8
  readonly created_at: string;
9
9
  readonly updated_at: string;
10
10
  } | null, {
11
- readonly email: string;
12
11
  readonly id: number;
12
+ readonly email: string;
13
13
  readonly name: string;
14
14
  readonly role: string;
15
15
  readonly created_at: string;
@@ -12,7 +12,7 @@ export const useAuth = () => {
12
12
  };
13
13
  const logout = async () => {
14
14
  try {
15
- await $fetch("/api/logout", {
15
+ await $fetch("/api/auth/logout", {
16
16
  method: "GET"
17
17
  });
18
18
  user.value = null;
@@ -26,7 +26,7 @@ export const useAuth = () => {
26
26
  };
27
27
  const fetchUser = async () => {
28
28
  try {
29
- const response = await $fetch("/api/profile", {
29
+ const response = await $fetch("/api/user/profile", {
30
30
  method: "GET"
31
31
  });
32
32
  user.value = response.user;
@@ -25,7 +25,7 @@ export declare const usePasswordValidation: (moduleOptions?: ModuleOptions, opti
25
25
  isValid: import("vue").ComputedRef<boolean>;
26
26
  errors: import("vue").ComputedRef<string[]>;
27
27
  hints: import("vue").ComputedRef<string[]>;
28
- strength: import("vue").ComputedRef<"strong" | "weak" | "medium">;
28
+ strength: import("vue").ComputedRef<"weak" | "medium" | "strong">;
29
29
  score: import("vue").ComputedRef<number>;
30
30
  strengthColor: import("vue").ComputedRef<string>;
31
31
  strengthText: import("vue").ComputedRef<string>;
@@ -11,10 +11,10 @@ export default defineNuxtRouteMiddleware((to) => {
11
11
  console.log(`[Nuxt Users] client.middleware.auth.global: Whitelisted: ${to.path}`);
12
12
  return;
13
13
  }
14
- const { user } = useAuth();
15
- if (!user.value?.id) {
16
- console.log("[Nuxt Users] client.middleware.auth.global: Unauthenticated, redirecting to /login");
14
+ const { isAuthenticated } = useAuth();
15
+ if (!isAuthenticated.value) {
16
+ console.log(`[Nuxt Users] client.middleware.auth.global: Unauthenticated ${to.path}, redirecting to /login`);
17
17
  return navigateTo("/login");
18
18
  }
19
- console.log("[Nuxt Users] client.middleware.auth.global", { user: user.value?.id, to: to.path });
19
+ console.log("[Nuxt Users] client.middleware.auth.global", { isAuthenticated: isAuthenticated.value, to: to.path });
20
20
  });
@@ -2,7 +2,7 @@ import { createError, defineEventHandler, readBody, setCookie } from "h3";
2
2
  import bcrypt from "bcrypt";
3
3
  import crypto from "node:crypto";
4
4
  import { useRuntimeConfig } from "#imports";
5
- import { useDb } from "../utils/index.js";
5
+ import { useDb } from "../../utils/index.js";
6
6
  export default defineEventHandler(async (event) => {
7
7
  const body = await readBody(event);
8
8
  const { email, password } = body;
@@ -1,6 +1,6 @@
1
1
  import { defineEventHandler, getCookie, setCookie } from "h3";
2
2
  import { useRuntimeConfig } from "#imports";
3
- import { useDb } from "../utils/index.js";
3
+ import { useDb } from "../../utils/index.js";
4
4
  export default defineEventHandler(async (event) => {
5
5
  const { nuxtUsers } = useRuntimeConfig();
6
6
  const options = nuxtUsers;
@@ -1,8 +1,8 @@
1
1
  import { createError, defineEventHandler, getCookie, readBody } from "h3";
2
2
  import bcrypt from "bcrypt";
3
3
  import { useRuntimeConfig } from "#imports";
4
- import { getCurrentUserFromToken, updateUserPassword } from "../utils/index.js";
5
- import { validatePassword, getPasswordValidationOptions } from "../../../utils";
4
+ import { getCurrentUserFromToken, updateUserPassword } from "../../utils/index.js";
5
+ import { validatePassword, getPasswordValidationOptions } from "../../../../utils";
6
6
  export default defineEventHandler(async (event) => {
7
7
  const { nuxtUsers } = useRuntimeConfig();
8
8
  const options = nuxtUsers;
@@ -0,0 +1,4 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
2
+ user: import("../../../../types.js").UserWithoutPassword;
3
+ }>>;
4
+ export default _default;
@@ -1,6 +1,6 @@
1
1
  import { createError, defineEventHandler, getCookie } from "h3";
2
2
  import { useRuntimeConfig } from "#imports";
3
- import { getCurrentUserFromToken } from "../utils/index.js";
3
+ import { getCurrentUserFromToken } from "../../utils/index.js";
4
4
  export default defineEventHandler(async (event) => {
5
5
  const { nuxtUsers } = useRuntimeConfig();
6
6
  const options = nuxtUsers;
@@ -18,5 +18,5 @@ export default defineEventHandler(async (event) => {
18
18
  statusMessage: "Unauthorized - Invalid authentication token"
19
19
  });
20
20
  }
21
- return user;
21
+ return { user };
22
22
  });
@@ -1,6 +1,7 @@
1
1
  import { defineEventHandler, getCookie, sendRedirect } from "h3";
2
2
  import { useRuntimeConfig } from "#imports";
3
3
  import { getCurrentUserFromToken } from "../utils/index.js";
4
+ const noAuthPaths = ["/login", "/api/auth/login", "/api/auth/forgot-password"];
4
5
  export default defineEventHandler(async (event) => {
5
6
  const { nuxtUsers } = useRuntimeConfig();
6
7
  const options = nuxtUsers;
@@ -8,7 +9,7 @@ export default defineEventHandler(async (event) => {
8
9
  if (!isPageOrApiRoute) {
9
10
  return;
10
11
  }
11
- if (event.path === "/login" || event.path === "/api/login") {
12
+ if (noAuthPaths.includes(event.path)) {
12
13
  console.log("[Nuxt Users] server.middleware.auth.global: /login");
13
14
  return;
14
15
  }
@@ -26,6 +27,6 @@ export default defineEventHandler(async (event) => {
26
27
  console.log(`[Nuxt Users] server.middleware.auth.global: ${event.path} No user found redirecting to login`);
27
28
  return sendRedirect(event, "/login");
28
29
  }
29
- console.log(`[Nuxt Users] server.middleware.auth.global: Authenticated request for ${user.id}`);
30
+ console.log(`[Nuxt Users] server.middleware.auth.global: Authenticated request to ${event.path} for ${user.id}`);
30
31
  return;
31
32
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-users",
3
- "version": "1.6.5",
3
+ "version": "1.6.6",
4
4
  "description": "A comprehensive user management module for Nuxt 3 applications with authentication, database support, and CLI tools",
5
5
  "keywords": [
6
6
  "nuxt",
@@ -1,77 +0,0 @@
1
- <script setup>
2
- import { ref, reactive } from "vue";
3
- const formData = reactive({
4
- email: ""
5
- });
6
- const message = ref("");
7
- const loading = ref(false);
8
- const isError = ref(false);
9
- const handleForgotPassword = async () => {
10
- loading.value = true;
11
- message.value = "";
12
- isError.value = false;
13
- try {
14
- const response = await fetch("/api/forgot-password", {
15
- method: "POST",
16
- headers: {
17
- "Content-Type": "application/json",
18
- "Accept": "application/json"
19
- },
20
- body: JSON.stringify({ email: formData.email })
21
- });
22
- const responseData = await response.json();
23
- if (!response.ok) {
24
- throw new Error(responseData.message || responseData.statusMessage || "An error occurred.");
25
- }
26
- message.value = responseData.message;
27
- isError.value = false;
28
- formData.email = "";
29
- } catch (err) {
30
- if (err instanceof Error) {
31
- message.value = err.message;
32
- } else {
33
- message.value = "Failed to send password reset link.";
34
- }
35
- console.error("[Nuxt Users] Forgot Password Form Error:", err);
36
- isError.value = true;
37
- } finally {
38
- loading.value = false;
39
- }
40
- };
41
- </script>
42
-
43
- <template>
44
- <div>
45
- <h2>Forgot Password</h2>
46
- <form @submit.prevent="handleForgotPassword">
47
- <div class="form-group">
48
- <label for="email">Email</label>
49
- <input
50
- id="email"
51
- v-model="formData.email"
52
- type="email"
53
- name="email"
54
- placeholder="your@email.com"
55
- required
56
- >
57
- </div>
58
- <button
59
- type="submit"
60
- :disabled="loading"
61
- >
62
- {{ loading ? "Sending..." : "Send Password Reset Link" }}
63
- </button>
64
- <p
65
- v-if="message"
66
- :class="{ error: isError, success: !isError }"
67
- class="form-message"
68
- >
69
- {{ message }}
70
- </p>
71
- </form>
72
- </div>
73
- </template>
74
-
75
- <style scoped>
76
- .error{color:red}.success{color:green}.form-message{font-size:.9rem;margin-top:1em}.form-group{margin-bottom:1em}label{display:block;font-weight:700;margin-bottom:.25em}input[type=email]{border:1px solid var(--color-border-dark);border-radius:4px;box-sizing:border-box;padding:.75em;width:100%}button[type=submit]{background-color:var(--color-gray-800);border:none;border-radius:4px;color:#fff;cursor:pointer;padding:.75em 1.5em}button[type=submit]:disabled{background-color:var(--color-gray-500);cursor:not-allowed}
77
- </style>
@@ -1,240 +0,0 @@
1
- <script setup>
2
- import { ref, onMounted, watch } from "vue";
3
- import { usePasswordValidation } from "../composables/usePasswordValidation";
4
- import { useRuntimeConfig } from "#imports";
5
- import PasswordStrengthIndicator from "./PasswordStrengthIndicator.vue";
6
- const props = defineProps({
7
- apiEndpoint: { type: String, required: false, default: "/api/profile" },
8
- updatePasswordEndpoint: { type: String, required: false, default: "/api/update-password" }
9
- });
10
- const emit = defineEmits(["success", "error", "password-error", "password-updated"]);
11
- const isLoading = ref(false);
12
- const isPasswordLoading = ref(false);
13
- const error = ref("");
14
- const passwordError = ref("");
15
- const passwordSuccess = ref("");
16
- const user = ref(null);
17
- const passwordForm = ref({
18
- currentPassword: "",
19
- newPassword: "",
20
- newPasswordConfirmation: ""
21
- });
22
- const { public: { nuxtUsers } } = useRuntimeConfig();
23
- const moduleOptions = nuxtUsers;
24
- const passwordValidation = usePasswordValidation(moduleOptions);
25
- watch(() => passwordForm.value.newPassword, (newPassword) => {
26
- if (newPassword) {
27
- passwordValidation.validate(newPassword);
28
- } else {
29
- passwordValidation.clearValidation();
30
- }
31
- });
32
- const loadProfile = async () => {
33
- isLoading.value = true;
34
- error.value = "";
35
- try {
36
- const response = await $fetch(props.apiEndpoint, {
37
- method: "GET"
38
- });
39
- user.value = response.user;
40
- emit("success", response.user);
41
- } catch (err) {
42
- const errorMessage = err && typeof err === "object" && "data" in err && err.data && typeof err.data === "object" && "statusMessage" in err.data ? String(err.data.statusMessage) : err instanceof Error ? err.message : "Failed to load profile";
43
- error.value = errorMessage;
44
- emit("error", errorMessage);
45
- } finally {
46
- isLoading.value = false;
47
- }
48
- };
49
- const updatePassword = async () => {
50
- isPasswordLoading.value = true;
51
- passwordError.value = "";
52
- passwordSuccess.value = "";
53
- try {
54
- await $fetch(props.updatePasswordEndpoint, {
55
- method: "POST",
56
- body: passwordForm.value
57
- });
58
- passwordSuccess.value = "Password updated successfully";
59
- emit("password-updated");
60
- passwordForm.value = {
61
- currentPassword: "",
62
- newPassword: "",
63
- newPasswordConfirmation: ""
64
- };
65
- } catch (err) {
66
- const errorMessage = err && typeof err === "object" && "data" in err && err.data && typeof err.data === "object" && "statusMessage" in err.data ? String(err.data.statusMessage) : err instanceof Error ? err.message : "Failed to update password";
67
- passwordError.value = errorMessage;
68
- emit("password-error", errorMessage);
69
- } finally {
70
- isPasswordLoading.value = false;
71
- }
72
- };
73
- const formatDate = (dateString) => {
74
- return new Date(dateString).toLocaleDateString("en-US", {
75
- year: "numeric",
76
- month: "long",
77
- day: "numeric",
78
- hour: "2-digit",
79
- minute: "2-digit"
80
- });
81
- };
82
- onMounted(() => {
83
- loadProfile();
84
- });
85
- </script>
86
-
87
- <template>
88
- <div class="profile-form">
89
- <!-- Profile Information Section -->
90
- <div class="profile-section">
91
- <h2 class="section-title">
92
- Profile Information
93
- </h2>
94
-
95
- <div
96
- v-if="isLoading"
97
- class="loading"
98
- >
99
- Loading profile...
100
- </div>
101
-
102
- <div
103
- v-else-if="error"
104
- class="error-message"
105
- >
106
- {{ error }}
107
- </div>
108
-
109
- <div
110
- v-else-if="user"
111
- class="profile-info"
112
- >
113
- <div class="info-row">
114
- <label class="info-label">Name:</label>
115
- <span class="info-value">{{ user.name }}</span>
116
- </div>
117
-
118
- <div class="info-row">
119
- <label class="info-label">Email:</label>
120
- <span class="info-value">{{ user.email }}</span>
121
- </div>
122
-
123
- <div class="info-row">
124
- <label class="info-label">Role:</label>
125
- <span class="info-value">{{ user.role }}</span>
126
- </div>
127
-
128
- <div class="info-row">
129
- <label class="info-label">Member since:</label>
130
- <span class="info-value">{{ formatDate(user.created_at) }}</span>
131
- </div>
132
-
133
- <div class="info-row">
134
- <label class="info-label">Last updated:</label>
135
- <span class="info-value">{{ formatDate(user.updated_at) }}</span>
136
- </div>
137
- </div>
138
- </div>
139
-
140
- <!-- Change Password Section -->
141
- <div class="password-section">
142
- <h2 class="section-title">
143
- Change Password
144
- </h2>
145
-
146
- <form
147
- class="password-form"
148
- @submit.prevent="updatePassword"
149
- >
150
- <div class="form-group">
151
- <label
152
- for="currentPassword"
153
- class="form-label"
154
- >Current Password</label>
155
- <input
156
- id="currentPassword"
157
- v-model="passwordForm.currentPassword"
158
- type="password"
159
- class="form-input"
160
- required
161
- :disabled="isPasswordLoading"
162
- >
163
- </div>
164
-
165
- <div class="form-group">
166
- <label
167
- for="newPassword"
168
- class="form-label"
169
- >New Password</label>
170
- <input
171
- id="newPassword"
172
- v-model="passwordForm.newPassword"
173
- type="password"
174
- class="form-input"
175
- required
176
- :minlength="moduleOptions.passwordValidation?.minLength || 8"
177
- :class="{ error: passwordValidation.errors.value.length > 0 && passwordForm.newPassword }"
178
- :disabled="isPasswordLoading"
179
- >
180
-
181
- <PasswordStrengthIndicator
182
- :password="passwordForm.newPassword"
183
- :validation-result="passwordValidation.validationResult.value"
184
- />
185
-
186
- <!-- Password requirements -->
187
- <small class="form-help">
188
- Password must contain at least {{ moduleOptions.passwordValidation?.minLength || 8 }} characters
189
- <span v-if="moduleOptions.passwordValidation?.requireUppercase">, including uppercase letters</span>
190
- <span v-if="moduleOptions.passwordValidation?.requireLowercase">, lowercase letters</span>
191
- <span v-if="moduleOptions.passwordValidation?.requireNumbers">, numbers</span>
192
- <span v-if="moduleOptions.passwordValidation?.requireSpecialChars">, and special characters</span>.
193
- </small>
194
- </div>
195
-
196
- <div class="form-group">
197
- <label
198
- for="newPasswordConfirmation"
199
- class="form-label"
200
- >Confirm New Password</label>
201
- <input
202
- id="newPasswordConfirmation"
203
- v-model="passwordForm.newPasswordConfirmation"
204
- type="password"
205
- class="form-input"
206
- required
207
- :disabled="isPasswordLoading"
208
- >
209
- </div>
210
-
211
- <div
212
- v-if="passwordError"
213
- class="error-message"
214
- >
215
- {{ passwordError }}
216
- </div>
217
-
218
- <div
219
- v-if="passwordSuccess"
220
- class="success-message"
221
- >
222
- {{ passwordSuccess }}
223
- </div>
224
-
225
- <button
226
- type="submit"
227
- class="btn btn-primary"
228
- :disabled="isPasswordLoading"
229
- >
230
- <span v-if="isPasswordLoading">Updating...</span>
231
- <span v-else>Update Password</span>
232
- </button>
233
- </form>
234
- </div>
235
- </div>
236
- </template>
237
-
238
- <style scoped>
239
- .profile-form{margin:0 auto;max-width:600px;padding:20px}.password-section,.profile-section{background:#fff;border:1px solid #e2e8f0;border-radius:8px;margin-bottom:24px;padding:24px}.section-title{border-bottom:2px solid #e2e8f0;color:#1a202c;font-size:1.5rem;font-weight:600;margin:0 0 20px;padding-bottom:12px}.loading{color:#718096;font-style:italic;text-align:center}.profile-info{display:flex;flex-direction:column;gap:16px}.info-row{align-items:center;border-bottom:1px solid #f7fafc;display:flex;padding:12px 0}.info-row:last-child{border-bottom:none}.info-label{color:#4a5568;font-weight:600;margin-right:16px;min-width:120px}.info-value{color:#2d3748;flex:1}.password-form{gap:20px}.form-group,.password-form{display:flex;flex-direction:column}.form-group{gap:8px}.form-label{color:#4a5568;font-size:.875rem;font-weight:500}.form-input{border:1px solid #e2e8f0;border-radius:6px;font-size:1rem;padding:12px 16px;transition:border-color .2s,box-shadow .2s}.form-input:focus{border-color:#4299e1;box-shadow:0 0 0 3px rgba(66,153,225,.1);outline:none}.form-input:disabled{background-color:#f7fafc;cursor:not-allowed}.form-help{color:#718096;font-size:.75rem;margin-top:4px}.btn{align-self:flex-start;border:none;border-radius:6px;cursor:pointer;font-size:1rem;font-weight:500;padding:12px 24px;transition:background-color .2s,transform .1s}.btn:disabled{cursor:not-allowed;opacity:.6;transform:none}.btn-primary{background-color:#4299e1;color:#fff}.btn-primary:hover:not(:disabled){background-color:#3182ce;transform:translateY(-1px)}.error-message{background-color:#fed7d7;border:1px solid #feb2b2;border-radius:6px;color:#e53e3e;font-size:.875rem;padding:12px}.success-message{background-color:#f0fff4;border:1px solid #9ae6b4;border-radius:6px;color:#38a169;font-size:.875rem;padding:12px}
240
- </style>
@@ -1,20 +0,0 @@
1
- import type { UserWithoutPassword } from '../../types.js';
2
- interface ProfileFormProps {
3
- apiEndpoint?: string;
4
- updatePasswordEndpoint?: string;
5
- }
6
- declare const _default: import("vue").DefineComponent<ProfileFormProps, void, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
7
- error: (error: string) => any;
8
- success: (user: UserWithoutPassword) => any;
9
- "password-error": (error: string) => any;
10
- "password-updated": () => any;
11
- }, string, import("vue").PublicProps, Readonly<ProfileFormProps> & Readonly<{
12
- onError?: ((error: string) => any) | undefined;
13
- onSuccess?: ((user: UserWithoutPassword) => any) | undefined;
14
- "onPassword-error"?: ((error: string) => any) | undefined;
15
- "onPassword-updated"?: (() => any) | undefined;
16
- }>, {
17
- apiEndpoint: string;
18
- updatePasswordEndpoint: string;
19
- }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
20
- export default _default;
@@ -1,2 +0,0 @@
1
- declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<import("../../../types.js").UserWithoutPassword>>;
2
- export default _default;