nuxt-users 1.1.2 → 1.1.4
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/.vscode/settings.json +18 -0
- package/CHANGELOG.md +174 -0
- package/README.md +12 -2
- package/dist/cli.d.mts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.mjs +269 -0
- package/dist/module.d.mts +1 -51
- package/dist/module.d.ts +7 -0
- package/dist/module.json +2 -2
- package/dist/module.mjs +11 -81
- package/dist/runtime/assets/colors.css +1 -0
- package/dist/runtime/components/ForgotPasswordForm.vue +21 -23
- package/dist/runtime/components/LoginForm.vue +42 -30
- package/dist/runtime/components/LoginForm.vue.d.ts +8 -8
- package/dist/runtime/components/ResetPasswordForm.vue +40 -33
- package/dist/runtime/plugin.js +13 -3
- package/dist/runtime/server/api/login.post.js +1 -1
- package/dist/runtime/server/services/password.js +1 -2
- package/dist/shared/nuxt-users.Dtq4WcpY.d.mts +58 -0
- package/dist/shared/nuxt-users.Dtq4WcpY.d.ts +58 -0
- package/dist/shared/nuxt-users.eSQUrS6l.mjs +333 -0
- package/dist/utils.d.mts +37 -0
- package/dist/utils.d.ts +37 -0
- package/dist/utils.mjs +3 -0
- package/package.json +59 -27
- package/data/default.sqlite3 +0 -0
- package/dist/runtime/server/utils/create-migrations-table.d.ts +0 -2
- package/dist/runtime/server/utils/create-migrations-table.js +0 -35
- package/dist/runtime/server/utils/create-password-reset-tokens-table.d.ts +0 -2
- package/dist/runtime/server/utils/create-password-reset-tokens-table.js +0 -40
- package/dist/runtime/server/utils/create-personal-access-tokens-table.d.ts +0 -2
- package/dist/runtime/server/utils/create-personal-access-tokens-table.js +0 -56
- package/dist/runtime/server/utils/create-user.d.ts +0 -1
- package/dist/runtime/server/utils/create-user.js +0 -1
- package/dist/runtime/server/utils/create-users-table.d.ts +0 -2
- package/dist/runtime/server/utils/create-users-table.js +0 -44
- package/dist/runtime/server/utils/db.d.ts +0 -8
- package/dist/runtime/server/utils/db.js +0 -61
- package/dist/runtime/server/utils/migrate.d.ts +0 -4
- package/dist/runtime/server/utils/migrate.js +0 -63
- package/dist/runtime/server/utils/user.d.ts +0 -21
- package/dist/runtime/server/utils/user.js +0 -50
- package/src/cli/create-migrations-table.ts +0 -24
- package/src/cli/create-password-reset-tokens-table.ts +0 -24
- package/src/cli/create-personal-access-tokens-table.ts +0 -24
- package/src/cli/create-user.ts +0 -40
- package/src/cli/create-users-table.ts +0 -24
- package/src/cli/main.ts +0 -28
- package/src/cli/migrate.ts +0 -24
- package/src/cli/project-info.ts +0 -45
- package/src/cli/utils.ts +0 -70
package/dist/module.mjs
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
1
|
-
import { defineNuxtModule, createResolver,
|
|
1
|
+
import { defineNuxtModule, createResolver, addPlugin, addServerHandler, addComponent } from '@nuxt/kit';
|
|
2
2
|
import { defu } from 'defu';
|
|
3
|
-
import { hasAnyUsers, checkUsersTableExists, checkPersonalAccessTokensTableExists, checkPasswordResetTokensTableExists } from '../dist/runtime/server/utils/db.js';
|
|
4
|
-
import { getAppliedMigrations } from '../dist/runtime/server/utils/migrate.js';
|
|
5
|
-
import { promises } from 'node:fs';
|
|
6
|
-
import { dirname } from 'node:path';
|
|
7
3
|
|
|
8
4
|
const defaultOptions = {
|
|
9
5
|
connector: {
|
|
10
6
|
name: "sqlite",
|
|
11
7
|
options: {
|
|
12
|
-
path: "./data/
|
|
8
|
+
path: "./data/users.sqlite3"
|
|
13
9
|
}
|
|
14
10
|
},
|
|
15
11
|
tables: {
|
|
12
|
+
migrations: "migrations",
|
|
16
13
|
users: "users",
|
|
17
14
|
personalAccessTokens: "personal_access_tokens",
|
|
18
15
|
passwordResetTokens: "password_reset_tokens"
|
|
@@ -31,39 +28,6 @@ const defaultOptions = {
|
|
|
31
28
|
defaults: {
|
|
32
29
|
from: '"Nuxt Users Module" <noreply@example.com>'
|
|
33
30
|
}
|
|
34
|
-
},
|
|
35
|
-
passwordResetBaseUrl: "http://localhost:3000",
|
|
36
|
-
// Added
|
|
37
|
-
// Add option to disable database checks during setup
|
|
38
|
-
skipDatabaseChecks: false
|
|
39
|
-
};
|
|
40
|
-
const runWithTimeout = async (operation, timeoutMs = 5e3) => {
|
|
41
|
-
return Promise.race([
|
|
42
|
-
operation(),
|
|
43
|
-
new Promise((resolve) => {
|
|
44
|
-
setTimeout(() => {
|
|
45
|
-
console.warn("[Nuxt Users DB] \u26A0\uFE0F Database operation timed out after", timeoutMs, "ms");
|
|
46
|
-
resolve(null);
|
|
47
|
-
}, timeoutMs);
|
|
48
|
-
})
|
|
49
|
-
]);
|
|
50
|
-
};
|
|
51
|
-
const ensureSqliteDatabaseExists = async (options) => {
|
|
52
|
-
if (options.connector?.name === "sqlite" && options.connector.options.path) {
|
|
53
|
-
const dbPath = options.connector.options.path;
|
|
54
|
-
try {
|
|
55
|
-
await promises.access(dbPath);
|
|
56
|
-
console.log(`[Nuxt Users DB] \u2139\uFE0F SQLite database exists: ${dbPath}`);
|
|
57
|
-
} catch {
|
|
58
|
-
try {
|
|
59
|
-
const dbDir = dirname(dbPath);
|
|
60
|
-
await promises.mkdir(dbDir, { recursive: true });
|
|
61
|
-
console.log(`[Nuxt Users DB] \u2139\uFE0F Created SQLite database directory: ${dbDir}`);
|
|
62
|
-
console.log(`[Nuxt Users DB] \u2139\uFE0F SQLite database will be created at: ${dbPath}`);
|
|
63
|
-
} catch (error) {
|
|
64
|
-
console.warn(`[Nuxt Users DB] \u26A0\uFE0F Failed to create database directory: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
31
|
}
|
|
68
32
|
};
|
|
69
33
|
const module = defineNuxtModule({
|
|
@@ -75,61 +39,26 @@ const module = defineNuxtModule({
|
|
|
75
39
|
defaults: defaultOptions,
|
|
76
40
|
async setup(options, nuxt) {
|
|
77
41
|
const resolver = createResolver(import.meta.url);
|
|
78
|
-
await ensureSqliteDatabaseExists(options);
|
|
79
|
-
if (!options.skipDatabaseChecks) {
|
|
80
|
-
const appliedMigrations = await runWithTimeout(() => getAppliedMigrations(options)) || [];
|
|
81
|
-
const requiredMigrations = [
|
|
82
|
-
"create_migrations_table",
|
|
83
|
-
"create_users_table",
|
|
84
|
-
"create_personal_access_tokens_table",
|
|
85
|
-
"create_password_reset_tokens_table"
|
|
86
|
-
];
|
|
87
|
-
const missingMigrations = requiredMigrations.filter((migration) => !appliedMigrations.includes(migration));
|
|
88
|
-
if (missingMigrations.length > 0) {
|
|
89
|
-
console.warn(`[Nuxt Users DB] \u26A0\uFE0F Missing migrations: ${missingMigrations.join(", ")}`);
|
|
90
|
-
console.warn("[Nuxt Users DB] \u26A0\uFE0F Run migrations with: npx nuxt-users-migrate");
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
42
|
const runtimeConfigOptions = defu(options, nuxt.options.runtimeConfig.nuxtUsers || {}, defaultOptions);
|
|
94
43
|
nuxt.options.runtimeConfig.nuxtUsers = {
|
|
95
44
|
...runtimeConfigOptions,
|
|
96
45
|
tables: {
|
|
46
|
+
migrations: options.tables?.migrations || "migrations",
|
|
97
47
|
users: options.tables?.users || "users",
|
|
98
48
|
personalAccessTokens: options.tables?.personalAccessTokens || "personal_access_tokens",
|
|
99
49
|
passwordResetTokens: options.tables?.passwordResetTokens || "password_reset_tokens"
|
|
100
50
|
}
|
|
101
51
|
};
|
|
102
|
-
let hasUsersTable = false;
|
|
103
|
-
let hasPersonalAccessTokensTable = false;
|
|
104
|
-
let hasPasswordResetTokensTable = false;
|
|
105
|
-
if (!options.skipDatabaseChecks) {
|
|
106
|
-
hasUsersTable = await runWithTimeout(() => checkUsersTableExists(options)) || false;
|
|
107
|
-
if (!hasUsersTable) {
|
|
108
|
-
console.warn("[Nuxt Users DB] \u26A0\uFE0F Users table does not exist, you should run the migration script to create it by running: npx nuxt-users-migrate");
|
|
109
|
-
}
|
|
110
|
-
hasPersonalAccessTokensTable = await runWithTimeout(() => checkPersonalAccessTokensTableExists(options)) || false;
|
|
111
|
-
if (!hasPersonalAccessTokensTable) {
|
|
112
|
-
console.warn("[Nuxt Users DB] \u26A0\uFE0F Personal access tokens table does not exist, you should run the migration script to create it by running: npx nuxt-users-migrate");
|
|
113
|
-
}
|
|
114
|
-
hasPasswordResetTokensTable = await runWithTimeout(() => checkPasswordResetTokensTableExists(options)) || false;
|
|
115
|
-
if (!hasPasswordResetTokensTable) {
|
|
116
|
-
console.warn("[Nuxt Users DB] \u26A0\uFE0F Password reset tokens table does not exist, you should run the migration script to create it by running: npx nuxt-users-migrate");
|
|
117
|
-
}
|
|
118
|
-
const hasUsers = await runWithTimeout(() => hasAnyUsers(options)) || false;
|
|
119
|
-
if (!hasUsers) {
|
|
120
|
-
console.warn('[Nuxt Users DB] \u26A0\uFE0F No users found! Create a default user by running: npx nuxt-users-create-user rrd@example.com "John Doe" mypassword123');
|
|
121
|
-
}
|
|
122
|
-
} else {
|
|
123
|
-
console.log("[Nuxt Users DB] \u2139\uFE0F Skipping database checks during setup");
|
|
124
|
-
}
|
|
125
52
|
nuxt.options.runtimeConfig.public = nuxt.options.runtimeConfig.public || {};
|
|
126
53
|
nuxt.options.runtimeConfig.public.nuxtUsers = {
|
|
127
54
|
tables: {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
55
|
+
migrations: options.tables?.migrations,
|
|
56
|
+
users: options.tables?.users,
|
|
57
|
+
personalAccessTokens: options.tables?.personalAccessTokens,
|
|
58
|
+
passwordResetTokens: options.tables?.passwordResetTokens
|
|
131
59
|
}
|
|
132
60
|
};
|
|
61
|
+
addPlugin(resolver.resolve("./runtime/plugin"));
|
|
133
62
|
addServerHandler({
|
|
134
63
|
route: "/api/login",
|
|
135
64
|
method: "post",
|
|
@@ -145,7 +74,6 @@ const module = defineNuxtModule({
|
|
|
145
74
|
method: "post",
|
|
146
75
|
handler: resolver.resolve("./runtime/server/api/auth/reset-password.post")
|
|
147
76
|
});
|
|
148
|
-
addPlugin(resolver.resolve("./runtime/plugin"));
|
|
149
77
|
addComponent({
|
|
150
78
|
name: "LoginForm",
|
|
151
79
|
filePath: resolver.resolve("./runtime/components/LoginForm.vue")
|
|
@@ -158,6 +86,8 @@ const module = defineNuxtModule({
|
|
|
158
86
|
name: "ResetPasswordForm",
|
|
159
87
|
filePath: resolver.resolve("./runtime/components/ResetPasswordForm.vue")
|
|
160
88
|
});
|
|
89
|
+
nuxt.options.css = nuxt.options.css || [];
|
|
90
|
+
nuxt.options.css.push(resolver.resolve("./runtime/assets/colors.css"));
|
|
161
91
|
}
|
|
162
92
|
});
|
|
163
93
|
|
|
@@ -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-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-border:#e5e7eb;--color-border-light:#d1d5db;--color-border-dark:#ccc;--color-bg-primary:#fff;--color-bg-secondary:#f9fafb;--color-bg-tertiary:#f3f4f6}
|
|
@@ -6,7 +6,7 @@ const formData = reactive({
|
|
|
6
6
|
const message = ref("");
|
|
7
7
|
const loading = ref(false);
|
|
8
8
|
const isError = ref(false);
|
|
9
|
-
const handleForgotPassword = async (
|
|
9
|
+
const handleForgotPassword = async () => {
|
|
10
10
|
loading.value = true;
|
|
11
11
|
message.value = "";
|
|
12
12
|
isError.value = false;
|
|
@@ -17,8 +17,7 @@ const handleForgotPassword = async (data) => {
|
|
|
17
17
|
"Content-Type": "application/json",
|
|
18
18
|
"Accept": "application/json"
|
|
19
19
|
},
|
|
20
|
-
body: JSON.stringify({ email:
|
|
21
|
-
// Use email from form data
|
|
20
|
+
body: JSON.stringify({ email: formData.email })
|
|
22
21
|
});
|
|
23
22
|
const responseData = await response.json();
|
|
24
23
|
if (!response.ok) {
|
|
@@ -44,25 +43,24 @@ const handleForgotPassword = async (data) => {
|
|
|
44
43
|
<template>
|
|
45
44
|
<div>
|
|
46
45
|
<h2>Forgot Password</h2>
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
/>
|
|
61
|
-
<FormKit
|
|
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
|
|
62
59
|
type="submit"
|
|
63
|
-
:
|
|
64
|
-
|
|
65
|
-
|
|
60
|
+
:disabled="loading"
|
|
61
|
+
>
|
|
62
|
+
{{ loading ? "Sending..." : "Send Password Reset Link" }}
|
|
63
|
+
</button>
|
|
66
64
|
<p
|
|
67
65
|
v-if="message"
|
|
68
66
|
:class="{ error: isError, success: !isError }"
|
|
@@ -70,10 +68,10 @@ const handleForgotPassword = async (data) => {
|
|
|
70
68
|
>
|
|
71
69
|
{{ message }}
|
|
72
70
|
</p>
|
|
73
|
-
</
|
|
71
|
+
</form>
|
|
74
72
|
</div>
|
|
75
73
|
</template>
|
|
76
74
|
|
|
77
75
|
<style scoped>
|
|
78
|
-
.error{color:red}.success{color:green}.form-message{font-size:.9rem;margin-top:
|
|
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}
|
|
79
77
|
</style>
|
|
@@ -8,16 +8,21 @@ const props = defineProps({
|
|
|
8
8
|
const emit = defineEmits(["success", "error", "submit"]);
|
|
9
9
|
const isLoading = ref(false);
|
|
10
10
|
const error = ref("");
|
|
11
|
-
const
|
|
11
|
+
const formData = ref({
|
|
12
|
+
email: "",
|
|
13
|
+
password: "",
|
|
14
|
+
rememberMe: false
|
|
15
|
+
});
|
|
16
|
+
const handleSubmit = async () => {
|
|
12
17
|
isLoading.value = true;
|
|
13
18
|
error.value = "";
|
|
14
19
|
try {
|
|
15
|
-
emit("submit", formData);
|
|
20
|
+
emit("submit", formData.value);
|
|
16
21
|
const response = await $fetch(props.apiEndpoint, {
|
|
17
22
|
method: "POST",
|
|
18
23
|
body: {
|
|
19
|
-
email: formData.email,
|
|
20
|
-
password: formData.password
|
|
24
|
+
email: formData.value.email,
|
|
25
|
+
password: formData.value.password
|
|
21
26
|
}
|
|
22
27
|
});
|
|
23
28
|
emit("success", response.user);
|
|
@@ -36,11 +41,7 @@ const handleSubmit = async (formData) => {
|
|
|
36
41
|
|
|
37
42
|
<template>
|
|
38
43
|
<div class="login-form-container">
|
|
39
|
-
<
|
|
40
|
-
type="form"
|
|
41
|
-
:actions="false"
|
|
42
|
-
@submit="handleSubmit"
|
|
43
|
-
>
|
|
44
|
+
<form @submit.prevent="handleSubmit">
|
|
44
45
|
<!-- Header slot -->
|
|
45
46
|
<slot name="header">
|
|
46
47
|
<div class="login-header">
|
|
@@ -55,40 +56,51 @@ const handleSubmit = async (formData) => {
|
|
|
55
56
|
|
|
56
57
|
<!-- Email field -->
|
|
57
58
|
<slot name="email-field">
|
|
58
|
-
<
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
59
|
+
<div class="form-group">
|
|
60
|
+
<label for="email">Email</label>
|
|
61
|
+
<input
|
|
62
|
+
id="email"
|
|
63
|
+
v-model="formData.email"
|
|
64
|
+
type="email"
|
|
65
|
+
name="email"
|
|
66
|
+
placeholder="Enter your email"
|
|
67
|
+
required
|
|
68
|
+
>
|
|
69
|
+
</div>
|
|
65
70
|
</slot>
|
|
66
71
|
|
|
67
72
|
<!-- Password field -->
|
|
68
73
|
<slot name="password-field">
|
|
69
|
-
<
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
<div class="form-group">
|
|
75
|
+
<label for="password">Password</label>
|
|
76
|
+
<input
|
|
77
|
+
id="password"
|
|
78
|
+
v-model="formData.password"
|
|
79
|
+
type="password"
|
|
80
|
+
name="password"
|
|
81
|
+
placeholder="Enter your password"
|
|
82
|
+
required
|
|
83
|
+
minlength="6"
|
|
84
|
+
>
|
|
85
|
+
</div>
|
|
76
86
|
</slot>
|
|
77
87
|
|
|
78
88
|
<!-- Remember me slot -->
|
|
79
89
|
<slot name="remember-me">
|
|
80
90
|
<div class="remember-me">
|
|
81
|
-
<
|
|
91
|
+
<input
|
|
92
|
+
id="rememberMe"
|
|
93
|
+
v-model="formData.rememberMe"
|
|
82
94
|
type="checkbox"
|
|
83
95
|
name="rememberMe"
|
|
84
|
-
|
|
85
|
-
|
|
96
|
+
>
|
|
97
|
+
<label for="rememberMe">Remember me</label>
|
|
86
98
|
</div>
|
|
87
99
|
</slot>
|
|
88
100
|
|
|
89
101
|
<!-- Submit button slot -->
|
|
90
102
|
<slot name="submit-button">
|
|
91
|
-
<
|
|
103
|
+
<button
|
|
92
104
|
type="submit"
|
|
93
105
|
:disabled="isLoading"
|
|
94
106
|
>
|
|
@@ -97,7 +109,7 @@ const handleSubmit = async (formData) => {
|
|
|
97
109
|
class="loading-spinner"
|
|
98
110
|
/>
|
|
99
111
|
{{ isLoading ? "Signing in..." : "Sign In" }}
|
|
100
|
-
</
|
|
112
|
+
</button>
|
|
101
113
|
</slot>
|
|
102
114
|
|
|
103
115
|
<!-- Footer slot -->
|
|
@@ -124,10 +136,10 @@ const handleSubmit = async (formData) => {
|
|
|
124
136
|
{{ error }}
|
|
125
137
|
</div>
|
|
126
138
|
</slot>
|
|
127
|
-
</
|
|
139
|
+
</form>
|
|
128
140
|
</div>
|
|
129
141
|
</template>
|
|
130
142
|
|
|
131
143
|
<style scoped>
|
|
132
|
-
.login-form-container{background
|
|
144
|
+
.login-form-container{background:var(--color-bg-primary);border:1px solid var(--color-border);border-radius:12px;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:400px;padding:2em;width:100%}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{color:var(--color-primary);font-size:.875rem;text-decoration:none;transition:color .2s ease-in-out}.forgot-link:hover{color:var(--color-primary-dark);text-decoration:underline}.error-message{background-color:var(--color-error-light);border:1px solid var(--color-error-border);border-radius:8px;color:var(--color-error);font-size:.875rem;margin-top:1em;padding:.75em 1em;text-align:center}@media (max-width:480px){.login-form-container{margin:1em;padding:1.5em}.login-title{font-size:1.5rem}}
|
|
133
145
|
</style>
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
import type { LoginFormData, LoginFormProps, User } from '../../types.js';
|
|
2
|
-
declare var
|
|
2
|
+
declare var __VLS_1: {}, __VLS_3: {}, __VLS_5: {}, __VLS_7: {}, __VLS_9: {}, __VLS_11: {}, __VLS_13: {
|
|
3
3
|
error: string;
|
|
4
4
|
};
|
|
5
5
|
type __VLS_Slots = {} & {
|
|
6
|
-
header?: (props: typeof
|
|
6
|
+
header?: (props: typeof __VLS_1) => any;
|
|
7
7
|
} & {
|
|
8
|
-
'email-field'?: (props: typeof
|
|
8
|
+
'email-field'?: (props: typeof __VLS_3) => any;
|
|
9
9
|
} & {
|
|
10
|
-
'password-field'?: (props: typeof
|
|
10
|
+
'password-field'?: (props: typeof __VLS_5) => any;
|
|
11
11
|
} & {
|
|
12
|
-
'remember-me'?: (props: typeof
|
|
12
|
+
'remember-me'?: (props: typeof __VLS_7) => any;
|
|
13
13
|
} & {
|
|
14
|
-
'submit-button'?: (props: typeof
|
|
14
|
+
'submit-button'?: (props: typeof __VLS_9) => any;
|
|
15
15
|
} & {
|
|
16
|
-
footer?: (props: typeof
|
|
16
|
+
footer?: (props: typeof __VLS_11) => any;
|
|
17
17
|
} & {
|
|
18
|
-
'error-message'?: (props: typeof
|
|
18
|
+
'error-message'?: (props: typeof __VLS_13) => any;
|
|
19
19
|
};
|
|
20
20
|
declare const __VLS_component: import("vue").DefineComponent<LoginFormProps, void, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
21
21
|
error: (error: string) => any;
|
|
@@ -27,12 +27,17 @@ onMounted(() => {
|
|
|
27
27
|
}
|
|
28
28
|
});
|
|
29
29
|
const canSubmit = computed(() => !!tokenFromUrl.value && !!emailFromUrl.value && !formInvalidDueToToken.value);
|
|
30
|
-
const handleResetPassword = async (
|
|
30
|
+
const handleResetPassword = async () => {
|
|
31
31
|
if (!canSubmit.value) {
|
|
32
32
|
message.value = "Token or email is missing or invalid. Cannot reset password.";
|
|
33
33
|
isError.value = true;
|
|
34
34
|
return;
|
|
35
35
|
}
|
|
36
|
+
if (formData.password !== formData.password_confirm) {
|
|
37
|
+
message.value = "Passwords do not match.";
|
|
38
|
+
isError.value = true;
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
36
41
|
loading.value = true;
|
|
37
42
|
message.value = "";
|
|
38
43
|
isError.value = false;
|
|
@@ -46,10 +51,8 @@ const handleResetPassword = async (data) => {
|
|
|
46
51
|
body: JSON.stringify({
|
|
47
52
|
token: tokenFromUrl.value,
|
|
48
53
|
email: emailFromUrl.value,
|
|
49
|
-
password:
|
|
50
|
-
|
|
51
|
-
password_confirmation: data.password_confirm
|
|
52
|
-
// Use confirmation from form data
|
|
54
|
+
password: formData.password,
|
|
55
|
+
password_confirmation: formData.password_confirm
|
|
53
56
|
})
|
|
54
57
|
});
|
|
55
58
|
const responseData = await response.json();
|
|
@@ -80,33 +83,37 @@ const handleResetPassword = async (data) => {
|
|
|
80
83
|
<template>
|
|
81
84
|
<div>
|
|
82
85
|
<h2>Reset Password</h2>
|
|
83
|
-
<
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
86
|
+
<form @submit.prevent="handleResetPassword">
|
|
87
|
+
<div class="form-group">
|
|
88
|
+
<label for="password">New Password</label>
|
|
89
|
+
<input
|
|
90
|
+
id="password"
|
|
91
|
+
v-model="formData.password"
|
|
92
|
+
type="password"
|
|
93
|
+
name="password"
|
|
94
|
+
placeholder="Enter new password"
|
|
95
|
+
required
|
|
96
|
+
minlength="8"
|
|
97
|
+
>
|
|
98
|
+
</div>
|
|
99
|
+
<div class="form-group">
|
|
100
|
+
<label for="password_confirm">Confirm New Password</label>
|
|
101
|
+
<input
|
|
102
|
+
id="password_confirm"
|
|
103
|
+
v-model="formData.password_confirm"
|
|
104
|
+
type="password"
|
|
105
|
+
name="password_confirm"
|
|
106
|
+
placeholder="Confirm new password"
|
|
107
|
+
required
|
|
108
|
+
minlength="8"
|
|
109
|
+
>
|
|
110
|
+
</div>
|
|
111
|
+
<button
|
|
106
112
|
type="submit"
|
|
107
|
-
:
|
|
108
|
-
|
|
109
|
-
|
|
113
|
+
:disabled="loading || formInvalidDueToToken"
|
|
114
|
+
>
|
|
115
|
+
{{ loading ? "Resetting..." : "Reset Password" }}
|
|
116
|
+
</button>
|
|
110
117
|
<p
|
|
111
118
|
v-if="message"
|
|
112
119
|
:class="{ error: isError, success: !isError }"
|
|
@@ -114,10 +121,10 @@ const handleResetPassword = async (data) => {
|
|
|
114
121
|
>
|
|
115
122
|
{{ message }}
|
|
116
123
|
</p>
|
|
117
|
-
</
|
|
124
|
+
</form>
|
|
118
125
|
</div>
|
|
119
126
|
</template>
|
|
120
127
|
|
|
121
128
|
<style scoped>
|
|
122
|
-
.error{color:red}.success{color:green}.form-message{font-size:.9rem;margin-top:
|
|
129
|
+
.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%}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}
|
|
123
130
|
</style>
|
package/dist/runtime/plugin.js
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
|
-
import { defineNuxtPlugin } from "#app";
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { defineNuxtPlugin, useRuntimeConfig } from "#app";
|
|
2
|
+
import { checkTableExists } from "../utils";
|
|
3
|
+
export default defineNuxtPlugin(async (_nuxtApp) => {
|
|
4
|
+
if (import.meta.client) {
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
const { nuxtUsers } = useRuntimeConfig();
|
|
8
|
+
const options = nuxtUsers;
|
|
9
|
+
const hasMigrationsTable = await checkTableExists(options, options.tables.migrations);
|
|
10
|
+
if (!hasMigrationsTable) {
|
|
11
|
+
useRuntimeConfig().public.nuxtUsers.tables.migrations = "";
|
|
12
|
+
console.warn("[Nuxt Users] \u26A0\uFE0F Migrations table does not exist, you should run the migration script to create it by running: npx nuxt-users migrate");
|
|
13
|
+
}
|
|
4
14
|
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createError, defineEventHandler, readBody, setCookie } from "h3";
|
|
2
|
-
import { useDb } from "../utils/db.js";
|
|
3
2
|
import bcrypt from "bcrypt";
|
|
4
3
|
import crypto from "node:crypto";
|
|
5
4
|
import { useRuntimeConfig } from "#imports";
|
|
5
|
+
import { useDb } from "../../../utils";
|
|
6
6
|
export default defineEventHandler(async (event) => {
|
|
7
7
|
const body = await readBody(event);
|
|
8
8
|
const { email, password } = body;
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { createTransport } from "nodemailer";
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
3
|
import bcrypt from "bcrypt";
|
|
4
|
-
import { findUserByEmail, updateUserPassword } from "
|
|
5
|
-
import { useDb } from "../utils/db.js";
|
|
4
|
+
import { findUserByEmail, updateUserPassword, useDb } from "../../../utils";
|
|
6
5
|
const TOKEN_EXPIRATION_HOURS = 1;
|
|
7
6
|
export const sendPasswordResetLink = async (email, options) => {
|
|
8
7
|
const user = await findUserByEmail(email, options);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
type DatabaseType = 'sqlite' | 'mysql' | 'postgresql';
|
|
2
|
+
type DatabaseConfig = {
|
|
3
|
+
path?: string;
|
|
4
|
+
host?: string;
|
|
5
|
+
port?: number;
|
|
6
|
+
user?: string;
|
|
7
|
+
password?: string;
|
|
8
|
+
database?: string;
|
|
9
|
+
};
|
|
10
|
+
interface ModuleOptions {
|
|
11
|
+
connector?: {
|
|
12
|
+
name: DatabaseType;
|
|
13
|
+
options: DatabaseConfig;
|
|
14
|
+
};
|
|
15
|
+
tables: {
|
|
16
|
+
migrations: string;
|
|
17
|
+
users: string;
|
|
18
|
+
personalAccessTokens: string;
|
|
19
|
+
passwordResetTokens: string;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Mailer configuration options for sending emails (e.g., password resets)
|
|
23
|
+
* Uses nodemailer
|
|
24
|
+
*/
|
|
25
|
+
mailer?: MailerOptions;
|
|
26
|
+
/**
|
|
27
|
+
* Base URL for password reset links
|
|
28
|
+
* @default 'http://localhost:3000' // Example, will be set in module defaults
|
|
29
|
+
*/
|
|
30
|
+
passwordResetBaseUrl?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Skip database checks during module setup to prevent hanging
|
|
33
|
+
* @default false
|
|
34
|
+
*/
|
|
35
|
+
skipDatabaseChecks?: boolean;
|
|
36
|
+
}
|
|
37
|
+
interface MailerOptions {
|
|
38
|
+
host: string;
|
|
39
|
+
port: number;
|
|
40
|
+
secure?: boolean;
|
|
41
|
+
auth: {
|
|
42
|
+
user: string;
|
|
43
|
+
pass: string;
|
|
44
|
+
};
|
|
45
|
+
defaults?: {
|
|
46
|
+
from: string;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
interface User {
|
|
50
|
+
id: number;
|
|
51
|
+
email: string;
|
|
52
|
+
name: string;
|
|
53
|
+
password: string;
|
|
54
|
+
created_at: string;
|
|
55
|
+
updated_at: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type { ModuleOptions as M, User as U };
|