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.
- package/CHANGELOG.md +33 -0
- package/dist/cli.mjs +2 -3
- package/dist/module.d.mts +1 -1
- package/dist/module.d.ts +1 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +30 -9
- package/dist/runtime/assets/nuxt-users.css +1 -0
- package/dist/runtime/components/NUsersList.vue +136 -0
- package/dist/runtime/components/NUsersList.vue.d.ts +74 -0
- package/dist/runtime/components/{LoginForm.vue → NUsersLoginForm.vue} +20 -23
- package/dist/runtime/components/{LoginForm.vue.d.ts → NUsersLoginForm.vue.d.ts} +5 -9
- package/dist/runtime/components/{LogoutLink.vue → NUsersLogoutLink.vue} +2 -6
- package/dist/runtime/components/{PasswordStrengthIndicator.vue → NUsersPasswordStrengthIndicator.vue} +18 -22
- package/dist/runtime/components/{ProfileInfo.vue → NUsersProfileInfo.vue} +5 -7
- package/dist/runtime/components/{ResetPasswordForm.vue → NUsersResetPasswordForm.vue} +22 -26
- package/dist/runtime/components/{ResetPasswordForm.vue.d.ts → NUsersResetPasswordForm.vue.d.ts} +3 -6
- package/dist/runtime/components/NUsersUserCard.vue +93 -0
- package/dist/runtime/components/NUsersUserCard.vue.d.ts +33 -0
- package/dist/runtime/components/NUsersUserForm.vue +141 -0
- package/dist/runtime/components/NUsersUserForm.vue.d.ts +12 -0
- package/dist/runtime/composables/useAuthentication.d.ts +2 -0
- package/dist/runtime/composables/usePasswordValidation.d.ts +2 -2
- package/dist/runtime/server/api/nuxt-users/index.get.d.ts +20 -0
- package/dist/runtime/server/api/nuxt-users/index.get.js +58 -0
- package/dist/runtime/server/api/nuxt-users/session/index.post.d.ts +1 -0
- package/dist/runtime/server/middleware/authorization.server.js +23 -8
- package/dist/runtime/server/utils/user.js +8 -5
- package/dist/shared/{nuxt-users.C7T1DKBV.d.mts → nuxt-users.B5ttEpIO.d.mts} +1 -0
- package/dist/shared/{nuxt-users.C7T1DKBV.d.ts → nuxt-users.B5ttEpIO.d.ts} +1 -0
- package/dist/utils.d.mts +1 -2
- package/dist/utils.d.ts +1 -2
- package/dist/utils.mjs +162 -2
- package/package.json +1 -1
- package/dist/runtime/assets/colors.css +0 -1
- package/dist/shared/nuxt-users.BnOQs2LI.mjs +0 -162
- package/dist/runtime/components/{LogoutLink.vue.d.ts → NUsersLogoutLink.vue.d.ts} +4 -4
- /package/dist/runtime/components/{PasswordStrengthIndicator.vue.d.ts → NUsersPasswordStrengthIndicator.vue.d.ts} +0 -0
- /package/dist/runtime/components/{ProfileInfo.vue.d.ts → NUsersProfileInfo.vue.d.ts} +0 -0
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 {
|
|
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.
|
|
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.
|
|
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.
|
|
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
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: "
|
|
184
|
-
filePath: resolver.resolve("./runtime/components/
|
|
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: "
|
|
188
|
-
filePath: resolver.resolve("./runtime/components/
|
|
208
|
+
name: "NUsersList",
|
|
209
|
+
filePath: resolver.resolve("./runtime/components/NUsersList.vue")
|
|
189
210
|
});
|
|
190
211
|
addComponent({
|
|
191
|
-
name: "
|
|
192
|
-
filePath: resolver.resolve("./runtime/components/
|
|
212
|
+
name: "NUsersUserCard",
|
|
213
|
+
filePath: resolver.resolve("./runtime/components/NUsersUserCard.vue")
|
|
193
214
|
});
|
|
194
215
|
addComponent({
|
|
195
|
-
name: "
|
|
196
|
-
filePath: resolver.resolve("./runtime/components/
|
|
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/
|
|
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
|
|
10
|
-
redirectTo: { type: String, required: false
|
|
11
|
-
forgotPasswordEndpoint: { type: String, required: false
|
|
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(
|
|
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(
|
|
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>
|