nuxt-users 1.11.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 (38) hide show
  1. package/CHANGELOG.md +33 -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 +1 -1
  6. package/dist/module.mjs +30 -9
  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 -23
  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} +22 -26
  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/usePasswordValidation.d.ts +2 -2
  23. package/dist/runtime/server/api/nuxt-users/index.get.d.ts +20 -0
  24. package/dist/runtime/server/api/nuxt-users/index.get.js +58 -0
  25. package/dist/runtime/server/api/nuxt-users/session/index.post.d.ts +1 -0
  26. package/dist/runtime/server/middleware/authorization.server.js +23 -8
  27. package/dist/runtime/server/utils/user.js +8 -5
  28. package/dist/shared/{nuxt-users.C7T1DKBV.d.mts → nuxt-users.B5ttEpIO.d.mts} +1 -0
  29. package/dist/shared/{nuxt-users.C7T1DKBV.d.ts → nuxt-users.B5ttEpIO.d.ts} +1 -0
  30. package/dist/utils.d.mts +1 -2
  31. package/dist/utils.d.ts +1 -2
  32. package/dist/utils.mjs +162 -2
  33. package/package.json +1 -1
  34. package/dist/runtime/assets/colors.css +0 -1
  35. package/dist/shared/nuxt-users.BnOQs2LI.mjs +0 -162
  36. package/dist/runtime/components/{LogoutLink.vue.d.ts → NUsersLogoutLink.vue.d.ts} +4 -4
  37. /package/dist/runtime/components/{PasswordStrengthIndicator.vue.d.ts → NUsersPasswordStrengthIndicator.vue.d.ts} +0 -0
  38. /package/dist/runtime/components/{ProfileInfo.vue.d.ts → NUsersProfileInfo.vue.d.ts} +0 -0
package/CHANGELOG.md CHANGED
@@ -1,6 +1,39 @@
1
1
  # Changelog
2
2
 
3
3
 
4
+ ## v1.12.0
5
+
6
+ [compare changes](https://github.com/rrd108/nuxt-users/compare/v1.11.0...v1.12.0)
7
+
8
+ ### 🚀 Enhancements
9
+
10
+ - Enhances user management and login flow ([894563a](https://github.com/rrd108/nuxt-users/commit/894563a))
11
+ - Implement new components ([87689c9](https://github.com/rrd108/nuxt-users/commit/87689c9))
12
+
13
+ ### 🩹 Fixes
14
+
15
+ - Authorization middleware for API routes closes #76 ([#76](https://github.com/rrd108/nuxt-users/issues/76))
16
+ - (temporary) strange test failing ([ff8c376](https://github.com/rrd108/nuxt-users/commit/ff8c376))
17
+
18
+ ### 💅 Refactors
19
+
20
+ - Component names ([159f334](https://github.com/rrd108/nuxt-users/commit/159f334))
21
+ - Css classes renamed, removed or added ([c5c3334](https://github.com/rrd108/nuxt-users/commit/c5c3334))
22
+ - Remove unused css ([7abc8a5](https://github.com/rrd108/nuxt-users/commit/7abc8a5))
23
+
24
+ ### 🏡 Chore
25
+
26
+ - Bump version number ([5045086](https://github.com/rrd108/nuxt-users/commit/5045086))
27
+
28
+ ### ✅ Tests
29
+
30
+ - Fix ([cbf300c](https://github.com/rrd108/nuxt-users/commit/cbf300c))
31
+ - Fix failing test ([3973eec](https://github.com/rrd108/nuxt-users/commit/3973eec))
32
+
33
+ ### ❤️ Contributors
34
+
35
+ - Rrd108 ([@rrd108](https://github.com/rrd108))
36
+
4
37
  ## v1.11.0
5
38
 
6
39
  [compare changes](https://github.com/rrd108/nuxt-users/compare/v1.9.0...v1.11.0)
package/dist/cli.mjs CHANGED
@@ -4,8 +4,7 @@ import { defaultOptions } from './module.mjs';
4
4
  import { loadNuxt } from '@nuxt/kit';
5
5
  import { defu } from 'defu';
6
6
  import { runMigrations, createUser as createUser$1, createUsersTable as createUsersTable$1, createPersonalAccessTokensTable as createPersonalAccessTokensTable$1, createPasswordResetTokensTable as createPasswordResetTokensTable$1, createMigrationsTable as createMigrationsTable$1 } from '../dist/runtime/server/utils/index.js';
7
- import { g as getPasswordValidationOptions, v as validatePassword } from './shared/nuxt-users.BnOQs2LI.mjs';
8
- import '../dist/runtime/server/utils/user.js';
7
+ import { getPasswordValidationOptions, validatePassword } from './utils.mjs';
9
8
 
10
9
  const getOptionsFromEnv = () => {
11
10
  const connectorName = process.env.DB_CONNECTOR || "sqlite";
@@ -248,7 +247,7 @@ const main = defineCommand({
248
247
  meta: {
249
248
  name: "nuxt-users",
250
249
  description: "CLI for Nuxt Users Module - Manage users, migrations, and database operations",
251
- version: "1.10.0"
250
+ version: "1.11.0"
252
251
  },
253
252
  subCommands: {
254
253
  migrate,
package/dist/module.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as _nuxt_schema from '@nuxt/schema';
2
- import { R as RuntimeModuleOptions, M as ModuleOptions } from './shared/nuxt-users.C7T1DKBV.mjs';
2
+ import { R as RuntimeModuleOptions, M as ModuleOptions } from './shared/nuxt-users.B5ttEpIO.mjs';
3
3
  export * from '../dist/runtime/server/utils/index.js';
4
4
 
5
5
  declare const defaultOptions: ModuleOptions;
package/dist/module.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as _nuxt_schema from '@nuxt/schema';
2
- import { R as RuntimeModuleOptions, M as ModuleOptions } from './shared/nuxt-users.C7T1DKBV.js';
2
+ import { R as RuntimeModuleOptions, M as ModuleOptions } from './shared/nuxt-users.B5ttEpIO.js';
3
3
  export * from '../dist/runtime/server/utils/index.js';
4
4
 
5
5
  declare const defaultOptions: ModuleOptions;
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-users",
3
3
  "configKey": "nuxtUsers",
4
- "version": "1.11.0",
4
+ "version": "1.12.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.0"
package/dist/module.mjs CHANGED
@@ -130,6 +130,11 @@ const module = defineNuxtModule({
130
130
  method: "post",
131
131
  handler: resolver.resolve("./runtime/server/api/nuxt-users/password/reset.post")
132
132
  });
133
+ addServerHandler({
134
+ route: `${base}`,
135
+ method: "get",
136
+ handler: resolver.resolve("./runtime/server/api/nuxt-users/index.get")
137
+ });
133
138
  addServerHandler({
134
139
  route: `${base}`,
135
140
  method: "post",
@@ -180,23 +185,39 @@ const module = defineNuxtModule({
180
185
  nitroConfig.scanDirs.push(resolver.resolve("./runtime/server/tasks"));
181
186
  });
182
187
  addComponent({
183
- name: "LoginForm",
184
- filePath: resolver.resolve("./runtime/components/LoginForm.vue")
188
+ name: "NUsersLoginForm",
189
+ filePath: resolver.resolve("./runtime/components/NUsersLoginForm.vue")
190
+ });
191
+ addComponent({
192
+ name: "NUsersLogoutLink",
193
+ filePath: resolver.resolve("./runtime/components/NUsersLogoutLink.vue")
194
+ });
195
+ addComponent({
196
+ name: "NUsersProfileInfo",
197
+ filePath: resolver.resolve("./runtime/components/NUsersProfileInfo.vue")
198
+ });
199
+ addComponent({
200
+ name: "NUsersResetPasswordForm",
201
+ filePath: resolver.resolve("./runtime/components/NUsersResetPasswordForm.vue")
202
+ });
203
+ addComponent({
204
+ name: "NUsersPasswordStrengthIndicator",
205
+ filePath: resolver.resolve("./runtime/components/NUsersPasswordStrengthIndicator.vue")
185
206
  });
186
207
  addComponent({
187
- name: "LogoutLink",
188
- filePath: resolver.resolve("./runtime/components/LogoutLink.vue")
208
+ name: "NUsersList",
209
+ filePath: resolver.resolve("./runtime/components/NUsersList.vue")
189
210
  });
190
211
  addComponent({
191
- name: "ProfileInfo",
192
- filePath: resolver.resolve("./runtime/components/ProfileInfo.vue")
212
+ name: "NUsersUserCard",
213
+ filePath: resolver.resolve("./runtime/components/NUsersUserCard.vue")
193
214
  });
194
215
  addComponent({
195
- name: "ResetPasswordForm",
196
- filePath: resolver.resolve("./runtime/components/ResetPasswordForm.vue")
216
+ name: "NUsersUserForm",
217
+ filePath: resolver.resolve("./runtime/components/NUsersUserForm.vue")
197
218
  });
198
219
  nuxt.options.css = nuxt.options.css || [];
199
- nuxt.options.css.push(resolver.resolve("./runtime/assets/colors.css"));
220
+ nuxt.options.css.push(resolver.resolve("./runtime/assets/nuxt-users.css"));
200
221
  }
201
222
  });
202
223
 
@@ -0,0 +1 @@
1
+ :root{--color-primary:#3b82f6;--color-primary-dark:#2563eb;--color-primary-light:#60a5fa;--color-secondary:#059669;--color-secondary-dark:#047857;--color-secondary-light:#10b981;--color-white:#fff;--color-black:#000;--color-gray-50:#f9fafb;--color-gray-100:#f3f4f6;--color-gray-200:#e5e7eb;--color-gray-300:#d1d5db;--color-gray-400:#9ca3af;--color-gray-500:#6b7280;--color-gray-600:#4b5563;--color-gray-700:#374151;--color-gray-800:#1f2937;--color-gray-900:#111827;--color-success:#059669;--color-success-light:#f0fdf4;--color-success-border:#9ae6b4;--color-warning:#f59e0b;--color-warning-light:#fef3c7;--color-warning-text:#92400e;--color-error:#dc2626;--color-error-light:#fef2f2;--color-error-border:#fecaca;--color-error-alt:#db5050;--color-error-dark:#b91c1c;--color-border:#e5e7eb;--color-border-light:#d1d5db;--color-border-dark:#ccc;--color-bg-primary:#fff;--color-bg-secondary:#f9fafb;--color-bg-tertiary:#f3f4f6;--color-text-primary:#000;--color-text-secondary:#6b7280;--color-strength-weak:#dc3545;--color-strength-medium:#ffc107;--color-strength-strong:#28a745;--color-strength-unknown:#6c757d;--color-strength-bg:#e9ecef;--color-validation-passed:#28a745;--color-validation-failed:#dc3545;--color-validation-hint:#6c757d}@media (prefers-color-scheme:dark){.dark,:root{--color-primary:#60a5fa;--color-primary-dark:#3b82f6;--color-primary-light:#93c5fd;--color-secondary:#10b981;--color-secondary-dark:#059669;--color-secondary-light:#34d399;--color-white:#000;--color-black:#fff;--color-gray-50:#111827;--color-gray-100:#1f2937;--color-gray-200:#374151;--color-gray-300:#4b5563;--color-gray-400:#6b7280;--color-gray-500:#9ca3af;--color-gray-600:#d1d5db;--color-gray-700:#e5e7eb;--color-gray-800:#f3f4f6;--color-gray-900:#f9fafb;--color-success:#10b981;--color-success-light:#064e3b;--color-success-border:#065f46;--color-warning:#fbbf24;--color-warning-light:#451a03;--color-warning-text:#fef3c7;--color-error:#ef4444;--color-error-light:#450a0a;--color-error-border:#7f1d1d;--color-error-alt:#f87171;--color-error-dark:#dc2626;--color-border:#374151;--color-border-light:#4b5563;--color-border-dark:#6b7280;--color-bg-primary:#111827;--color-bg-secondary:#1f2937;--color-bg-tertiary:#374151;--color-text-primary:#fff;--color-text-secondary:#9ca3af;--color-strength-weak:#ef4444;--color-strength-medium:#fbbf24;--color-strength-strong:#22c55e;--color-strength-unknown:#9ca3af;--color-strength-bg:#374151;--color-validation-passed:#22c55e;--color-validation-failed:#ef4444;--color-validation-hint:#9ca3af}}.n-users-form-group{display:flex;flex-direction:column;gap:.5rem;margin-bottom:1rem;width:100%}.n-users-form-group.n-users-form-group-narrow{width:90%}.n-users-form-group label,.n-users-form-label{color:var(--color-text-primary,#000);display:block;font-size:1rem;font-weight:500;margin-bottom:.5rem}.n-users-form-group input,.n-users-form-group select,.n-users-form-input{border:1px solid var(--color-border,#ccc);border-radius:.25rem;font-size:1rem;padding:.5rem;transition:border-color .2s,box-shadow .2s;width:100%}.n-users-form-group input:focus,.n-users-form-group select:focus,.n-users-form-input:focus{border-color:var(--color-primary);box-shadow:0 0 0 .1rem rgba(59,130,246,.1);outline:none}.n-users-form-group input:disabled,.n-users-form-input:disabled{background-color:var(--color-bg-secondary,#f9fafb);cursor:not-allowed}.n-users-form-group input.error,.n-users-form-input.error{border-color:var(--color-error,#dc3545)}.n-users-form-group input[type=email],.n-users-form-group input[type=password],.n-users-form-group input[type=text]{background-color:var(--color-bg-secondary);border:1px solid var(--color-border-light);border-radius:.5em;box-sizing:border-box;color:var(--color-gray-700);font-size:.875rem;padding:.75em 1em;transition:all .2s ease-in-out;width:100%}.n-users-form-group input[type=email]:focus,.n-users-form-group input[type=password]:focus,.n-users-form-group input[type=text]: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}.n-users-form-group input[type=checkbox]{margin:0;width:auto}.n-users-btn,.n-users-cancel-btn,.n-users-delete-btn,.n-users-edit-btn,.n-users-submit-btn{align-self:flex-start;border:none;border-radius:.25rem;cursor:pointer;display:inline-block;font-size:1rem;font-weight:500;padding:.5rem 1rem;text-decoration:none;transition:background-color .2s,transform .1s}.n-users-btn:hover,.n-users-edit-btn:hover,.n-users-submit-btn:hover{background-color:var(--color-primary-dark,#2563eb);transform:translateY(-1px)}.n-users-btn:disabled,.n-users-submit-btn:disabled{cursor:not-allowed;opacity:.6;transform:none}.n-users-btn-primary,.n-users-edit-btn,.n-users-submit-btn{background-color:var(--color-primary,#3b82f6);color:#fff}.n-users-btn-secondary,.n-users-cancel-btn{background-color:var(--color-gray-500,#6b7280);color:#fff}.n-users-cancel-btn:hover{background-color:var(--color-gray-600,#4b5563)}.n-users-delete-btn{background-color:var(--color-error,#ef4444);color:#fff}.n-users-delete-btn:hover{background-color:var(--color-error-alt,#dc2626)}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}.n-users-error-message,.n-users-error-text{background-color:var(--color-error-light,#fef2f2);border:1px solid var(--color-error-border,#fecaca);border-radius:.5rem;color:var(--color-error,#dc3545);font-size:.875rem;margin-top:.5rem;padding:.75rem 1rem}.n-users-success-message{background-color:var(--color-success-light,#f0fff4);border:1px solid var(--color-success-border,#9ae6b4);border-radius:.5rem;color:var(--color-success,#10b981);font-size:.875rem;margin-top:.5rem;padding:.75rem 1rem}.n-users-grid{display:grid;gap:1rem}.n-users-grid-2{grid-template-columns:1fr 5fr}.n-users-grid-auto{grid-template-columns:repeat(auto-fill,minmax(12em,1fr))}.n-users-form-actions{align-items:center;display:flex;gap:1rem;margin-top:1.5rem}.n-users-loading-spinner{animation:n-users-spin 1s linear infinite;border:2px solid var(--color-white,#fff);border-radius:50%;border-top:2px solid transparent;height:16px;width:16px}@keyframes n-users-spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}.n-users-card{background-color:var(--color-bg-primary,#fff);border:1px solid var(--color-border,#e5e7eb);border-radius:.5rem;padding:1rem}.n-users-section{margin:0 auto;max-width:42em;padding:1em}.n-users-section-header{border-bottom:.1em solid var(--color-border,#e5e7eb);margin-bottom:1rem;padding-bottom:.5rem}.n-users-form-help{color:var(--color-text-secondary,#6b7280);font-size:.75rem;margin-top:.25rem}.n-users-strength-bar{background-color:var(--color-strength-bg,#e9ecef);border-radius:2px;height:4px;margin-bottom:4px;overflow:hidden;width:100%}.n-users-strength-fill{height:100%;transition:width .3s ease,background-color .3s ease}.n-users-strength-text{font-size:.75rem;font-weight:500}.n-users-validation-container{background-color:#f8f9fa;border:1px solid #e9ecef;border-radius:4px;margin-top:12px;padding:12px}.n-users-container-title{color:#495057;font-size:.875rem;font-weight:600;margin-bottom:8px}.n-users-list-unstyled{list-style:none;margin:0;padding-left:0}.n-users-list-item{align-items:center;display:flex;font-size:.875rem;margin-bottom:6px;transition:color .2s ease}.n-users-list-item:last-child{margin-bottom:0}.n-users-icon{font-weight:700;margin-right:8px;min-width:16px;text-align:center}.n-users-text{flex:1}.n-users-list ul{list-style:none}form{display:flex;flex-direction:column;gap:1.5rem}.n-users-login-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);max-width:25rem}.n-users-login-header{margin-bottom:1em;text-align:center}.n-users-login-title{color:var(--color-gray-800);font-size:1.875rem;font-weight:700;line-height:1.2;margin:0 0 .5em}.n-users-login-subtitle{color:var(--color-gray-700);font-size:.875rem;line-height:1.5;margin:0}.n-users-remember-me{align-items:center;display:flex;gap:.5rem}.n-users-remember-me label{color:var(--color-gray-700);font-size:.875rem;margin:0}.n-users-login-footer{margin-top:1em;text-align:center}.n-users-forgot-password{margin:0}.n-users-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}.n-users-forgot-link:hover:not(.loading){color:var(--color-primary-dark);text-decoration:underline}.n-users-forgot-link.loading{cursor:not-allowed;opacity:.6;pointer-events:none}.n-users-password-strength{margin-top:8px}.n-users-validation-rules{margin-top:12px}.n-users-rules-title{margin-bottom:8px}.n-users-rules-list{padding-left:0}.n-users-rule-item{margin-bottom:6px}.n-users-rule-item:last-child{margin-bottom:0}.n-users-rule-passed{color:var(--color-validation-passed,#28a745)}.n-users-rule-failed{color:var(--color-validation-failed,#dc3545)}.n-users-rule-icon{margin-right:8px}.n-users-rule-text{flex:1}.n-users-validation-errors{margin-top:8px}.n-users-error-text:last-child{margin-bottom:0}.n-users-password-hints{margin-top:8px}.n-users-hint-title{margin-bottom:8px}.n-users-hint-list{padding-left:16px}.n-users-hint-item{color:var(--color-validation-hint,#6c757d);margin-bottom:4px}.n-users-hint-item:last-child{margin-bottom:0}.n-users-logout-link{background:var(--color-error);border:2px solid transparent;border-color:var(--color-error-dark);border-radius:.5em;color:#fff;padding:.5em 1em;text-decoration:none}.n-users-logout-link:hover{border-color:var(--color-primary)}.n-users-user-card dd{margin:0}.n-users-user-card-actions{display:flex;gap:.5rem;margin-top:1rem}.n-users-user-form{margin:0 auto;max-width:500px}.n-users-form-group input.error{border-color:#dc3545}.n-users-profile-info{background-color:var(--color-bg-primary);border-top-left-radius:.5em;border-top-right-radius:.5em}.n-users-profile-details{font-size:1rem;list-style:none;margin:0;padding:0}.n-users-reset-password-container{background-color:var(--color-bg-primary);border-bottom-left-radius:.5em;border-bottom-right-radius:.5em}@media (max-width:480px){.n-users-login-title{font-size:1.5rem}}
@@ -0,0 +1,136 @@
1
+ <script setup>
2
+ import { ref, onMounted } from "vue";
3
+ import { useRuntimeConfig } from "#imports";
4
+ import { defaultDisplayFields, defaultFieldLabels } from "../../types";
5
+ defineProps({
6
+ displayFields: { type: Array, required: false, default: () => defaultDisplayFields },
7
+ fieldLabels: { type: Object, required: false, default: () => defaultFieldLabels }
8
+ });
9
+ defineEmits(["editClick", "delete"]);
10
+ const { public: { nuxtUsers } } = useRuntimeConfig();
11
+ const users = ref([]);
12
+ const pagination = ref(null);
13
+ const loading = ref(false);
14
+ const error = ref(null);
15
+ const fetchUsers = async (page = 1, limit = 100) => {
16
+ loading.value = true;
17
+ error.value = null;
18
+ try {
19
+ const response = await $fetch(`${nuxtUsers.apiBasePath}?page=${page}&limit=${limit}`);
20
+ users.value = response.users;
21
+ pagination.value = response.pagination;
22
+ } catch (err) {
23
+ error.value = err instanceof Error ? err.message : "Failed to fetch users";
24
+ } finally {
25
+ loading.value = false;
26
+ }
27
+ };
28
+ const refresh = () => {
29
+ fetchUsers();
30
+ };
31
+ defineExpose({
32
+ refresh
33
+ });
34
+ onMounted(() => {
35
+ fetchUsers();
36
+ });
37
+ </script>
38
+
39
+ <template>
40
+ <div class="n-users-list">
41
+ <slot name="title">
42
+ <h2>
43
+ Users List
44
+ </h2>
45
+ </slot>
46
+
47
+ <slot
48
+ name="loading"
49
+ :loading="loading"
50
+ >
51
+ <div v-if="loading">
52
+ Loading users...
53
+ </div>
54
+ </slot>
55
+
56
+ <slot
57
+ name="error"
58
+ :error="error"
59
+ >
60
+ <div v-if="error">
61
+ Error: {{ error }}
62
+ </div>
63
+ </slot>
64
+
65
+ <slot name="noUsers">
66
+ <div v-if="users.length === 0">
67
+ No users found
68
+ </div>
69
+ </slot>
70
+
71
+ <div v-if="users.length">
72
+ <slot name="usersList">
73
+ <ul class="n-users-grid n-users-grid-auto">
74
+ <li
75
+ v-for="(user, index) in users"
76
+ :key="user.id"
77
+ >
78
+ <slot
79
+ name="user"
80
+ :user="user"
81
+ :index="index"
82
+ >
83
+ <NUsersUserCard
84
+ :user="user"
85
+ :index="index"
86
+ :display-fields="displayFields"
87
+ :field-labels="fieldLabels"
88
+ @edit-click="$emit('editClick', user)"
89
+ @delete="$emit('delete', user)"
90
+ />
91
+ </slot>
92
+ </li>
93
+ </ul>
94
+ </slot>
95
+
96
+ <slot
97
+ name="paginationInfo"
98
+ :pagination="pagination"
99
+ >
100
+ <div v-if="pagination && pagination.totalPages > 1">
101
+ <div>
102
+ <span>Page {{ pagination.page }} of {{ pagination.totalPages }}</span>
103
+ <span>Total: {{ pagination.total }} users</span>
104
+ </div>
105
+ </div>
106
+ </slot>
107
+
108
+ <slot
109
+ name="pagination"
110
+ :pagination="pagination"
111
+ :fetch-users="fetchUsers"
112
+ :loading="loading"
113
+ >
114
+ <div v-if="pagination && pagination.totalPages > 1">
115
+ <div>
116
+ <button
117
+ v-if="pagination.hasPrev"
118
+ :disabled="loading"
119
+ @click="fetchUsers(pagination.page - 1)"
120
+ >
121
+ Previous
122
+ </button>
123
+
124
+ <button
125
+ v-if="pagination.hasNext"
126
+ :disabled="loading"
127
+ @click="fetchUsers(pagination.page + 1)"
128
+ >
129
+ Next
130
+ </button>
131
+ </div>
132
+ </div>
133
+ </slot>
134
+ </div>
135
+ </div>
136
+ </template>
@@ -0,0 +1,74 @@
1
+ import { type DisplayFieldsProps, type User } from '../../types.js';
2
+ declare var __VLS_1: {}, __VLS_3: {
3
+ loading: boolean;
4
+ }, __VLS_5: {
5
+ error: string | null;
6
+ }, __VLS_7: {}, __VLS_9: {}, __VLS_11: {
7
+ user: {
8
+ id: number;
9
+ email: string;
10
+ name: string;
11
+ password: string;
12
+ role: string;
13
+ created_at: string;
14
+ updated_at: string;
15
+ last_login_at?: string | undefined;
16
+ };
17
+ index: number;
18
+ }, __VLS_22: {
19
+ pagination: {
20
+ page: number;
21
+ limit: number;
22
+ total: number;
23
+ totalPages: number;
24
+ hasNext: boolean;
25
+ hasPrev: boolean;
26
+ } | null;
27
+ }, __VLS_24: {
28
+ pagination: {
29
+ page: number;
30
+ limit: number;
31
+ total: number;
32
+ totalPages: number;
33
+ hasNext: boolean;
34
+ hasPrev: boolean;
35
+ } | null;
36
+ fetchUsers: (page?: number, limit?: number) => Promise<void>;
37
+ loading: boolean;
38
+ };
39
+ type __VLS_Slots = {} & {
40
+ title?: (props: typeof __VLS_1) => any;
41
+ } & {
42
+ loading?: (props: typeof __VLS_3) => any;
43
+ } & {
44
+ error?: (props: typeof __VLS_5) => any;
45
+ } & {
46
+ noUsers?: (props: typeof __VLS_7) => any;
47
+ } & {
48
+ usersList?: (props: typeof __VLS_9) => any;
49
+ } & {
50
+ user?: (props: typeof __VLS_11) => any;
51
+ } & {
52
+ paginationInfo?: (props: typeof __VLS_22) => any;
53
+ } & {
54
+ pagination?: (props: typeof __VLS_24) => any;
55
+ };
56
+ declare const __VLS_component: import("vue").DefineComponent<DisplayFieldsProps, {
57
+ refresh: () => void;
58
+ }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
59
+ delete: (user: User) => void;
60
+ editClick: (user: User) => void;
61
+ }, string, import("vue").PublicProps, Readonly<DisplayFieldsProps> & Readonly<{
62
+ onDelete?: ((user: User) => any) | undefined;
63
+ onEditClick?: ((user: User) => any) | undefined;
64
+ }>, {
65
+ displayFields: string[];
66
+ fieldLabels: Record<string, string>;
67
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
68
+ declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
69
+ export default _default;
70
+ type __VLS_WithSlots<T, S> = T & {
71
+ new (): {
72
+ $slots: S;
73
+ };
74
+ };
@@ -1,16 +1,17 @@
1
1
  <script setup>
2
- import { ref } from "vue";
2
+ import { ref, computed } from "vue";
3
3
  import { navigateTo } from "#app";
4
4
  import { useRuntimeConfig } from "#imports";
5
5
  const { public: { nuxtUsers } } = useRuntimeConfig();
6
6
  const { passwordValidation } = nuxtUsers;
7
- const apiBasePath = nuxtUsers?.apiBasePath;
8
7
  const props = defineProps({
9
- apiEndpoint: { type: String, required: false, default: `${apiBasePath}/session` },
10
- redirectTo: { type: String, required: false, default: "/" },
11
- forgotPasswordEndpoint: { type: String, required: false, default: `${apiBasePath}/password/forgot` }
8
+ apiEndpoint: { type: String, required: false },
9
+ redirectTo: { type: String, required: false },
10
+ forgotPasswordEndpoint: { type: String, required: false }
12
11
  });
13
12
  const emit = defineEmits(["success", "error", "forgot-password-error", "submit", "forgot-password-success"]);
13
+ const apiEndpoint = computed(() => props.apiEndpoint || `${nuxtUsers?.apiBasePath}/session`);
14
+ const forgotPasswordEndpoint = computed(() => props.forgotPasswordEndpoint || `${nuxtUsers?.apiBasePath}/password/forgot`);
14
15
  const isLoading = ref(false);
15
16
  const isForgotPasswordLoading = ref(false);
16
17
  const error = ref("");
@@ -25,7 +26,7 @@ const handleSubmit = async () => {
25
26
  error.value = "";
26
27
  try {
27
28
  emit("submit", formData.value);
28
- const response = await $fetch(props.apiEndpoint, {
29
+ const response = await $fetch(apiEndpoint.value, {
29
30
  method: "POST",
30
31
  body: {
31
32
  email: formData.value.email,
@@ -53,7 +54,7 @@ const handleForgotPassword = async () => {
53
54
  forgotPasswordMessage.value = "";
54
55
  error.value = "";
55
56
  try {
56
- const response = await $fetch(props.forgotPasswordEndpoint, {
57
+ const response = await $fetch(forgotPasswordEndpoint.value, {
57
58
  method: "POST",
58
59
  body: {
59
60
  email: formData.value.email
@@ -72,15 +73,15 @@ const handleForgotPassword = async () => {
72
73
  </script>
73
74
 
74
75
  <template>
75
- <section>
76
+ <section class="n-users-login-section n-users-section">
76
77
  <form @submit.prevent="handleSubmit">
77
78
  <!-- Header slot -->
78
79
  <slot name="header">
79
- <div class="login-header">
80
- <h2 class="login-title">
80
+ <div class="n-users-login-header">
81
+ <h2 class="n-users-login-title">
81
82
  Welcome Back
82
83
  </h2>
83
- <p class="login-subtitle">
84
+ <p class="n-users-login-subtitle">
84
85
  Sign in to your account
85
86
  </p>
86
87
  </div>
@@ -88,7 +89,7 @@ const handleForgotPassword = async () => {
88
89
 
89
90
  <!-- Email field -->
90
91
  <slot name="email-field">
91
- <div class="form-group">
92
+ <div class="n-users-form-group">
92
93
  <label for="email">Email</label>
93
94
  <input
94
95
  id="email"
@@ -103,7 +104,7 @@ const handleForgotPassword = async () => {
103
104
 
104
105
  <!-- Password field -->
105
106
  <slot name="password-field">
106
- <div class="form-group">
107
+ <div class="n-users-form-group">
107
108
  <label for="password">Password</label>
108
109
  <input
109
110
  id="password"
@@ -119,7 +120,7 @@ const handleForgotPassword = async () => {
119
120
 
120
121
  <!-- Remember me slot -->
121
122
  <slot name="remember-me">
122
- <div class="remember-me">
123
+ <div class="n-users-remember-me">
123
124
  <input
124
125
  id="rememberMe"
125
126
  v-model="formData.rememberMe"
@@ -138,7 +139,7 @@ const handleForgotPassword = async () => {
138
139
  >
139
140
  <span
140
141
  v-if="isLoading"
141
- class="loading-spinner"
142
+ class="n-users-loading-spinner"
142
143
  />
143
144
  {{ isLoading ? "Signing in..." : "Sign In" }}
144
145
  </button>
@@ -146,17 +147,17 @@ const handleForgotPassword = async () => {
146
147
 
147
148
  <!-- Footer slot -->
148
149
  <slot name="footer">
149
- <div class="login-footer">
150
- <p class="forgot-password">
150
+ <div class="n-users-login-footer">
151
+ <p class="n-users-forgot-password">
151
152
  <a
152
153
  href="#"
153
- class="forgot-link"
154
+ class="n-users-forgot-link"
154
155
  :class="{ loading: isForgotPasswordLoading }"
155
156
  @click.prevent="handleForgotPassword"
156
157
  >
157
158
  <span
158
159
  v-if="isForgotPasswordLoading"
159
- class="loading-spinner"
160
+ class="n-users-loading-spinner"
160
161
  />
161
162
  {{ isForgotPasswordLoading ? "Sending..." : "Forgot your password?" }}
162
163
  </a>
@@ -192,7 +193,3 @@ const handleForgotPassword = async () => {
192
193
  </form>
193
194
  </section>
194
195
  </template>
195
-
196
- <style scoped>
197
- 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}}
198
- </style>
@@ -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>