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.
Files changed (39) hide show
  1. package/README.md +69 -0
  2. package/dist/module.d.mts +49 -0
  3. package/dist/module.json +9 -0
  4. package/dist/module.mjs +122 -0
  5. package/dist/runtime/components/ForgotPasswordForm.vue +79 -0
  6. package/dist/runtime/components/ForgotPasswordForm.vue.d.ts +2 -0
  7. package/dist/runtime/components/LoginForm.vue +133 -0
  8. package/dist/runtime/components/LoginForm.vue.d.ts +35 -0
  9. package/dist/runtime/components/ResetPasswordForm.vue +123 -0
  10. package/dist/runtime/components/ResetPasswordForm.vue.d.ts +2 -0
  11. package/dist/runtime/plugin.d.ts +2 -0
  12. package/dist/runtime/plugin.js +4 -0
  13. package/dist/runtime/server/api/auth/forgot-password.post.d.ts +4 -0
  14. package/dist/runtime/server/api/auth/forgot-password.post.js +26 -0
  15. package/dist/runtime/server/api/auth/reset-password.post.d.ts +4 -0
  16. package/dist/runtime/server/api/auth/reset-password.post.js +48 -0
  17. package/dist/runtime/server/api/login.post.d.ts +10 -0
  18. package/dist/runtime/server/api/login.post.js +57 -0
  19. package/dist/runtime/server/services/password.d.ts +14 -0
  20. package/dist/runtime/server/services/password.js +113 -0
  21. package/dist/runtime/server/tsconfig.json +3 -0
  22. package/dist/runtime/server/utils/create-migrations-table.d.ts +2 -0
  23. package/dist/runtime/server/utils/create-migrations-table.js +37 -0
  24. package/dist/runtime/server/utils/create-password-reset-tokens-table.d.ts +2 -0
  25. package/dist/runtime/server/utils/create-password-reset-tokens-table.js +42 -0
  26. package/dist/runtime/server/utils/create-personal-access-tokens-table.d.ts +2 -0
  27. package/dist/runtime/server/utils/create-personal-access-tokens-table.js +58 -0
  28. package/dist/runtime/server/utils/create-user.d.ts +1 -0
  29. package/dist/runtime/server/utils/create-user.js +1 -0
  30. package/dist/runtime/server/utils/create-users-table.d.ts +2 -0
  31. package/dist/runtime/server/utils/create-users-table.js +46 -0
  32. package/dist/runtime/server/utils/db.d.ts +7 -0
  33. package/dist/runtime/server/utils/db.js +45 -0
  34. package/dist/runtime/server/utils/migrate.d.ts +4 -0
  35. package/dist/runtime/server/utils/migrate.js +68 -0
  36. package/dist/runtime/server/utils/user.d.ts +21 -0
  37. package/dist/runtime/server/utils/user.js +57 -0
  38. package/dist/types.d.mts +7 -0
  39. 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 };
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "nuxt-users",
3
+ "configKey": "nuxtUsers",
4
+ "version": "1.1.0",
5
+ "builder": {
6
+ "@nuxt/module-builder": "1.0.1",
7
+ "unbuild": "3.5.0"
8
+ }
9
+ }
@@ -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,2 @@
1
+ declare const _default: import("#app").Plugin<Record<string, unknown>> & import("#app").ObjectPlugin<Record<string, unknown>>;
2
+ export default _default;
@@ -0,0 +1,4 @@
1
+ import { defineNuxtPlugin } from "#app";
2
+ export default defineNuxtPlugin((_nuxtApp) => {
3
+ console.log("Plugin injected by nuxt-users!");
4
+ });
@@ -0,0 +1,4 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
2
+ message: string;
3
+ }>>;
4
+ 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
+ });
@@ -0,0 +1,4 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
2
+ message: string;
3
+ }>>;
4
+ export default _default;