nuxt-users 1.28.2 → 1.29.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 +31 -0
- package/dist/module.d.mts +1 -1
- package/dist/module.d.ts +1 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +59 -3
- package/dist/runtime/components/NUsersRegisterForm.vue +229 -0
- package/dist/runtime/components/NUsersRegisterForm.vue.d.ts +60 -0
- package/dist/runtime/server/api/nuxt-users/confirm-email.get.d.ts +5 -0
- package/dist/runtime/server/api/nuxt-users/confirm-email.get.js +42 -0
- package/dist/runtime/server/api/nuxt-users/register.post.d.ts +6 -0
- package/dist/runtime/server/api/nuxt-users/register.post.js +54 -0
- package/dist/runtime/server/services/registration.d.ts +22 -0
- package/dist/runtime/server/services/registration.js +187 -0
- package/dist/shared/{nuxt-users.CydOpwYH.d.mts → nuxt-users.CFq7zOql.d.mts} +1 -1
- package/dist/shared/{nuxt-users.CydOpwYH.d.ts → nuxt-users.CFq7zOql.d.ts} +1 -1
- package/dist/utils.d.mts +1 -1
- package/dist/utils.d.ts +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
## v1.29.0
|
|
5
|
+
|
|
6
|
+
[compare changes](https://github.com/rrd108/nuxt-users/compare/v1.28.2...v1.29.0)
|
|
7
|
+
|
|
8
|
+
### 🚀 Enhancements
|
|
9
|
+
|
|
10
|
+
- Implement register functionality ([8ea4f89](https://github.com/rrd108/nuxt-users/commit/8ea4f89))
|
|
11
|
+
|
|
12
|
+
### 🩹 Fixes
|
|
13
|
+
|
|
14
|
+
- Lost reactivity for user ([742cd1e](https://github.com/rrd108/nuxt-users/commit/742cd1e))
|
|
15
|
+
- Types and test ([db71fe4](https://github.com/rrd108/nuxt-users/commit/db71fe4))
|
|
16
|
+
- Type annotations ([d4a408b](https://github.com/rrd108/nuxt-users/commit/d4a408b))
|
|
17
|
+
- Missing server middleware ([50e22d5](https://github.com/rrd108/nuxt-users/commit/50e22d5))
|
|
18
|
+
- Auto whitelist urls on register ([8a05dab](https://github.com/rrd108/nuxt-users/commit/8a05dab))
|
|
19
|
+
|
|
20
|
+
### 🏡 Chore
|
|
21
|
+
|
|
22
|
+
- Remove login from example ([8d4611a](https://github.com/rrd108/nuxt-users/commit/8d4611a))
|
|
23
|
+
- **release:** V1.28.2 ([b1e25cc](https://github.com/rrd108/nuxt-users/commit/b1e25cc))
|
|
24
|
+
- Fix linting issues ([7e45645](https://github.com/rrd108/nuxt-users/commit/7e45645))
|
|
25
|
+
- Lint fixes ([bf5786f](https://github.com/rrd108/nuxt-users/commit/bf5786f))
|
|
26
|
+
|
|
27
|
+
### ✅ Tests
|
|
28
|
+
|
|
29
|
+
- Add test for register ([ac29f3a](https://github.com/rrd108/nuxt-users/commit/ac29f3a))
|
|
30
|
+
|
|
31
|
+
### ❤️ Contributors
|
|
32
|
+
|
|
33
|
+
- Rrd108 ([@rrd108](https://github.com/rrd108))
|
|
34
|
+
|
|
4
35
|
## v1.28.2
|
|
5
36
|
|
|
6
37
|
[compare changes](https://github.com/rrd108/nuxt-users/compare/v1.28.1...v1.28.2)
|
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.CFq7zOql.mjs';
|
|
3
3
|
export * from '../dist/runtime/server/internal/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.CFq7zOql.js';
|
|
3
3
|
export * from '../dist/runtime/server/internal/index.js';
|
|
4
4
|
|
|
5
5
|
declare const defaultOptions: ModuleOptions;
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineNuxtModule, createResolver, addPlugin, addImportsDir, addServerImportsDir,
|
|
1
|
+
import { defineNuxtModule, createResolver, addPlugin, addImportsDir, addServerImportsDir, addServerHandler, addRouteMiddleware, addComponent } from '@nuxt/kit';
|
|
2
2
|
import { defu } from 'defu';
|
|
3
3
|
export * from '../dist/runtime/server/internal/index.js';
|
|
4
4
|
|
|
@@ -70,7 +70,26 @@ const module = defineNuxtModule({
|
|
|
70
70
|
passwordResetTokens: options.tables?.passwordResetTokens || defaultOptions.tables.passwordResetTokens
|
|
71
71
|
},
|
|
72
72
|
auth: {
|
|
73
|
-
whitelist:
|
|
73
|
+
whitelist: (() => {
|
|
74
|
+
const combinedWhitelist = [...defaultOptions.auth?.whitelist || [], ...options.auth?.whitelist || []];
|
|
75
|
+
if (combinedWhitelist.includes("/register")) {
|
|
76
|
+
const apiBasePath = options.apiBasePath || defaultOptions.apiBasePath;
|
|
77
|
+
const registrationEndpoints = [
|
|
78
|
+
"/confirm-email",
|
|
79
|
+
// Page route for email confirmation
|
|
80
|
+
`${apiBasePath}/register`,
|
|
81
|
+
// API endpoint for registration
|
|
82
|
+
`${apiBasePath}/confirm-email`
|
|
83
|
+
// API endpoint for email confirmation
|
|
84
|
+
];
|
|
85
|
+
registrationEndpoints.forEach((endpoint) => {
|
|
86
|
+
if (!combinedWhitelist.includes(endpoint)) {
|
|
87
|
+
combinedWhitelist.push(endpoint);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
return combinedWhitelist;
|
|
92
|
+
})(),
|
|
74
93
|
tokenExpiration: options.auth?.tokenExpiration || defaultOptions.auth.tokenExpiration,
|
|
75
94
|
permissions: options.auth?.permissions || defaultOptions.auth.permissions
|
|
76
95
|
},
|
|
@@ -87,7 +106,26 @@ const module = defineNuxtModule({
|
|
|
87
106
|
preventCommonPasswords: options.passwordValidation?.preventCommonPasswords ?? defaultOptions.passwordValidation.preventCommonPasswords
|
|
88
107
|
},
|
|
89
108
|
auth: {
|
|
90
|
-
whitelist:
|
|
109
|
+
whitelist: (() => {
|
|
110
|
+
const combinedWhitelist = [...defaultOptions.auth?.whitelist || [], ...options.auth?.whitelist || []];
|
|
111
|
+
if (combinedWhitelist.includes("/register")) {
|
|
112
|
+
const apiBasePath = options.apiBasePath || defaultOptions.apiBasePath;
|
|
113
|
+
const registrationEndpoints = [
|
|
114
|
+
"/confirm-email",
|
|
115
|
+
// Page route for email confirmation
|
|
116
|
+
`${apiBasePath}/register`,
|
|
117
|
+
// API endpoint for registration
|
|
118
|
+
`${apiBasePath}/confirm-email`
|
|
119
|
+
// API endpoint for email confirmation
|
|
120
|
+
];
|
|
121
|
+
registrationEndpoints.forEach((endpoint) => {
|
|
122
|
+
if (!combinedWhitelist.includes(endpoint)) {
|
|
123
|
+
combinedWhitelist.push(endpoint);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return combinedWhitelist;
|
|
128
|
+
})(),
|
|
91
129
|
permissions: options.auth?.permissions || defaultOptions.auth.permissions
|
|
92
130
|
},
|
|
93
131
|
apiBasePath: options.apiBasePath || defaultOptions.apiBasePath
|
|
@@ -98,6 +136,10 @@ const module = defineNuxtModule({
|
|
|
98
136
|
});
|
|
99
137
|
addImportsDir(resolver.resolve("./runtime/composables"));
|
|
100
138
|
addServerImportsDir(resolver.resolve("./runtime/server/composables"));
|
|
139
|
+
addServerHandler({
|
|
140
|
+
middleware: true,
|
|
141
|
+
handler: resolver.resolve("./runtime/server/middleware/authorization.server")
|
|
142
|
+
});
|
|
101
143
|
addRouteMiddleware({
|
|
102
144
|
name: "authorization.client",
|
|
103
145
|
path: resolver.resolve("./runtime/middleware/authorization.client"),
|
|
@@ -139,6 +181,16 @@ const module = defineNuxtModule({
|
|
|
139
181
|
method: "post",
|
|
140
182
|
handler: resolver.resolve("./runtime/server/api/nuxt-users/password/reset.post")
|
|
141
183
|
});
|
|
184
|
+
addServerHandler({
|
|
185
|
+
route: `${base}/register`,
|
|
186
|
+
method: "post",
|
|
187
|
+
handler: resolver.resolve("./runtime/server/api/nuxt-users/register.post")
|
|
188
|
+
});
|
|
189
|
+
addServerHandler({
|
|
190
|
+
route: `${base}/confirm-email`,
|
|
191
|
+
method: "get",
|
|
192
|
+
handler: resolver.resolve("./runtime/server/api/nuxt-users/confirm-email.get")
|
|
193
|
+
});
|
|
142
194
|
addServerHandler({
|
|
143
195
|
route: `${base}`,
|
|
144
196
|
method: "get",
|
|
@@ -225,6 +277,10 @@ const module = defineNuxtModule({
|
|
|
225
277
|
name: "NUsersUserForm",
|
|
226
278
|
filePath: resolver.resolve("./runtime/components/NUsersUserForm.vue")
|
|
227
279
|
});
|
|
280
|
+
addComponent({
|
|
281
|
+
name: "NUsersRegisterForm",
|
|
282
|
+
filePath: resolver.resolve("./runtime/components/NUsersRegisterForm.vue")
|
|
283
|
+
});
|
|
228
284
|
nuxt.options.css = nuxt.options.css || [];
|
|
229
285
|
nuxt.options.css.push(resolver.resolve("./runtime/assets/nuxt-users.css"));
|
|
230
286
|
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, computed, watch } from "vue";
|
|
3
|
+
import { navigateTo } from "#app";
|
|
4
|
+
import { useRuntimeConfig } from "#imports";
|
|
5
|
+
import { validatePassword } from "nuxt-users/utils";
|
|
6
|
+
const { public: { nuxtUsers } } = useRuntimeConfig();
|
|
7
|
+
const { passwordValidation } = nuxtUsers;
|
|
8
|
+
const props = defineProps({
|
|
9
|
+
apiEndpoint: { type: String, required: false },
|
|
10
|
+
redirectTo: { type: String, required: false },
|
|
11
|
+
loginLink: { type: String, required: false }
|
|
12
|
+
});
|
|
13
|
+
const emit = defineEmits(["success", "error", "submit"]);
|
|
14
|
+
const apiEndpoint = computed(() => props.apiEndpoint || `${nuxtUsers?.apiBasePath}/register`);
|
|
15
|
+
const isLoading = ref(false);
|
|
16
|
+
const error = ref("");
|
|
17
|
+
const successMessage = ref("");
|
|
18
|
+
const passwordValidationResult = ref(null);
|
|
19
|
+
const formData = ref({
|
|
20
|
+
email: "",
|
|
21
|
+
name: "",
|
|
22
|
+
password: "",
|
|
23
|
+
confirmPassword: ""
|
|
24
|
+
});
|
|
25
|
+
watch(() => formData.value.password, (newPassword) => {
|
|
26
|
+
if (newPassword) {
|
|
27
|
+
passwordValidationResult.value = validatePassword(newPassword, passwordValidation);
|
|
28
|
+
} else {
|
|
29
|
+
passwordValidationResult.value = null;
|
|
30
|
+
}
|
|
31
|
+
}, { immediate: true });
|
|
32
|
+
const passwordsMatch = computed(() => {
|
|
33
|
+
return formData.value.password === formData.value.confirmPassword || !formData.value.confirmPassword;
|
|
34
|
+
});
|
|
35
|
+
const isFormValid = computed(() => {
|
|
36
|
+
return formData.value.email && formData.value.name && formData.value.password && formData.value.confirmPassword && passwordsMatch.value && (passwordValidationResult.value?.isValid ?? false);
|
|
37
|
+
});
|
|
38
|
+
const handleSubmit = async () => {
|
|
39
|
+
if (!isFormValid.value) {
|
|
40
|
+
error.value = "Please fill all fields correctly";
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
isLoading.value = true;
|
|
44
|
+
error.value = "";
|
|
45
|
+
successMessage.value = "";
|
|
46
|
+
try {
|
|
47
|
+
emit("submit", formData.value);
|
|
48
|
+
const response = await $fetch(apiEndpoint.value, {
|
|
49
|
+
method: "POST",
|
|
50
|
+
body: {
|
|
51
|
+
email: formData.value.email,
|
|
52
|
+
name: formData.value.name,
|
|
53
|
+
password: formData.value.password
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
successMessage.value = response.message;
|
|
57
|
+
emit("success", response);
|
|
58
|
+
formData.value = {
|
|
59
|
+
email: "",
|
|
60
|
+
name: "",
|
|
61
|
+
password: "",
|
|
62
|
+
confirmPassword: ""
|
|
63
|
+
};
|
|
64
|
+
if (props.redirectTo) {
|
|
65
|
+
setTimeout(() => {
|
|
66
|
+
navigateTo(props.redirectTo);
|
|
67
|
+
}, 3e3);
|
|
68
|
+
}
|
|
69
|
+
} catch (err) {
|
|
70
|
+
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 : "Registration failed";
|
|
71
|
+
error.value = errorMessage;
|
|
72
|
+
emit("error", errorMessage);
|
|
73
|
+
} finally {
|
|
74
|
+
isLoading.value = false;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
</script>
|
|
78
|
+
|
|
79
|
+
<template>
|
|
80
|
+
<section class="n-users-register-section n-users-section">
|
|
81
|
+
<form @submit.prevent="handleSubmit">
|
|
82
|
+
<!-- Header slot -->
|
|
83
|
+
<slot name="header">
|
|
84
|
+
<div class="n-users-register-header">
|
|
85
|
+
<h2 class="n-users-register-title">
|
|
86
|
+
Create Account
|
|
87
|
+
</h2>
|
|
88
|
+
<p class="n-users-register-subtitle">
|
|
89
|
+
Sign up for a new account
|
|
90
|
+
</p>
|
|
91
|
+
</div>
|
|
92
|
+
</slot>
|
|
93
|
+
|
|
94
|
+
<!-- Name field -->
|
|
95
|
+
<slot name="name-field">
|
|
96
|
+
<div class="n-users-form-group">
|
|
97
|
+
<label for="name">Full Name</label>
|
|
98
|
+
<input
|
|
99
|
+
id="name"
|
|
100
|
+
v-model="formData.name"
|
|
101
|
+
type="text"
|
|
102
|
+
name="name"
|
|
103
|
+
placeholder="Enter your full name"
|
|
104
|
+
required
|
|
105
|
+
>
|
|
106
|
+
</div>
|
|
107
|
+
</slot>
|
|
108
|
+
|
|
109
|
+
<!-- Email field -->
|
|
110
|
+
<slot name="email-field">
|
|
111
|
+
<div class="n-users-form-group">
|
|
112
|
+
<label for="email">Email</label>
|
|
113
|
+
<input
|
|
114
|
+
id="email"
|
|
115
|
+
v-model="formData.email"
|
|
116
|
+
type="email"
|
|
117
|
+
name="email"
|
|
118
|
+
placeholder="Enter your email"
|
|
119
|
+
required
|
|
120
|
+
>
|
|
121
|
+
</div>
|
|
122
|
+
</slot>
|
|
123
|
+
|
|
124
|
+
<!-- Password field -->
|
|
125
|
+
<slot name="password-field">
|
|
126
|
+
<div class="n-users-form-group">
|
|
127
|
+
<label for="password">Password</label>
|
|
128
|
+
<input
|
|
129
|
+
id="password"
|
|
130
|
+
v-model="formData.password"
|
|
131
|
+
type="password"
|
|
132
|
+
name="password"
|
|
133
|
+
placeholder="Enter your password"
|
|
134
|
+
required
|
|
135
|
+
:minlength="passwordValidation.minLength"
|
|
136
|
+
>
|
|
137
|
+
</div>
|
|
138
|
+
</slot>
|
|
139
|
+
|
|
140
|
+
<!-- Password strength indicator -->
|
|
141
|
+
<slot name="password-strength">
|
|
142
|
+
<NUsersPasswordStrengthIndicator
|
|
143
|
+
v-if="formData.password"
|
|
144
|
+
:password="formData.password"
|
|
145
|
+
:validation-result="passwordValidationResult"
|
|
146
|
+
:show-hints="true"
|
|
147
|
+
:show-rules="true"
|
|
148
|
+
/>
|
|
149
|
+
</slot>
|
|
150
|
+
|
|
151
|
+
<!-- Confirm Password field -->
|
|
152
|
+
<slot name="confirm-password-field">
|
|
153
|
+
<div class="n-users-form-group">
|
|
154
|
+
<label for="confirmPassword">Confirm Password</label>
|
|
155
|
+
<input
|
|
156
|
+
id="confirmPassword"
|
|
157
|
+
v-model="formData.confirmPassword"
|
|
158
|
+
type="password"
|
|
159
|
+
name="confirmPassword"
|
|
160
|
+
placeholder="Confirm your password"
|
|
161
|
+
required
|
|
162
|
+
:class="{ 'n-users-input-error': !passwordsMatch }"
|
|
163
|
+
>
|
|
164
|
+
<small
|
|
165
|
+
v-if="!passwordsMatch && formData.confirmPassword"
|
|
166
|
+
class="n-users-error-text"
|
|
167
|
+
>
|
|
168
|
+
Passwords do not match
|
|
169
|
+
</small>
|
|
170
|
+
</div>
|
|
171
|
+
</slot>
|
|
172
|
+
|
|
173
|
+
<!-- Submit button slot -->
|
|
174
|
+
<slot name="submit-button">
|
|
175
|
+
<button
|
|
176
|
+
type="submit"
|
|
177
|
+
:disabled="isLoading || !isFormValid"
|
|
178
|
+
>
|
|
179
|
+
<span
|
|
180
|
+
v-if="isLoading"
|
|
181
|
+
class="n-users-loading-spinner"
|
|
182
|
+
/>
|
|
183
|
+
{{ isLoading ? "Creating Account..." : "Create Account" }}
|
|
184
|
+
</button>
|
|
185
|
+
</slot>
|
|
186
|
+
|
|
187
|
+
<!-- Footer slot -->
|
|
188
|
+
<slot name="footer">
|
|
189
|
+
<div class="n-users-register-footer">
|
|
190
|
+
<p class="n-users-login-link">
|
|
191
|
+
Already have an account?
|
|
192
|
+
<a
|
|
193
|
+
:href="loginLink || '/login'"
|
|
194
|
+
class="n-users-link"
|
|
195
|
+
>
|
|
196
|
+
Sign in here
|
|
197
|
+
</a>
|
|
198
|
+
</p>
|
|
199
|
+
</div>
|
|
200
|
+
</slot>
|
|
201
|
+
|
|
202
|
+
<!-- Success message slot -->
|
|
203
|
+
<slot
|
|
204
|
+
name="success-message"
|
|
205
|
+
:message="successMessage"
|
|
206
|
+
>
|
|
207
|
+
<div
|
|
208
|
+
v-if="successMessage"
|
|
209
|
+
class="n-users-success-message"
|
|
210
|
+
>
|
|
211
|
+
{{ successMessage }}
|
|
212
|
+
</div>
|
|
213
|
+
</slot>
|
|
214
|
+
|
|
215
|
+
<!-- Error message slot -->
|
|
216
|
+
<slot
|
|
217
|
+
name="error-message"
|
|
218
|
+
:error="error"
|
|
219
|
+
>
|
|
220
|
+
<div
|
|
221
|
+
v-if="error"
|
|
222
|
+
class="n-users-error-message"
|
|
223
|
+
>
|
|
224
|
+
{{ error }}
|
|
225
|
+
</div>
|
|
226
|
+
</slot>
|
|
227
|
+
</form>
|
|
228
|
+
</section>
|
|
229
|
+
</template>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { UserWithoutPassword } from 'nuxt-users/utils';
|
|
2
|
+
interface Props {
|
|
3
|
+
apiEndpoint?: string;
|
|
4
|
+
redirectTo?: string;
|
|
5
|
+
loginLink?: string;
|
|
6
|
+
}
|
|
7
|
+
interface RegistrationFormData {
|
|
8
|
+
email: string;
|
|
9
|
+
name: string;
|
|
10
|
+
password: string;
|
|
11
|
+
confirmPassword: string;
|
|
12
|
+
}
|
|
13
|
+
declare var __VLS_1: {}, __VLS_3: {}, __VLS_5: {}, __VLS_7: {}, __VLS_9: {}, __VLS_16: {}, __VLS_18: {}, __VLS_20: {}, __VLS_22: {
|
|
14
|
+
message: string;
|
|
15
|
+
}, __VLS_24: {
|
|
16
|
+
error: string;
|
|
17
|
+
};
|
|
18
|
+
type __VLS_Slots = {} & {
|
|
19
|
+
header?: (props: typeof __VLS_1) => any;
|
|
20
|
+
} & {
|
|
21
|
+
'name-field'?: (props: typeof __VLS_3) => any;
|
|
22
|
+
} & {
|
|
23
|
+
'email-field'?: (props: typeof __VLS_5) => any;
|
|
24
|
+
} & {
|
|
25
|
+
'password-field'?: (props: typeof __VLS_7) => any;
|
|
26
|
+
} & {
|
|
27
|
+
'password-strength'?: (props: typeof __VLS_9) => any;
|
|
28
|
+
} & {
|
|
29
|
+
'confirm-password-field'?: (props: typeof __VLS_16) => any;
|
|
30
|
+
} & {
|
|
31
|
+
'submit-button'?: (props: typeof __VLS_18) => any;
|
|
32
|
+
} & {
|
|
33
|
+
footer?: (props: typeof __VLS_20) => any;
|
|
34
|
+
} & {
|
|
35
|
+
'success-message'?: (props: typeof __VLS_22) => any;
|
|
36
|
+
} & {
|
|
37
|
+
'error-message'?: (props: typeof __VLS_24) => any;
|
|
38
|
+
};
|
|
39
|
+
declare const __VLS_component: import("vue").DefineComponent<Props, void, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
40
|
+
error: (error: string) => any;
|
|
41
|
+
submit: (data: RegistrationFormData) => any;
|
|
42
|
+
success: (data: {
|
|
43
|
+
user: Omit<UserWithoutPassword, "active">;
|
|
44
|
+
message: string;
|
|
45
|
+
}) => any;
|
|
46
|
+
}, string, import("vue").PublicProps, Readonly<Props> & Readonly<{
|
|
47
|
+
onError?: ((error: string) => any) | undefined;
|
|
48
|
+
onSubmit?: ((data: RegistrationFormData) => any) | undefined;
|
|
49
|
+
onSuccess?: ((data: {
|
|
50
|
+
user: Omit<UserWithoutPassword, "active">;
|
|
51
|
+
message: string;
|
|
52
|
+
}) => any) | undefined;
|
|
53
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
54
|
+
declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
|
|
55
|
+
export default _default;
|
|
56
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
57
|
+
new (): {
|
|
58
|
+
$slots: S;
|
|
59
|
+
};
|
|
60
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { createError, defineEventHandler, getQuery } from "h3";
|
|
2
|
+
import { useRuntimeConfig } from "#imports";
|
|
3
|
+
import { confirmUserEmail } from "../../services/registration.js";
|
|
4
|
+
export default defineEventHandler(async (event) => {
|
|
5
|
+
try {
|
|
6
|
+
const { nuxtUsers } = useRuntimeConfig();
|
|
7
|
+
const options = nuxtUsers;
|
|
8
|
+
const query = getQuery(event);
|
|
9
|
+
if (!query.token || !query.email) {
|
|
10
|
+
throw createError({
|
|
11
|
+
statusCode: 400,
|
|
12
|
+
statusMessage: "Token and email are required"
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
const token = String(query.token);
|
|
16
|
+
const email = String(query.email);
|
|
17
|
+
const success = await confirmUserEmail(token, email, options);
|
|
18
|
+
if (success) {
|
|
19
|
+
return {
|
|
20
|
+
success: true,
|
|
21
|
+
message: "Email confirmed successfully! Your account is now active. You can now log in."
|
|
22
|
+
};
|
|
23
|
+
} else {
|
|
24
|
+
throw createError({
|
|
25
|
+
statusCode: 400,
|
|
26
|
+
statusMessage: "Invalid or expired confirmation token"
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.error("[Nuxt Users] Email confirmation error:", error);
|
|
31
|
+
if (error instanceof Error) {
|
|
32
|
+
throw createError({
|
|
33
|
+
statusCode: 400,
|
|
34
|
+
statusMessage: error.message
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
throw createError({
|
|
38
|
+
statusCode: 500,
|
|
39
|
+
statusMessage: "Email confirmation failed"
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { createError, defineEventHandler, readBody, getHeader } from "h3";
|
|
2
|
+
import { useRuntimeConfig } from "#imports";
|
|
3
|
+
import { registerUser } from "../../services/registration.js";
|
|
4
|
+
export default defineEventHandler(async (event) => {
|
|
5
|
+
try {
|
|
6
|
+
const { nuxtUsers } = useRuntimeConfig();
|
|
7
|
+
const options = nuxtUsers;
|
|
8
|
+
const body = await readBody(event);
|
|
9
|
+
if (!body.email || !body.name || !body.password) {
|
|
10
|
+
throw createError({
|
|
11
|
+
statusCode: 400,
|
|
12
|
+
statusMessage: "Email, name, and password are required"
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
const emailRegex = /^[^\s@]+@[^\s@][^\s.@]*\.[^\s@]+$/;
|
|
16
|
+
if (!emailRegex.test(body.email)) {
|
|
17
|
+
throw createError({
|
|
18
|
+
statusCode: 400,
|
|
19
|
+
statusMessage: "Please provide a valid email address"
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
if (body.name.trim().length < 1) {
|
|
23
|
+
throw createError({
|
|
24
|
+
statusCode: 400,
|
|
25
|
+
statusMessage: "Name cannot be empty"
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
const host = getHeader(event, "host") || "localhost:3000";
|
|
29
|
+
const protocol = getHeader(event, "x-forwarded-proto") || "http";
|
|
30
|
+
const baseUrl = `${protocol}://${host}`;
|
|
31
|
+
const result = await registerUser(
|
|
32
|
+
{
|
|
33
|
+
email: body.email.toLowerCase().trim(),
|
|
34
|
+
name: body.name.trim(),
|
|
35
|
+
password: body.password
|
|
36
|
+
},
|
|
37
|
+
options,
|
|
38
|
+
baseUrl
|
|
39
|
+
);
|
|
40
|
+
return result;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error("[Nuxt Users] Registration error:", error);
|
|
43
|
+
if (error instanceof Error) {
|
|
44
|
+
throw createError({
|
|
45
|
+
statusCode: 400,
|
|
46
|
+
statusMessage: error.message
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
throw createError({
|
|
50
|
+
statusCode: 500,
|
|
51
|
+
statusMessage: "Registration failed"
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ModuleOptions, UserWithoutPassword } from 'nuxt-users/utils';
|
|
2
|
+
interface RegistrationData {
|
|
3
|
+
email: string;
|
|
4
|
+
name: string;
|
|
5
|
+
password: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Creates a new user with email confirmation required
|
|
9
|
+
*/
|
|
10
|
+
export declare const registerUser: (userData: RegistrationData, options: ModuleOptions, baseUrl?: string) => Promise<{
|
|
11
|
+
user: Omit<UserWithoutPassword, "active">;
|
|
12
|
+
message: string;
|
|
13
|
+
}>;
|
|
14
|
+
/**
|
|
15
|
+
* Sends a confirmation email to the newly registered user
|
|
16
|
+
*/
|
|
17
|
+
export declare const sendConfirmationEmail: (email: string, name: string, token: string, options: ModuleOptions, baseUrl?: string) => Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Confirms a user's email address and activates their account
|
|
20
|
+
*/
|
|
21
|
+
export declare const confirmUserEmail: (token: string, email: string, options: ModuleOptions) => Promise<boolean>;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { createTransport } from "nodemailer";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import bcrypt from "bcrypt";
|
|
4
|
+
import { findUserByEmail, useDb } from "../utils/index.js";
|
|
5
|
+
import { validatePassword, getPasswordValidationOptions } from "nuxt-users/utils";
|
|
6
|
+
const TOKEN_EXPIRATION_HOURS = 24;
|
|
7
|
+
export const registerUser = async (userData, options, baseUrl) => {
|
|
8
|
+
const existingUser = await findUserByEmail(userData.email, options);
|
|
9
|
+
if (existingUser) {
|
|
10
|
+
throw new Error("A user with this email already exists");
|
|
11
|
+
}
|
|
12
|
+
const passwordOptions = getPasswordValidationOptions(options);
|
|
13
|
+
const passwordValidation = validatePassword(userData.password, passwordOptions);
|
|
14
|
+
if (!passwordValidation.isValid) {
|
|
15
|
+
throw new Error(`Password validation failed: ${passwordValidation.errors.join(", ")}`);
|
|
16
|
+
}
|
|
17
|
+
const db = await useDb(options);
|
|
18
|
+
const usersTable = options.tables.users;
|
|
19
|
+
const hashedPassword = await bcrypt.hash(userData.password, 10);
|
|
20
|
+
const confirmationToken = crypto.randomBytes(32).toString("hex");
|
|
21
|
+
const hashedToken = await bcrypt.hash(confirmationToken, 10);
|
|
22
|
+
await db.sql`
|
|
23
|
+
INSERT INTO {${usersTable}} (email, name, password, role, active, created_at, updated_at)
|
|
24
|
+
VALUES (${userData.email}, ${userData.name}, ${hashedPassword}, 'user', false, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
25
|
+
`;
|
|
26
|
+
const result = await db.sql`
|
|
27
|
+
SELECT id, email, name, role, created_at, updated_at
|
|
28
|
+
FROM {${usersTable}}
|
|
29
|
+
WHERE email = ${userData.email}
|
|
30
|
+
`;
|
|
31
|
+
if (result.rows.length === 0) {
|
|
32
|
+
throw new Error("Failed to create user.");
|
|
33
|
+
}
|
|
34
|
+
const user = result.rows[0];
|
|
35
|
+
const passwordResetTokensTable = options.tables.passwordResetTokens;
|
|
36
|
+
await db.sql`
|
|
37
|
+
INSERT INTO {${passwordResetTokensTable}} (email, token, created_at)
|
|
38
|
+
VALUES (${userData.email}, ${hashedToken}, CURRENT_TIMESTAMP)
|
|
39
|
+
`;
|
|
40
|
+
try {
|
|
41
|
+
await sendConfirmationEmail(userData.email, userData.name, confirmationToken, options, baseUrl);
|
|
42
|
+
} catch (emailError) {
|
|
43
|
+
console.error("[Nuxt Users] Failed to send confirmation email, but user was created successfully:", emailError);
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
user: {
|
|
47
|
+
id: user.id,
|
|
48
|
+
email: user.email,
|
|
49
|
+
name: user.name,
|
|
50
|
+
role: user.role,
|
|
51
|
+
created_at: user.created_at instanceof Date ? user.created_at.toISOString() : user.created_at,
|
|
52
|
+
updated_at: user.updated_at instanceof Date ? user.updated_at.toISOString() : user.updated_at
|
|
53
|
+
},
|
|
54
|
+
message: "Registration successful! Please check your email to confirm your account."
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
export const sendConfirmationEmail = async (email, name, token, options, baseUrl) => {
|
|
58
|
+
if (!options.mailer) {
|
|
59
|
+
console.error("[Nuxt Users] Mailer configuration is missing. Cannot send confirmation email.");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const transporter = createTransport({
|
|
63
|
+
host: options.mailer.host,
|
|
64
|
+
port: options.mailer.port,
|
|
65
|
+
secure: options.mailer.secure,
|
|
66
|
+
auth: {
|
|
67
|
+
user: options.mailer.auth.user,
|
|
68
|
+
pass: options.mailer.auth.pass
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
const appBaseUrl = baseUrl || "http://localhost:3000";
|
|
72
|
+
const confirmationUrl = new URL(`${options.apiBasePath}/confirm-email`, appBaseUrl);
|
|
73
|
+
confirmationUrl.searchParams.set("token", token);
|
|
74
|
+
confirmationUrl.searchParams.set("email", email);
|
|
75
|
+
const confirmationLink = confirmationUrl.toString();
|
|
76
|
+
try {
|
|
77
|
+
await transporter.sendMail({
|
|
78
|
+
from: options.mailer.defaults?.from || '"Nuxt Users" <noreply@example.com>',
|
|
79
|
+
to: email,
|
|
80
|
+
subject: "Confirm your email address",
|
|
81
|
+
text: `Hi ${name},
|
|
82
|
+
|
|
83
|
+
Welcome! Please click the following link to confirm your email address and activate your account:
|
|
84
|
+
|
|
85
|
+
${confirmationLink}
|
|
86
|
+
|
|
87
|
+
This link will expire in ${TOKEN_EXPIRATION_HOURS} hours.
|
|
88
|
+
|
|
89
|
+
If you did not create an account, please ignore this email.`,
|
|
90
|
+
html: `
|
|
91
|
+
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
|
92
|
+
<h2>Welcome, ${name}!</h2>
|
|
93
|
+
<p>Thank you for registering! Please click the button below to confirm your email address and activate your account:</p>
|
|
94
|
+
|
|
95
|
+
<div style="text-align: center; margin: 30px 0;">
|
|
96
|
+
<a href="${confirmationLink}"
|
|
97
|
+
style="background-color: #28a745; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px; display: inline-block;">
|
|
98
|
+
Confirm Email Address
|
|
99
|
+
</a>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<p>Or copy and paste the following link into your browser:</p>
|
|
103
|
+
<p style="word-break: break-all; background-color: #f8f9fa; padding: 10px; border-radius: 3px;">
|
|
104
|
+
<a href="${confirmationLink}">${confirmationLink}</a>
|
|
105
|
+
</p>
|
|
106
|
+
|
|
107
|
+
<p style="color: #6c757d; font-size: 14px;">
|
|
108
|
+
This link will expire in ${TOKEN_EXPIRATION_HOURS} hours.
|
|
109
|
+
</p>
|
|
110
|
+
|
|
111
|
+
<p style="color: #6c757d; font-size: 14px;">
|
|
112
|
+
If you did not create an account, please ignore this email. Your account will remain inactive.
|
|
113
|
+
</p>
|
|
114
|
+
</div>
|
|
115
|
+
`
|
|
116
|
+
});
|
|
117
|
+
console.log(`[Nuxt Users] Confirmation email sent to ${email}`);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error(`[Nuxt Users] Failed to send confirmation email to ${email}:`, error);
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
export const confirmUserEmail = async (token, email, options) => {
|
|
124
|
+
const db = await useDb(options);
|
|
125
|
+
const passwordResetTokensTable = options.tables.passwordResetTokens;
|
|
126
|
+
const usersTable = options.tables.users;
|
|
127
|
+
const tokenRecords = await db.sql`
|
|
128
|
+
SELECT * FROM {${passwordResetTokensTable}}
|
|
129
|
+
WHERE email = ${email}
|
|
130
|
+
ORDER BY created_at DESC
|
|
131
|
+
`;
|
|
132
|
+
if (tokenRecords.rows.length === 0) {
|
|
133
|
+
console.log(`[Nuxt Users] No confirmation tokens found for email: ${email}`);
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
let validTokenRecord = null;
|
|
137
|
+
for (const record of tokenRecords.rows) {
|
|
138
|
+
const tokenMatch = await bcrypt.compare(token, record.token);
|
|
139
|
+
if (tokenMatch) {
|
|
140
|
+
validTokenRecord = record;
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (!validTokenRecord || !validTokenRecord.created_at) {
|
|
145
|
+
console.log(`[Nuxt Users] Invalid confirmation token provided for email: ${email}`);
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
const now = /* @__PURE__ */ new Date();
|
|
149
|
+
const currentTimeString = now.toISOString().slice(0, 19).replace("T", " ");
|
|
150
|
+
const [datePart, timePart] = validTokenRecord.created_at.split(/[ T]/);
|
|
151
|
+
const [year, month, day] = datePart.split("-").map(Number);
|
|
152
|
+
const [hour, minute, second] = timePart.split(":").map(Number);
|
|
153
|
+
let expirationHour = hour + TOKEN_EXPIRATION_HOURS;
|
|
154
|
+
let expirationDay = day;
|
|
155
|
+
let expirationMonth = month;
|
|
156
|
+
let expirationYear = year;
|
|
157
|
+
if (expirationHour >= 24) {
|
|
158
|
+
expirationHour -= 24;
|
|
159
|
+
expirationDay++;
|
|
160
|
+
const daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
161
|
+
if (expirationYear % 4 === 0 && (expirationYear % 100 !== 0 || expirationYear % 400 === 0)) {
|
|
162
|
+
daysInMonth[1] = 29;
|
|
163
|
+
}
|
|
164
|
+
if (expirationDay > daysInMonth[expirationMonth - 1]) {
|
|
165
|
+
expirationDay = 1;
|
|
166
|
+
expirationMonth++;
|
|
167
|
+
if (expirationMonth > 12) {
|
|
168
|
+
expirationMonth = 1;
|
|
169
|
+
expirationYear++;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const expirationTimeString = `${expirationYear.toString().padStart(4, "0")}-${expirationMonth.toString().padStart(2, "0")}-${expirationDay.toString().padStart(2, "0")} ${expirationHour.toString().padStart(2, "0")}:${minute.toString().padStart(2, "0")}:${second.toString().padStart(2, "0")}`;
|
|
174
|
+
if (currentTimeString > expirationTimeString) {
|
|
175
|
+
console.log(`[Nuxt Users] Expired confirmation token for email: ${email}`);
|
|
176
|
+
await db.sql`DELETE FROM {${passwordResetTokensTable}} WHERE id = ${validTokenRecord.id}`;
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
await db.sql`
|
|
180
|
+
UPDATE {${usersTable}}
|
|
181
|
+
SET active = true, updated_at = CURRENT_TIMESTAMP
|
|
182
|
+
WHERE email = ${email}
|
|
183
|
+
`;
|
|
184
|
+
await db.sql`DELETE FROM {${passwordResetTokensTable}} WHERE id = ${validTokenRecord.id}`;
|
|
185
|
+
console.log(`[Nuxt Users] Email confirmed and user activated for: ${email}`);
|
|
186
|
+
return true;
|
|
187
|
+
};
|
package/dist/utils.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { g as DatabaseConfig, f as DatabaseType, D as DisplayFieldsProps, L as LoginFormData, c as LoginFormProps, M as ModuleOptions, b as PasswordResetToken, e as Permission, P as PersonalAccessToken, d as ResetPasswordFormProps, R as RuntimeModuleOptions, U as User, a as UserWithoutPassword, h as defaultDisplayFields, i as defaultFieldLabels } from './shared/nuxt-users.
|
|
1
|
+
export { g as DatabaseConfig, f as DatabaseType, D as DisplayFieldsProps, L as LoginFormData, c as LoginFormProps, M as ModuleOptions, b as PasswordResetToken, e as Permission, P as PersonalAccessToken, d as ResetPasswordFormProps, R as RuntimeModuleOptions, U as User, a as UserWithoutPassword, h as defaultDisplayFields, i as defaultFieldLabels } from './shared/nuxt-users.CFq7zOql.mjs';
|
|
2
2
|
|
|
3
3
|
interface PasswordValidationResult {
|
|
4
4
|
isValid: boolean;
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { g as DatabaseConfig, f as DatabaseType, D as DisplayFieldsProps, L as LoginFormData, c as LoginFormProps, M as ModuleOptions, b as PasswordResetToken, e as Permission, P as PersonalAccessToken, d as ResetPasswordFormProps, R as RuntimeModuleOptions, U as User, a as UserWithoutPassword, h as defaultDisplayFields, i as defaultFieldLabels } from './shared/nuxt-users.
|
|
1
|
+
export { g as DatabaseConfig, f as DatabaseType, D as DisplayFieldsProps, L as LoginFormData, c as LoginFormProps, M as ModuleOptions, b as PasswordResetToken, e as Permission, P as PersonalAccessToken, d as ResetPasswordFormProps, R as RuntimeModuleOptions, U as User, a as UserWithoutPassword, h as defaultDisplayFields, i as defaultFieldLabels } from './shared/nuxt-users.CFq7zOql.js';
|
|
2
2
|
|
|
3
3
|
interface PasswordValidationResult {
|
|
4
4
|
isValid: boolean;
|
package/package.json
CHANGED