nuxt-users 1.1.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/README.md +69 -0
- package/dist/module.d.mts +49 -0
- package/dist/module.json +9 -0
- package/dist/module.mjs +122 -0
- package/dist/runtime/components/ForgotPasswordForm.vue +79 -0
- package/dist/runtime/components/ForgotPasswordForm.vue.d.ts +2 -0
- package/dist/runtime/components/LoginForm.vue +133 -0
- package/dist/runtime/components/LoginForm.vue.d.ts +35 -0
- package/dist/runtime/components/ResetPasswordForm.vue +123 -0
- package/dist/runtime/components/ResetPasswordForm.vue.d.ts +2 -0
- package/dist/runtime/plugin.d.ts +2 -0
- package/dist/runtime/plugin.js +4 -0
- package/dist/runtime/server/api/auth/forgot-password.post.d.ts +4 -0
- package/dist/runtime/server/api/auth/forgot-password.post.js +26 -0
- package/dist/runtime/server/api/auth/reset-password.post.d.ts +4 -0
- package/dist/runtime/server/api/auth/reset-password.post.js +48 -0
- package/dist/runtime/server/api/login.post.d.ts +10 -0
- package/dist/runtime/server/api/login.post.js +57 -0
- package/dist/runtime/server/services/password.d.ts +14 -0
- package/dist/runtime/server/services/password.js +113 -0
- package/dist/runtime/server/tsconfig.json +3 -0
- package/dist/runtime/server/utils/create-migrations-table.d.ts +2 -0
- package/dist/runtime/server/utils/create-migrations-table.js +37 -0
- package/dist/runtime/server/utils/create-password-reset-tokens-table.d.ts +2 -0
- package/dist/runtime/server/utils/create-password-reset-tokens-table.js +42 -0
- package/dist/runtime/server/utils/create-personal-access-tokens-table.d.ts +2 -0
- package/dist/runtime/server/utils/create-personal-access-tokens-table.js +58 -0
- package/dist/runtime/server/utils/create-user.d.ts +1 -0
- package/dist/runtime/server/utils/create-user.js +1 -0
- package/dist/runtime/server/utils/create-users-table.d.ts +2 -0
- package/dist/runtime/server/utils/create-users-table.js +46 -0
- package/dist/runtime/server/utils/db.d.ts +7 -0
- package/dist/runtime/server/utils/db.js +45 -0
- package/dist/runtime/server/utils/migrate.d.ts +4 -0
- package/dist/runtime/server/utils/migrate.js +68 -0
- package/dist/runtime/server/utils/user.d.ts +21 -0
- package/dist/runtime/server/utils/user.js +57 -0
- package/dist/types.d.mts +7 -0
- package/package.json +82 -0
package/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Nuxt Users
|
|
2
|
+
|
|
3
|
+
A user authentication module for Nuxt 3 with database support for SQLite and MySQL.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🔐 User authentication with bcrypt password hashing
|
|
8
|
+
- 🗄️ Database support (SQLite, MySQL)
|
|
9
|
+
- 🛠️ CLI commands for database management
|
|
10
|
+
- 🔑 Password Reset Functionality
|
|
11
|
+
- 📦 Zero-config setup with sensible defaults
|
|
12
|
+
- 🔧 TypeScript support
|
|
13
|
+
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# Install the module
|
|
18
|
+
npm install nuxt-users
|
|
19
|
+
|
|
20
|
+
# Add to your nuxt.config.ts
|
|
21
|
+
export default defineNuxtConfig({
|
|
22
|
+
modules: ['nuxt-users'],
|
|
23
|
+
nuxtUsers: {
|
|
24
|
+
connector: {
|
|
25
|
+
name: 'sqlite',
|
|
26
|
+
options: {
|
|
27
|
+
path: './data/default.sqlite3'
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
# Run migrations
|
|
34
|
+
yarn db:migrate
|
|
35
|
+
|
|
36
|
+
# Create your first user
|
|
37
|
+
yarn db:create-user user@example.com "John Doe" password123
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Documentation
|
|
41
|
+
|
|
42
|
+
📚 **Full documentation is available at: [docs/](/docs/)**
|
|
43
|
+
|
|
44
|
+
- [Installation Guide](/docs/guide/installation)
|
|
45
|
+
- [Configuration Options](/docs/guide/configuration)
|
|
46
|
+
- [API Reference](/docs/api/)
|
|
47
|
+
- [Component Documentation](/docs/components/)
|
|
48
|
+
- [Database Schema](/docs/database/schema)
|
|
49
|
+
- [Contributing Guidelines](/docs/contributing/)
|
|
50
|
+
|
|
51
|
+
## Development
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# Install dependencies
|
|
55
|
+
yarn install
|
|
56
|
+
|
|
57
|
+
# Start development server
|
|
58
|
+
yarn dev
|
|
59
|
+
|
|
60
|
+
# Run tests
|
|
61
|
+
yarn test
|
|
62
|
+
|
|
63
|
+
# Build documentation
|
|
64
|
+
cd docs && yarn dev
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## License
|
|
68
|
+
|
|
69
|
+
MIT
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
|
+
|
|
3
|
+
type DatabaseType = 'sqlite' | 'mysql' | 'postgresql';
|
|
4
|
+
type DatabaseConfig = {
|
|
5
|
+
path?: string;
|
|
6
|
+
host?: string;
|
|
7
|
+
port?: number;
|
|
8
|
+
user?: string;
|
|
9
|
+
password?: string;
|
|
10
|
+
database?: string;
|
|
11
|
+
};
|
|
12
|
+
interface ModuleOptions {
|
|
13
|
+
connector?: {
|
|
14
|
+
name: DatabaseType;
|
|
15
|
+
options: DatabaseConfig;
|
|
16
|
+
};
|
|
17
|
+
tables: {
|
|
18
|
+
users: string;
|
|
19
|
+
personalAccessTokens: string;
|
|
20
|
+
passwordResetTokens: string;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Mailer configuration options for sending emails (e.g., password resets)
|
|
24
|
+
* Uses nodemailer
|
|
25
|
+
*/
|
|
26
|
+
mailer?: MailerOptions;
|
|
27
|
+
/**
|
|
28
|
+
* Base URL for password reset links
|
|
29
|
+
* @default 'http://localhost:3000' // Example, will be set in module defaults
|
|
30
|
+
*/
|
|
31
|
+
passwordResetBaseUrl?: string;
|
|
32
|
+
}
|
|
33
|
+
interface MailerOptions {
|
|
34
|
+
host: string;
|
|
35
|
+
port: number;
|
|
36
|
+
secure?: boolean;
|
|
37
|
+
auth: {
|
|
38
|
+
user: string;
|
|
39
|
+
pass: string;
|
|
40
|
+
};
|
|
41
|
+
defaults?: {
|
|
42
|
+
from: string;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
declare const defaultOptions: ModuleOptions;
|
|
47
|
+
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
48
|
+
|
|
49
|
+
export { _default as default, defaultOptions };
|
package/dist/module.json
ADDED
package/dist/module.mjs
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { defineNuxtModule, createResolver, addServerHandler, addPlugin, addComponent } from '@nuxt/kit';
|
|
2
|
+
import { defu } from 'defu';
|
|
3
|
+
import { checkUsersTableExists, checkPersonalAccessTokensTableExists, checkPasswordResetTokensTableExists, hasAnyUsers } from '../dist/runtime/server/utils/db.js';
|
|
4
|
+
import { getAppliedMigrations } from '../dist/runtime/server/utils/migrate.js';
|
|
5
|
+
|
|
6
|
+
const defaultOptions = {
|
|
7
|
+
connector: {
|
|
8
|
+
name: "sqlite",
|
|
9
|
+
options: {
|
|
10
|
+
path: "./data/default.sqlite3"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
tables: {
|
|
14
|
+
users: "users",
|
|
15
|
+
personalAccessTokens: "personal_access_tokens",
|
|
16
|
+
passwordResetTokens: "password_reset_tokens"
|
|
17
|
+
},
|
|
18
|
+
mailer: {
|
|
19
|
+
// Added default mailer (example using ethereal.email)
|
|
20
|
+
host: "smtp.ethereal.email",
|
|
21
|
+
port: 587,
|
|
22
|
+
secure: false,
|
|
23
|
+
auth: {
|
|
24
|
+
user: "user@ethereal.email",
|
|
25
|
+
// Replace with actual Ethereal user
|
|
26
|
+
pass: "password"
|
|
27
|
+
// Replace with actual Ethereal password
|
|
28
|
+
},
|
|
29
|
+
defaults: {
|
|
30
|
+
from: '"Nuxt Users Module" <noreply@example.com>'
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
passwordResetBaseUrl: "http://localhost:3000"
|
|
34
|
+
// Added
|
|
35
|
+
};
|
|
36
|
+
const module = defineNuxtModule({
|
|
37
|
+
meta: {
|
|
38
|
+
name: "nuxt-users",
|
|
39
|
+
configKey: "nuxtUsers"
|
|
40
|
+
},
|
|
41
|
+
// Default configuration options of the Nuxt module
|
|
42
|
+
defaults: defaultOptions,
|
|
43
|
+
async setup(options, nuxt) {
|
|
44
|
+
const resolver = createResolver(import.meta.url);
|
|
45
|
+
const appliedMigrations = await getAppliedMigrations(options);
|
|
46
|
+
const requiredMigrations = [
|
|
47
|
+
"create_migrations_table",
|
|
48
|
+
"create_users_table",
|
|
49
|
+
"create_personal_access_tokens_table",
|
|
50
|
+
"create_password_reset_tokens_table"
|
|
51
|
+
];
|
|
52
|
+
const missingMigrations = requiredMigrations.filter((migration) => !appliedMigrations.includes(migration));
|
|
53
|
+
if (missingMigrations.length > 0) {
|
|
54
|
+
console.warn(`[Nuxt Users DB] \u26A0\uFE0F Missing migrations: ${missingMigrations.join(", ")}`);
|
|
55
|
+
console.warn("[Nuxt Users DB] \u26A0\uFE0F Run migrations with: yarn db:migrate");
|
|
56
|
+
}
|
|
57
|
+
const hasUsersTable = await checkUsersTableExists(options);
|
|
58
|
+
if (!hasUsersTable) {
|
|
59
|
+
console.warn("[Nuxt Users DB] \u26A0\uFE0F Users table does not exist, you should run the migration script to create it by running: yarn db:migrate");
|
|
60
|
+
}
|
|
61
|
+
const hasPersonalAccessTokensTable = await checkPersonalAccessTokensTableExists(options);
|
|
62
|
+
if (!hasPersonalAccessTokensTable) {
|
|
63
|
+
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: yarn db:migrate");
|
|
64
|
+
}
|
|
65
|
+
const hasPasswordResetTokensTable = await checkPasswordResetTokensTableExists(options);
|
|
66
|
+
if (!hasPasswordResetTokensTable) {
|
|
67
|
+
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: yarn db:migrate");
|
|
68
|
+
}
|
|
69
|
+
const runtimeConfigOptions = defu(options, nuxt.options.runtimeConfig.nuxtUsers || {}, defaultOptions);
|
|
70
|
+
nuxt.options.runtimeConfig.nuxtUsers = {
|
|
71
|
+
...runtimeConfigOptions,
|
|
72
|
+
tables: {
|
|
73
|
+
users: options.tables?.users || "users",
|
|
74
|
+
personalAccessTokens: options.tables?.personalAccessTokens || "personal_access_tokens",
|
|
75
|
+
passwordResetTokens: options.tables?.passwordResetTokens || "password_reset_tokens"
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
nuxt.options.runtimeConfig.public = nuxt.options.runtimeConfig.public || {};
|
|
79
|
+
nuxt.options.runtimeConfig.public.nuxtUsers = {
|
|
80
|
+
tables: {
|
|
81
|
+
users: hasUsersTable,
|
|
82
|
+
personalAccessTokens: hasPersonalAccessTokensTable,
|
|
83
|
+
passwordResetTokens: hasPasswordResetTokensTable
|
|
84
|
+
// Use stored value
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
const hasUsers = await hasAnyUsers(options);
|
|
88
|
+
if (!hasUsers) {
|
|
89
|
+
console.warn('[Nuxt Users DB] \u26A0\uFE0F No users found! Create a default user by running: yarn db:create-user rrd@example.com "John Doe" mypassword123');
|
|
90
|
+
}
|
|
91
|
+
addServerHandler({
|
|
92
|
+
route: "/api/login",
|
|
93
|
+
method: "post",
|
|
94
|
+
handler: resolver.resolve("./runtime/server/api/login.post")
|
|
95
|
+
});
|
|
96
|
+
addServerHandler({
|
|
97
|
+
route: "/api/forgot-password",
|
|
98
|
+
method: "post",
|
|
99
|
+
handler: resolver.resolve("./runtime/server/api/auth/forgot-password.post")
|
|
100
|
+
});
|
|
101
|
+
addServerHandler({
|
|
102
|
+
route: "/api/reset-password",
|
|
103
|
+
method: "post",
|
|
104
|
+
handler: resolver.resolve("./runtime/server/api/auth/reset-password.post")
|
|
105
|
+
});
|
|
106
|
+
addPlugin(resolver.resolve("./runtime/plugin"));
|
|
107
|
+
addComponent({
|
|
108
|
+
name: "LoginForm",
|
|
109
|
+
filePath: resolver.resolve("./runtime/components/LoginForm.vue")
|
|
110
|
+
});
|
|
111
|
+
addComponent({
|
|
112
|
+
name: "ForgotPasswordForm",
|
|
113
|
+
filePath: resolver.resolve("./runtime/components/ForgotPasswordForm.vue")
|
|
114
|
+
});
|
|
115
|
+
addComponent({
|
|
116
|
+
name: "ResetPasswordForm",
|
|
117
|
+
filePath: resolver.resolve("./runtime/components/ResetPasswordForm.vue")
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
export { module as default, defaultOptions };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, reactive } from "vue";
|
|
3
|
+
const formData = reactive({
|
|
4
|
+
email: ""
|
|
5
|
+
});
|
|
6
|
+
const message = ref("");
|
|
7
|
+
const loading = ref(false);
|
|
8
|
+
const isError = ref(false);
|
|
9
|
+
const handleForgotPassword = async (data) => {
|
|
10
|
+
loading.value = true;
|
|
11
|
+
message.value = "";
|
|
12
|
+
isError.value = false;
|
|
13
|
+
try {
|
|
14
|
+
const response = await fetch("/api/forgot-password", {
|
|
15
|
+
method: "POST",
|
|
16
|
+
headers: {
|
|
17
|
+
"Content-Type": "application/json",
|
|
18
|
+
"Accept": "application/json"
|
|
19
|
+
},
|
|
20
|
+
body: JSON.stringify({ email: data.email })
|
|
21
|
+
// Use email from form data
|
|
22
|
+
});
|
|
23
|
+
const responseData = await response.json();
|
|
24
|
+
if (!response.ok) {
|
|
25
|
+
throw new Error(responseData.message || responseData.statusMessage || "An error occurred.");
|
|
26
|
+
}
|
|
27
|
+
message.value = responseData.message;
|
|
28
|
+
isError.value = false;
|
|
29
|
+
formData.email = "";
|
|
30
|
+
} catch (err) {
|
|
31
|
+
if (err instanceof Error) {
|
|
32
|
+
message.value = err.message;
|
|
33
|
+
} else {
|
|
34
|
+
message.value = "Failed to send password reset link.";
|
|
35
|
+
}
|
|
36
|
+
console.error(err);
|
|
37
|
+
isError.value = true;
|
|
38
|
+
} finally {
|
|
39
|
+
loading.value = false;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<template>
|
|
45
|
+
<div>
|
|
46
|
+
<h2>Forgot Password</h2>
|
|
47
|
+
<FormKit
|
|
48
|
+
v-slot="{ disabled }"
|
|
49
|
+
type="form"
|
|
50
|
+
:actions="false"
|
|
51
|
+
@submit="handleForgotPassword"
|
|
52
|
+
>
|
|
53
|
+
<FormKit
|
|
54
|
+
v-model="formData.email"
|
|
55
|
+
type="email"
|
|
56
|
+
name="email"
|
|
57
|
+
label="Email"
|
|
58
|
+
placeholder="your@email.com"
|
|
59
|
+
validation="required|email"
|
|
60
|
+
/>
|
|
61
|
+
<FormKit
|
|
62
|
+
type="submit"
|
|
63
|
+
:label="loading ? 'Sending...' : 'Send Password Reset Link'"
|
|
64
|
+
:disabled="Boolean(disabled) || loading"
|
|
65
|
+
/>
|
|
66
|
+
<p
|
|
67
|
+
v-if="message"
|
|
68
|
+
:class="{ error: isError, success: !isError }"
|
|
69
|
+
class="form-message"
|
|
70
|
+
>
|
|
71
|
+
{{ message }}
|
|
72
|
+
</p>
|
|
73
|
+
</FormKit>
|
|
74
|
+
</div>
|
|
75
|
+
</template>
|
|
76
|
+
|
|
77
|
+
<style scoped>
|
|
78
|
+
.error{color:red}.success{color:green}.form-message{font-size:.9rem;margin-top:1rem}:deep(.formkit-outer){margin-bottom:1rem}:deep(.formkit-label){display:block;font-weight:700;margin-bottom:.25rem}:deep(.formkit-input input[type=email]),:deep(.formkit-input input[type=password]){border:1px solid #ccc;border-radius:4px;box-sizing:border-box;padding:.75rem;width:100%}:deep(.formkit-input input[type=submit]){background-color:#333;border:none;border-radius:4px;color:#fff;cursor:pointer;padding:.75rem 1.5rem}:deep(.formkit-input input[type=submit]:disabled){background-color:#aaa;cursor:not-allowed}:deep(.formkit-messages){list-style:none;margin-top:.25rem;padding:0}:deep(.formkit-message){color:red;font-size:.8rem}
|
|
79
|
+
</style>
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
declare const _default: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
export default _default;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref } from "vue";
|
|
3
|
+
import { navigateTo } from "#app";
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
apiEndpoint: { type: String, required: false, default: "/api/login" },
|
|
6
|
+
redirectTo: { type: String, required: false, default: "/" }
|
|
7
|
+
});
|
|
8
|
+
const emit = defineEmits(["success", "error", "submit"]);
|
|
9
|
+
const isLoading = ref(false);
|
|
10
|
+
const error = ref("");
|
|
11
|
+
const handleSubmit = async (formData) => {
|
|
12
|
+
isLoading.value = true;
|
|
13
|
+
error.value = "";
|
|
14
|
+
try {
|
|
15
|
+
emit("submit", formData);
|
|
16
|
+
const response = await $fetch(props.apiEndpoint, {
|
|
17
|
+
method: "POST",
|
|
18
|
+
body: {
|
|
19
|
+
email: formData.email,
|
|
20
|
+
password: formData.password
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
emit("success", response.user);
|
|
24
|
+
if (props.redirectTo) {
|
|
25
|
+
await navigateTo(props.redirectTo);
|
|
26
|
+
}
|
|
27
|
+
} catch (err) {
|
|
28
|
+
const errorMessage = err && typeof err === "object" && "data" in err && err.data && typeof err.data === "object" && "statusMessage" in err.data ? String(err.data.statusMessage) : err instanceof Error ? err.message : "Login failed";
|
|
29
|
+
error.value = errorMessage;
|
|
30
|
+
emit("error", errorMessage);
|
|
31
|
+
} finally {
|
|
32
|
+
isLoading.value = false;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<template>
|
|
38
|
+
<div class="login-form-container">
|
|
39
|
+
<FormKit
|
|
40
|
+
type="form"
|
|
41
|
+
:actions="false"
|
|
42
|
+
@submit="handleSubmit"
|
|
43
|
+
>
|
|
44
|
+
<!-- Header slot -->
|
|
45
|
+
<slot name="header">
|
|
46
|
+
<div class="login-header">
|
|
47
|
+
<h2 class="login-title">
|
|
48
|
+
Welcome Back
|
|
49
|
+
</h2>
|
|
50
|
+
<p class="login-subtitle">
|
|
51
|
+
Sign in to your account
|
|
52
|
+
</p>
|
|
53
|
+
</div>
|
|
54
|
+
</slot>
|
|
55
|
+
|
|
56
|
+
<!-- Email field -->
|
|
57
|
+
<slot name="email-field">
|
|
58
|
+
<FormKit
|
|
59
|
+
type="email"
|
|
60
|
+
name="email"
|
|
61
|
+
label="Email"
|
|
62
|
+
placeholder="Enter your email"
|
|
63
|
+
validation="required|email"
|
|
64
|
+
/>
|
|
65
|
+
</slot>
|
|
66
|
+
|
|
67
|
+
<!-- Password field -->
|
|
68
|
+
<slot name="password-field">
|
|
69
|
+
<FormKit
|
|
70
|
+
type="password"
|
|
71
|
+
name="password"
|
|
72
|
+
label="Password"
|
|
73
|
+
placeholder="Enter your password"
|
|
74
|
+
validation="required|length:6"
|
|
75
|
+
/>
|
|
76
|
+
</slot>
|
|
77
|
+
|
|
78
|
+
<!-- Remember me slot -->
|
|
79
|
+
<slot name="remember-me">
|
|
80
|
+
<div class="remember-me">
|
|
81
|
+
<FormKit
|
|
82
|
+
type="checkbox"
|
|
83
|
+
name="rememberMe"
|
|
84
|
+
label="Remember me"
|
|
85
|
+
/>
|
|
86
|
+
</div>
|
|
87
|
+
</slot>
|
|
88
|
+
|
|
89
|
+
<!-- Submit button slot -->
|
|
90
|
+
<slot name="submit-button">
|
|
91
|
+
<FormKit
|
|
92
|
+
type="submit"
|
|
93
|
+
:disabled="isLoading"
|
|
94
|
+
>
|
|
95
|
+
<span
|
|
96
|
+
v-if="isLoading"
|
|
97
|
+
class="loading-spinner"
|
|
98
|
+
/>
|
|
99
|
+
{{ isLoading ? "Signing in..." : "Sign In" }}
|
|
100
|
+
</FormKit>
|
|
101
|
+
</slot>
|
|
102
|
+
|
|
103
|
+
<!-- Footer slot -->
|
|
104
|
+
<slot name="footer">
|
|
105
|
+
<div class="login-footer">
|
|
106
|
+
<p class="forgot-password">
|
|
107
|
+
<a
|
|
108
|
+
href="#"
|
|
109
|
+
class="forgot-link"
|
|
110
|
+
>Forgot your password?</a>
|
|
111
|
+
</p>
|
|
112
|
+
</div>
|
|
113
|
+
</slot>
|
|
114
|
+
|
|
115
|
+
<!-- Error message slot -->
|
|
116
|
+
<slot
|
|
117
|
+
name="error-message"
|
|
118
|
+
:error="error"
|
|
119
|
+
>
|
|
120
|
+
<div
|
|
121
|
+
v-if="error"
|
|
122
|
+
class="error-message"
|
|
123
|
+
>
|
|
124
|
+
{{ error }}
|
|
125
|
+
</div>
|
|
126
|
+
</slot>
|
|
127
|
+
</FormKit>
|
|
128
|
+
</div>
|
|
129
|
+
</template>
|
|
130
|
+
|
|
131
|
+
<style scoped>
|
|
132
|
+
.login-form-container{background:#fff;border:1px solid #e5e7eb;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:2rem;width:100%}:deep(.formkit-form){display:flex;flex-direction:column;gap:1.5rem}.login-header{margin-bottom:1rem;text-align:center}.login-title{color:#111827;font-size:1.875rem;font-weight:700;line-height:1.2;margin:0 0 .5rem}.login-subtitle{color:#6b7280;font-size:.875rem;line-height:1.5;margin:0}:deep(.formkit-inner){--fk-border-radius:8px;--fk-border-color:#d1d5db;--fk-border-color-focus:#3b82f6;--fk-bg-color:#f9fafb;--fk-bg-color-focus:#fff;--fk-text-color:#374151;--fk-text-color-placeholder:#9ca3af;--fk-font-size:0.875rem;--fk-padding:0.75rem 1rem;--fk-transition:all 0.2s ease-in-out}:deep(.formkit-inner .formkit-input){background-color:var(--fk-bg-color);border:1px solid var(--fk-border-color);border-radius:var(--fk-border-radius);box-sizing:border-box;color:var(--fk-text-color);font-size:var(--fk-font-size);padding:var(--fk-padding);transition:var(--fk-transition);width:100%}:deep(.formkit-inner .formkit-input:focus){background-color:var(--fk-bg-color-focus);border-color:var(--fk-border-color-focus);box-shadow:0 0 0 3px rgba(59,130,246,.1);outline:none}:deep(.formkit-inner .formkit-label){color:#374151;display:block;font-size:.875rem;font-weight:500;margin-bottom:.5rem}:deep(.formkit-messages){font-size:.75rem;list-style:none;padding:0}:deep(.formkit-message){margin-top:.25rem}:deep(.formkit-message[data-message-type=validation]){color:#db5050}:deep(.formkit-message[data-message-type=error]){color:#dc2626}:deep(.formkit-wrapper:has(input[type=checkbox])){align-items:center;display:flex;gap:.5rem}:deep(.formkit-input[type=checkbox]){margin:0;width:auto}:deep(.formkit-label:has(input[type=checkbox])){color:#374151;font-size:.875rem;margin:0}:deep(button[type=submit]){align-items:center;background-color:#3b82f6;border:1px solid #3b82f6;border-radius:8px;color:#fff;cursor:pointer;display:flex;font-size:.875rem;font-weight:500;gap:.5rem;justify-content:center;min-height:44px;padding:.75rem 1rem;transition:all .2s ease-in-out;width:100%}:deep(button[type=submit]:hover:not(:disabled)){background-color:#2563eb;border-color:#2563eb}:deep(button[type=submit]:disabled){cursor:not-allowed;opacity:.6}.loading-spinner{animation:spin 1s linear infinite;border:2px solid #fff;border-radius:50%;border-top-color:transparent;height:16px;width:16px}@keyframes spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}.login-footer{margin-top:1rem;text-align:center}.forgot-password{margin:0}.forgot-link{color:#3b82f6;font-size:.875rem;text-decoration:none;transition:color .2s ease-in-out}.forgot-link:hover{color:#2563eb;text-decoration:underline}.error-message{background-color:#fef2f2;border:1px solid #fecaca;border-radius:8px;color:#dc2626;font-size:.875rem;margin-top:1rem;padding:.75rem 1rem;text-align:center}@media (max-width:480px){.login-form-container{margin:1rem;padding:1.5rem}.login-title{font-size:1.5rem}}
|
|
133
|
+
</style>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { LoginFormData, LoginFormProps, User } from '../../types.js';
|
|
2
|
+
declare var __VLS_9: {}, __VLS_11: {}, __VLS_18: {}, __VLS_25: {}, __VLS_32: {}, __VLS_39: {}, __VLS_41: {
|
|
3
|
+
error: any;
|
|
4
|
+
};
|
|
5
|
+
type __VLS_Slots = {} & {
|
|
6
|
+
header?: (props: typeof __VLS_9) => any;
|
|
7
|
+
} & {
|
|
8
|
+
'email-field'?: (props: typeof __VLS_11) => any;
|
|
9
|
+
} & {
|
|
10
|
+
'password-field'?: (props: typeof __VLS_18) => any;
|
|
11
|
+
} & {
|
|
12
|
+
'remember-me'?: (props: typeof __VLS_25) => any;
|
|
13
|
+
} & {
|
|
14
|
+
'submit-button'?: (props: typeof __VLS_32) => any;
|
|
15
|
+
} & {
|
|
16
|
+
footer?: (props: typeof __VLS_39) => any;
|
|
17
|
+
} & {
|
|
18
|
+
'error-message'?: (props: typeof __VLS_41) => any;
|
|
19
|
+
};
|
|
20
|
+
declare const __VLS_component: import("vue").DefineComponent<LoginFormProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
21
|
+
error: (error: string) => any;
|
|
22
|
+
submit: (data: LoginFormData) => any;
|
|
23
|
+
success: (user: User) => any;
|
|
24
|
+
}, string, import("vue").PublicProps, Readonly<LoginFormProps> & Readonly<{
|
|
25
|
+
onError?: ((error: string) => any) | undefined;
|
|
26
|
+
onSubmit?: ((data: LoginFormData) => any) | undefined;
|
|
27
|
+
onSuccess?: ((user: User) => any) | undefined;
|
|
28
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
29
|
+
declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
|
|
30
|
+
export default _default;
|
|
31
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
32
|
+
new (): {
|
|
33
|
+
$slots: S;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, reactive, onMounted, computed } from "vue";
|
|
3
|
+
import { useRoute, useRouter } from "#app";
|
|
4
|
+
const formData = reactive({
|
|
5
|
+
password: "",
|
|
6
|
+
password_confirm: ""
|
|
7
|
+
});
|
|
8
|
+
const message = ref("");
|
|
9
|
+
const loading = ref(false);
|
|
10
|
+
const isError = ref(false);
|
|
11
|
+
const formInvalidDueToToken = ref(false);
|
|
12
|
+
const route = useRoute();
|
|
13
|
+
const router = useRouter();
|
|
14
|
+
const tokenFromUrl = ref(null);
|
|
15
|
+
const emailFromUrl = ref(null);
|
|
16
|
+
onMounted(() => {
|
|
17
|
+
if (typeof route.query.token === "string") {
|
|
18
|
+
tokenFromUrl.value = route.query.token;
|
|
19
|
+
}
|
|
20
|
+
if (typeof route.query.email === "string") {
|
|
21
|
+
emailFromUrl.value = decodeURIComponent(route.query.email);
|
|
22
|
+
}
|
|
23
|
+
if (!tokenFromUrl.value || !emailFromUrl.value) {
|
|
24
|
+
message.value = "Invalid or missing password reset token or email in URL. Cannot reset password.";
|
|
25
|
+
isError.value = true;
|
|
26
|
+
formInvalidDueToToken.value = true;
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
const canSubmit = computed(() => !!tokenFromUrl.value && !!emailFromUrl.value && !formInvalidDueToToken.value);
|
|
30
|
+
const handleResetPassword = async (data) => {
|
|
31
|
+
if (!canSubmit.value) {
|
|
32
|
+
message.value = "Token or email is missing or invalid. Cannot reset password.";
|
|
33
|
+
isError.value = true;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
loading.value = true;
|
|
37
|
+
message.value = "";
|
|
38
|
+
isError.value = false;
|
|
39
|
+
try {
|
|
40
|
+
const response = await fetch("/api/reset-password", {
|
|
41
|
+
method: "POST",
|
|
42
|
+
headers: {
|
|
43
|
+
"Content-Type": "application/json",
|
|
44
|
+
"Accept": "application/json"
|
|
45
|
+
},
|
|
46
|
+
body: JSON.stringify({
|
|
47
|
+
token: tokenFromUrl.value,
|
|
48
|
+
email: emailFromUrl.value,
|
|
49
|
+
password: data.password,
|
|
50
|
+
// Use password from form data
|
|
51
|
+
password_confirmation: data.password_confirm
|
|
52
|
+
// Use confirmation from form data
|
|
53
|
+
})
|
|
54
|
+
});
|
|
55
|
+
const responseData = await response.json();
|
|
56
|
+
if (!response.ok) {
|
|
57
|
+
throw new Error(responseData.message || responseData.statusMessage || "An error occurred.");
|
|
58
|
+
}
|
|
59
|
+
message.value = responseData.message + " Redirecting to login...";
|
|
60
|
+
isError.value = false;
|
|
61
|
+
formData.password = "";
|
|
62
|
+
formData.password_confirm = "";
|
|
63
|
+
setTimeout(() => {
|
|
64
|
+
router.push("/login");
|
|
65
|
+
}, 3e3);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
if (err instanceof Error) {
|
|
68
|
+
message.value = err.message;
|
|
69
|
+
} else {
|
|
70
|
+
message.value = "Failed to reset password.";
|
|
71
|
+
}
|
|
72
|
+
console.error(err);
|
|
73
|
+
isError.value = true;
|
|
74
|
+
} finally {
|
|
75
|
+
loading.value = false;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
</script>
|
|
79
|
+
|
|
80
|
+
<template>
|
|
81
|
+
<div>
|
|
82
|
+
<h2>Reset Password</h2>
|
|
83
|
+
<FormKit
|
|
84
|
+
v-slot="{ disabled }"
|
|
85
|
+
v-model="formData"
|
|
86
|
+
type="form"
|
|
87
|
+
:actions="false"
|
|
88
|
+
@submit="handleResetPassword"
|
|
89
|
+
>
|
|
90
|
+
<FormKit
|
|
91
|
+
type="password"
|
|
92
|
+
name="password"
|
|
93
|
+
label="New Password"
|
|
94
|
+
validation="required|length:8"
|
|
95
|
+
placeholder="Enter new password"
|
|
96
|
+
/>
|
|
97
|
+
<FormKit
|
|
98
|
+
type="password"
|
|
99
|
+
name="password_confirm"
|
|
100
|
+
label="Confirm New Password"
|
|
101
|
+
placeholder="Confirm new password"
|
|
102
|
+
validation="required|confirm"
|
|
103
|
+
validation-label="Password confirmation"
|
|
104
|
+
/>
|
|
105
|
+
<FormKit
|
|
106
|
+
type="submit"
|
|
107
|
+
:label="loading ? 'Resetting...' : 'Reset Password'"
|
|
108
|
+
:disabled="Boolean(disabled) || loading || formInvalidDueToToken"
|
|
109
|
+
/>
|
|
110
|
+
<p
|
|
111
|
+
v-if="message"
|
|
112
|
+
:class="{ error: isError, success: !isError }"
|
|
113
|
+
class="form-message"
|
|
114
|
+
>
|
|
115
|
+
{{ message }}
|
|
116
|
+
</p>
|
|
117
|
+
</FormKit>
|
|
118
|
+
</div>
|
|
119
|
+
</template>
|
|
120
|
+
|
|
121
|
+
<style scoped>
|
|
122
|
+
.error{color:red}.success{color:green}.form-message{font-size:.9rem;margin-top:1rem}:deep(.formkit-outer){margin-bottom:1rem}:deep(.formkit-label){display:block;font-weight:700;margin-bottom:.25rem}:deep(.formkit-input input[type=email]),:deep(.formkit-input input[type=password]){border:1px solid #ccc;border-radius:4px;box-sizing:border-box;padding:.75rem;width:100%}:deep(.formkit-input input[type=submit]){background-color:#333;border:none;border-radius:4px;color:#fff;cursor:pointer;padding:.75rem 1.5rem}:deep(.formkit-input input[type=submit]:disabled){background-color:#aaa;cursor:not-allowed}:deep(.formkit-messages){list-style:none;margin-top:.25rem;padding:0}:deep(.formkit-message){color:red;font-size:.8rem}
|
|
123
|
+
</style>
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
declare const _default: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
export default _default;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { defineEventHandler, readBody, createError } from "h3";
|
|
2
|
+
import { sendPasswordResetLink } from "../../services/password.js";
|
|
3
|
+
export default defineEventHandler(async (event) => {
|
|
4
|
+
const body = await readBody(event);
|
|
5
|
+
const { email } = body;
|
|
6
|
+
if (!email || typeof email !== "string") {
|
|
7
|
+
throw createError({
|
|
8
|
+
statusCode: 400,
|
|
9
|
+
statusMessage: "Email is required and must be a string."
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
await sendPasswordResetLink(email, event.context.nuxtUsers);
|
|
14
|
+
return { message: "If a user with that email exists, a password reset link has been sent." };
|
|
15
|
+
} catch (error) {
|
|
16
|
+
if (error instanceof Error) {
|
|
17
|
+
console.error("Error in forgot-password endpoint:", error.message);
|
|
18
|
+
} else {
|
|
19
|
+
console.error("Error in forgot-password endpoint:", error);
|
|
20
|
+
}
|
|
21
|
+
throw createError({
|
|
22
|
+
statusCode: 500,
|
|
23
|
+
statusMessage: "An internal server error occurred."
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
});
|