nuxt-users 1.28.2 → 1.29.1

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 CHANGED
@@ -1,6 +1,61 @@
1
1
  # Changelog
2
2
 
3
3
 
4
+ ## v1.29.1
5
+
6
+ [compare changes](https://github.com/rrd108/nuxt-users/compare/v1.29.0...v1.29.1)
7
+
8
+ ### 🩹 Fixes
9
+
10
+ - Missing server middleware ([50e22d5](https://github.com/rrd108/nuxt-users/commit/50e22d5))
11
+ - Auto whitelist urls on register ([8a05dab](https://github.com/rrd108/nuxt-users/commit/8a05dab))
12
+ - Add missing endpoint ([a0a371c](https://github.com/rrd108/nuxt-users/commit/a0a371c))
13
+ - Auto whitelist /me endpoint ([5453da9](https://github.com/rrd108/nuxt-users/commit/5453da9))
14
+
15
+ ### 🏡 Chore
16
+
17
+ - Lint fixes ([bf5786f](https://github.com/rrd108/nuxt-users/commit/bf5786f))
18
+ - **release:** V1.29.0 ([b468e52](https://github.com/rrd108/nuxt-users/commit/b468e52))
19
+
20
+ ### ✅ Tests
21
+
22
+ - Add integration tests runner ([e6cfbfb](https://github.com/rrd108/nuxt-users/commit/e6cfbfb))
23
+
24
+ ### ❤️ Contributors
25
+
26
+ - Rrd108 ([@rrd108](https://github.com/rrd108))
27
+
28
+ ## v1.29.0
29
+
30
+ [compare changes](https://github.com/rrd108/nuxt-users/compare/v1.28.2...v1.29.0)
31
+
32
+ ### 🚀 Enhancements
33
+
34
+ - Implement register functionality ([8ea4f89](https://github.com/rrd108/nuxt-users/commit/8ea4f89))
35
+
36
+ ### 🩹 Fixes
37
+
38
+ - Lost reactivity for user ([742cd1e](https://github.com/rrd108/nuxt-users/commit/742cd1e))
39
+ - Types and test ([db71fe4](https://github.com/rrd108/nuxt-users/commit/db71fe4))
40
+ - Type annotations ([d4a408b](https://github.com/rrd108/nuxt-users/commit/d4a408b))
41
+ - Missing server middleware ([50e22d5](https://github.com/rrd108/nuxt-users/commit/50e22d5))
42
+ - Auto whitelist urls on register ([8a05dab](https://github.com/rrd108/nuxt-users/commit/8a05dab))
43
+
44
+ ### 🏡 Chore
45
+
46
+ - Remove login from example ([8d4611a](https://github.com/rrd108/nuxt-users/commit/8d4611a))
47
+ - **release:** V1.28.2 ([b1e25cc](https://github.com/rrd108/nuxt-users/commit/b1e25cc))
48
+ - Fix linting issues ([7e45645](https://github.com/rrd108/nuxt-users/commit/7e45645))
49
+ - Lint fixes ([bf5786f](https://github.com/rrd108/nuxt-users/commit/bf5786f))
50
+
51
+ ### ✅ Tests
52
+
53
+ - Add test for register ([ac29f3a](https://github.com/rrd108/nuxt-users/commit/ac29f3a))
54
+
55
+ ### ❤️ Contributors
56
+
57
+ - Rrd108 ([@rrd108](https://github.com/rrd108))
58
+
4
59
  ## v1.28.2
5
60
 
6
61
  [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.CydOpwYH.mjs';
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.CydOpwYH.js';
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-users",
3
3
  "configKey": "nuxtUsers",
4
- "version": "1.28.2",
4
+ "version": "1.29.1",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.0"
package/dist/module.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { defineNuxtModule, createResolver, addPlugin, addImportsDir, addServerImportsDir, addRouteMiddleware, addServerHandler, addComponent } from '@nuxt/kit';
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: [...defaultOptions.auth?.whitelist || [], ...options.auth?.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: [...defaultOptions.auth?.whitelist || [], ...options.auth?.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,5 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
2
+ success: boolean;
3
+ message: string;
4
+ }>>;
5
+ export default _default;
@@ -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,6 @@
1
+ import type { ModuleOptions } from 'nuxt-users/utils';
2
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
3
+ user: Omit<ModuleOptions, "active">;
4
+ message: string;
5
+ }>>;
6
+ export default _default;
@@ -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
+ });
@@ -47,6 +47,10 @@ export default defineEventHandler(async (event) => {
47
47
  return;
48
48
  }
49
49
  }
50
+ if (event.path === `${base}/me`) {
51
+ console.log(`[Nuxt Users] server.middleware.auth.global: Auto-whitelisted /me endpoint for authenticated user ${user.id}`);
52
+ return;
53
+ }
50
54
  if (!hasPermission(user.role, event.path, event.method, options.auth.permissions)) {
51
55
  if (event.path.startsWith("/api/")) {
52
56
  console.log(`[Nuxt Users] server.middleware.auth.global: ${event.path} User ${user.id} with role ${user.role} denied access - API request rejected`);
@@ -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
+ };
@@ -46,7 +46,7 @@ interface RuntimeModuleOptions {
46
46
  /**
47
47
  * Whitelisted routes that do not require authentication
48
48
  * @default ['/login']
49
- * @example ['/login', '/register']
49
+ * @example ['/register']
50
50
  */
51
51
  whitelist?: string[];
52
52
  /**
@@ -46,7 +46,7 @@ interface RuntimeModuleOptions {
46
46
  /**
47
47
  * Whitelisted routes that do not require authentication
48
48
  * @default ['/login']
49
- * @example ['/login', '/register']
49
+ * @example ['/register']
50
50
  */
51
51
  whitelist?: string[];
52
52
  /**
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.CydOpwYH.mjs';
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.CydOpwYH.js';
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-users",
3
- "version": "1.28.2",
3
+ "version": "1.29.1",
4
4
  "description": "A comprehensive user management module for Nuxt 3 and Nuxt 4 applications with authentication, authorization, database support, and CLI tools",
5
5
  "keywords": [
6
6
  "nuxt",
@@ -66,13 +66,14 @@
66
66
  "lint": "eslint .",
67
67
  "lint:fix": "eslint . --fix",
68
68
  "pretest": "node -v | grep -q 'v22' || (echo 'Please use Node.js v22 for testing' && exit 1)",
69
- "test:release": "yarn test:types && yarn test:unit && yarn test:sqlite && yarn build && yarn lint:fix",
69
+ "test:release": "yarn test:types && yarn test:unit && yarn test:integration && yarn test:sqlite && yarn build && yarn lint:fix",
70
70
  "test:types": "yarn dev:prepare && vue-tsc --noEmit && cd playground && vue-tsc --noEmit",
71
71
  "test:unit": "./scripts/test-unit.sh",
72
+ "test:integration": "./scripts/test-integration.sh",
72
73
  "test:sqlite": "./scripts/test-sqlite.sh",
73
74
  "test:mysql": "./scripts/test-mysql.sh",
74
75
  "test:postgresql": "./scripts/test-postgresql.sh",
75
- "test": "yarn test:types && yarn test:unit && yarn test:sqlite && yarn test:mysql && yarn test:postgresql",
76
+ "test": "yarn test:types && yarn test:unit && yarn test:integration && yarn test:sqlite && yarn test:mysql && yarn test:postgresql",
76
77
  "test:watch": "vitest watch",
77
78
  "db:create-users-table": "tsx src/cli/create-users-table.ts",
78
79
  "db:create-user": "tsx src/cli/create-user.ts",