nuxt-users 1.49.2 → 1.50.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/dist/cli.mjs +1 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +1 -1
- package/dist/runtime/constants.d.ts +1 -0
- package/dist/runtime/constants.js +1 -0
- package/dist/runtime/server/api/nuxt-users/[id].patch.js +1 -1
- package/dist/runtime/server/api/nuxt-users/auth/google/callback.get.js +18 -1
- package/dist/runtime/server/api/nuxt-users/auth/google/redirect.get.js +12 -2
- package/dist/runtime/server/api/nuxt-users/me.patch.js +3 -7
- package/dist/runtime/server/api/nuxt-users/password/forgot.post.js +3 -4
- package/dist/runtime/server/api/nuxt-users/password/index.patch.js +2 -1
- package/dist/runtime/server/api/nuxt-users/register.post.js +3 -4
- package/dist/runtime/server/middleware/authorization.server.js +16 -14
- package/dist/runtime/server/plugins/validate-mailer-config.js +3 -0
- package/dist/runtime/server/services/password.js +9 -3
- package/dist/runtime/server/services/registration.js +6 -3
- package/dist/runtime/server/utils/escape-html.d.ts +4 -0
- package/dist/runtime/server/utils/escape-html.js +8 -0
- package/dist/runtime/server/utils/google-oauth.d.ts +1 -1
- package/dist/runtime/server/utils/google-oauth.js +3 -2
- package/dist/runtime/server/utils/index.d.ts +1 -0
- package/dist/runtime/server/utils/index.js +1 -0
- package/dist/runtime/server/utils/site-url.d.ts +10 -0
- package/dist/runtime/server/utils/site-url.js +13 -0
- package/dist/runtime/server/utils/user.d.ts +2 -1
- package/dist/runtime/server/utils/user.js +9 -1
- package/dist/runtime/utils/permissions.js +10 -13
- package/dist/shared/{nuxt-users.DtwPhils.d.mts → nuxt-users.Bday8snh.d.mts} +7 -0
- package/dist/shared/{nuxt-users.DtwPhils.d.ts → nuxt-users.Bday8snh.d.ts} +7 -0
- package/dist/shared/{nuxt-users.KrHIaTTL.mjs → nuxt-users.CcNeTWsL.mjs} +3 -0
- package/dist/utils.d.mts +2 -2
- package/dist/utils.d.ts +2 -2
- package/package.json +5 -5
- package/scripts/deploy-docs.sh +3 -3
- package/scripts/test-integration.sh +11 -0
package/dist/cli.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { defineCommand, runMain } from 'citty';
|
|
3
3
|
import { readFileSync } from 'node:fs';
|
|
4
|
-
import { d as defaultOptions } from './shared/nuxt-users.
|
|
4
|
+
import { d as defaultOptions } from './shared/nuxt-users.CcNeTWsL.mjs';
|
|
5
5
|
import { defu } from 'defu';
|
|
6
6
|
import { runMigrations, getAppliedMigrations } from '../dist/runtime/server/utils/migrate.js';
|
|
7
7
|
import { createUser as createUser$1 } from '../dist/runtime/server/utils/user.js';
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { defineNuxtModule, createResolver, addPlugin, addImportsDir, addServerImportsDir, addServerScanDir, addServerHandler, addServerPlugin, addRouteMiddleware, addComponentsDir } from '@nuxt/kit';
|
|
2
2
|
import { defu } from 'defu';
|
|
3
|
-
import { d as defaultOptions } from './shared/nuxt-users.
|
|
3
|
+
import { d as defaultOptions } from './shared/nuxt-users.CcNeTWsL.mjs';
|
|
4
4
|
export * from '../dist/runtime/server/internal/index.js';
|
|
5
5
|
|
|
6
6
|
const module$1 = defineNuxtModule({
|
|
@@ -12,7 +12,7 @@ export default defineEventHandler(async (event) => {
|
|
|
12
12
|
});
|
|
13
13
|
}
|
|
14
14
|
const body = await readBody(event);
|
|
15
|
-
const currentUser = event.context.user;
|
|
15
|
+
const currentUser = event.context.nuxtUsers?.user;
|
|
16
16
|
if (currentUser && currentUser.id === userId && typeof body.active !== "undefined") {
|
|
17
17
|
throw createError({
|
|
18
18
|
statusCode: 403,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { defineEventHandler, sendRedirect, getQuery, setCookie, getCookie, deleteCookie } from "h3";
|
|
2
3
|
import { useRuntimeConfig } from "#imports";
|
|
3
4
|
import {
|
|
4
5
|
createGoogleOAuth2Client,
|
|
@@ -6,6 +7,7 @@ import {
|
|
|
6
7
|
findOrCreateGoogleUser,
|
|
7
8
|
createAuthTokenForUser
|
|
8
9
|
} from "../../../../utils/index.js";
|
|
10
|
+
import { OAUTH_STATE_COOKIE } from "../../../../../constants.js";
|
|
9
11
|
export default defineEventHandler(async (event) => {
|
|
10
12
|
const { nuxtUsers } = useRuntimeConfig();
|
|
11
13
|
const options = nuxtUsers;
|
|
@@ -13,6 +15,13 @@ export default defineEventHandler(async (event) => {
|
|
|
13
15
|
if (!options.auth.google) {
|
|
14
16
|
return sendRedirect(event, "/login?error=oauth_not_configured");
|
|
15
17
|
}
|
|
18
|
+
const expectedState = getCookie(event, OAUTH_STATE_COOKIE);
|
|
19
|
+
deleteCookie(event, OAUTH_STATE_COOKIE, { path: "/" });
|
|
20
|
+
if (!isValidState(expectedState, query.state)) {
|
|
21
|
+
console.error("[Nuxt Users] Invalid or missing OAuth state in callback");
|
|
22
|
+
const errorRedirect = options.auth.google.errorRedirect || "/login?error=oauth_failed";
|
|
23
|
+
return sendRedirect(event, errorRedirect);
|
|
24
|
+
}
|
|
16
25
|
if (query.error) {
|
|
17
26
|
console.error("[Nuxt Users] Google OAuth error:", query.error);
|
|
18
27
|
const errorRedirect = options.auth.google.errorRedirect || "/login?error=oauth_failed";
|
|
@@ -60,6 +69,14 @@ export default defineEventHandler(async (event) => {
|
|
|
60
69
|
return sendRedirect(event, errorRedirect);
|
|
61
70
|
}
|
|
62
71
|
});
|
|
72
|
+
const isValidState = (expected, received) => {
|
|
73
|
+
if (!expected || typeof received !== "string") {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
const expectedBuffer = Buffer.from(expected);
|
|
77
|
+
const receivedBuffer = Buffer.from(received);
|
|
78
|
+
return expectedBuffer.length === receivedBuffer.length && crypto.timingSafeEqual(expectedBuffer, receivedBuffer);
|
|
79
|
+
};
|
|
63
80
|
const getRequestURL = (event) => {
|
|
64
81
|
const headers = event.node.req.headers;
|
|
65
82
|
const host = headers.host || headers[":authority"];
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { defineEventHandler, sendRedirect, createError, setCookie } from "h3";
|
|
2
3
|
import { useRuntimeConfig } from "#imports";
|
|
3
4
|
import { createGoogleOAuth2Client, getGoogleAuthUrl } from "../../../../utils/index.js";
|
|
5
|
+
import { OAUTH_STATE_COOKIE } from "../../../../../constants.js";
|
|
4
6
|
export default defineEventHandler(async (event) => {
|
|
5
7
|
const { nuxtUsers } = useRuntimeConfig();
|
|
6
8
|
const options = nuxtUsers;
|
|
@@ -21,7 +23,15 @@ export default defineEventHandler(async (event) => {
|
|
|
21
23
|
const callbackPath = options.auth.google.callbackUrl || `${options.apiBasePath}/auth/google/callback`;
|
|
22
24
|
const callbackUrl = `${baseUrl}${callbackPath}`;
|
|
23
25
|
const oauth2Client = createGoogleOAuth2Client(options.auth.google, callbackUrl);
|
|
24
|
-
const
|
|
26
|
+
const state = crypto.randomBytes(32).toString("hex");
|
|
27
|
+
setCookie(event, OAUTH_STATE_COOKIE, state, {
|
|
28
|
+
httpOnly: true,
|
|
29
|
+
secure: process.env.NODE_ENV === "production",
|
|
30
|
+
sameSite: "lax",
|
|
31
|
+
path: "/",
|
|
32
|
+
maxAge: 10 * 60
|
|
33
|
+
});
|
|
34
|
+
const authUrl = getGoogleAuthUrl(oauth2Client, options.auth.google, state);
|
|
25
35
|
return sendRedirect(event, authUrl);
|
|
26
36
|
} catch (error) {
|
|
27
37
|
console.error("[Nuxt Users] Google OAuth redirect error:", error);
|
|
@@ -4,7 +4,7 @@ import { updateUser } from "../../utils/index.js";
|
|
|
4
4
|
export default defineEventHandler(async (event) => {
|
|
5
5
|
const { nuxtUsers } = useRuntimeConfig();
|
|
6
6
|
const options = nuxtUsers;
|
|
7
|
-
const user = event.context.user;
|
|
7
|
+
const user = event.context.nuxtUsers?.user;
|
|
8
8
|
if (!user) {
|
|
9
9
|
throw createError({
|
|
10
10
|
statusCode: 401,
|
|
@@ -12,12 +12,8 @@ export default defineEventHandler(async (event) => {
|
|
|
12
12
|
});
|
|
13
13
|
}
|
|
14
14
|
const body = await readBody(event);
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
if (body.active) {
|
|
19
|
-
delete body.active;
|
|
20
|
-
}
|
|
15
|
+
delete body.role;
|
|
16
|
+
delete body.active;
|
|
21
17
|
try {
|
|
22
18
|
const updatedUser = await updateUser(user.id, body, options);
|
|
23
19
|
return { user: updatedUser };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { defineEventHandler, readBody, createError
|
|
1
|
+
import { defineEventHandler, readBody, createError } from "h3";
|
|
2
2
|
import { sendPasswordResetLink } from "../../../services/password.js";
|
|
3
|
+
import { getSiteUrl } from "../../../utils/site-url.js";
|
|
3
4
|
import { useRuntimeConfig } from "#imports";
|
|
4
5
|
export default defineEventHandler(async (event) => {
|
|
5
6
|
const { nuxtUsers } = useRuntimeConfig();
|
|
@@ -13,9 +14,7 @@ export default defineEventHandler(async (event) => {
|
|
|
13
14
|
});
|
|
14
15
|
}
|
|
15
16
|
try {
|
|
16
|
-
const
|
|
17
|
-
const protocol = getHeader(event, "x-forwarded-proto") || "http";
|
|
18
|
-
const baseUrl = host ? `${protocol}://${host}` : void 0;
|
|
17
|
+
const baseUrl = getSiteUrl(event, options);
|
|
19
18
|
await sendPasswordResetLink(email, options, baseUrl);
|
|
20
19
|
return { message: "If a user with that email exists, a password reset link has been sent." };
|
|
21
20
|
} catch (error) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createError, defineEventHandler, getCookie, readBody } from "h3";
|
|
2
2
|
import bcrypt from "bcrypt";
|
|
3
3
|
import { useRuntimeConfig } from "#imports";
|
|
4
|
-
import { getCurrentUserFromToken, updateUserPassword } from "../../../utils/index.js";
|
|
4
|
+
import { getCurrentUserFromToken, updateUserPassword, revokeUserTokens } from "../../../utils/index.js";
|
|
5
5
|
import { validatePassword, getPasswordValidationOptions } from "../../../../../utils";
|
|
6
6
|
export default defineEventHandler(async (event) => {
|
|
7
7
|
const { nuxtUsers } = useRuntimeConfig();
|
|
@@ -56,5 +56,6 @@ export default defineEventHandler(async (event) => {
|
|
|
56
56
|
});
|
|
57
57
|
}
|
|
58
58
|
await updateUserPassword(user.email, newPassword, options);
|
|
59
|
+
await revokeUserTokens(user.id, options, token);
|
|
59
60
|
return { message: "Password updated successfully" };
|
|
60
61
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { createError, defineEventHandler, readBody
|
|
1
|
+
import { createError, defineEventHandler, readBody } from "h3";
|
|
2
2
|
import { useRuntimeConfig } from "#imports";
|
|
3
3
|
import { registerUser } from "../../services/registration.js";
|
|
4
|
+
import { getSiteUrl } from "../../utils/site-url.js";
|
|
4
5
|
export default defineEventHandler(async (event) => {
|
|
5
6
|
try {
|
|
6
7
|
const { nuxtUsers } = useRuntimeConfig();
|
|
@@ -25,9 +26,7 @@ export default defineEventHandler(async (event) => {
|
|
|
25
26
|
statusMessage: "Name cannot be empty"
|
|
26
27
|
});
|
|
27
28
|
}
|
|
28
|
-
const
|
|
29
|
-
const protocol = getHeader(event, "x-forwarded-proto") || "http";
|
|
30
|
-
const baseUrl = `${protocol}://${host}`;
|
|
29
|
+
const baseUrl = getSiteUrl(event, options);
|
|
31
30
|
const result = await registerUser(
|
|
32
31
|
{
|
|
33
32
|
email: body.email.toLowerCase().trim(),
|
|
@@ -7,28 +7,30 @@ export default defineEventHandler(async (event) => {
|
|
|
7
7
|
const { nuxtUsers } = useRuntimeConfig();
|
|
8
8
|
const options = nuxtUsers;
|
|
9
9
|
const base = options.apiBasePath || "/api/nuxt-users";
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
|
|
10
|
+
const path = event.path.split("?")[0];
|
|
11
|
+
const isApiRoute = path.startsWith("/api/") || path === base || path.startsWith(`${base}/`);
|
|
12
|
+
const isMeEndpoint = path === `${base}/me`;
|
|
13
|
+
const isStaticFile = !isApiRoute && path.includes(".");
|
|
14
|
+
const isNuxtInternal = !isApiRoute && path.startsWith("/_");
|
|
15
|
+
if (isStaticFile || isNuxtInternal) {
|
|
14
16
|
return;
|
|
15
17
|
}
|
|
16
18
|
const publicAuthPaths = [...PUBLIC_PAGES];
|
|
17
19
|
if (options.passwordResetUrl && options.passwordResetUrl !== "/reset-password") {
|
|
18
20
|
publicAuthPaths.push(options.passwordResetUrl);
|
|
19
21
|
}
|
|
20
|
-
if (publicAuthPaths.includes(
|
|
21
|
-
console.debug(`[Nuxt Users] authorization: NO_AUTH_PATH: ${
|
|
22
|
+
if (publicAuthPaths.includes(path)) {
|
|
23
|
+
console.debug(`[Nuxt Users] authorization: NO_AUTH_PATH: ${path}`);
|
|
22
24
|
return;
|
|
23
25
|
}
|
|
24
26
|
for (const endpoint of PUBLIC_API_ENDPOINTS) {
|
|
25
|
-
if (
|
|
26
|
-
console.debug(`[Nuxt Users] authorization: Public API endpoint ${
|
|
27
|
+
if (path === `${base}${endpoint.path}` && endpoint.methods.includes(event.method)) {
|
|
28
|
+
console.debug(`[Nuxt Users] authorization: Public API endpoint ${path} (${event.method})`);
|
|
27
29
|
return;
|
|
28
30
|
}
|
|
29
31
|
}
|
|
30
|
-
if (isWhitelisted(
|
|
31
|
-
console.debug(`[Nuxt Users] authorization: whitelisted: ${
|
|
32
|
+
if (isWhitelisted(path, options.auth.whitelist)) {
|
|
33
|
+
console.debug(`[Nuxt Users] authorization: whitelisted: ${path}`);
|
|
32
34
|
return;
|
|
33
35
|
}
|
|
34
36
|
const token = getCookie(event, "auth_token");
|
|
@@ -45,14 +47,14 @@ export default defineEventHandler(async (event) => {
|
|
|
45
47
|
event.context.nuxtUsers = event.context.nuxtUsers || {};
|
|
46
48
|
event.context.nuxtUsers.user = user;
|
|
47
49
|
for (const endpoint of AUTHENTICATED_AUTO_ACCESS_ENDPOINTS) {
|
|
48
|
-
if (
|
|
49
|
-
console.debug(`[Nuxt Users] authorization: Auto-whitelisted endpoint ${
|
|
50
|
+
if (path === `${base}${endpoint.path}` && endpoint.methods.includes(event.method)) {
|
|
51
|
+
console.debug(`[Nuxt Users] authorization: Auto-whitelisted endpoint ${path} for authenticated user ${user.id}`);
|
|
50
52
|
return;
|
|
51
53
|
}
|
|
52
54
|
}
|
|
53
|
-
if (!hasPermission(user.role,
|
|
55
|
+
if (!hasPermission(user.role, path, event.method, options.auth.permissions)) {
|
|
54
56
|
return handleUnauthorizedRequest(event, user, isApiRoute);
|
|
55
57
|
}
|
|
56
|
-
console.debug(`[Nuxt Users] authorization: Authenticated request to ${
|
|
58
|
+
console.debug(`[Nuxt Users] authorization: Authenticated request to ${path} for ${user.id} with role ${user.role}`);
|
|
57
59
|
return;
|
|
58
60
|
});
|
|
@@ -29,5 +29,8 @@ const plugin = () => {
|
|
|
29
29
|
return;
|
|
30
30
|
}
|
|
31
31
|
validateMailerConfig(options);
|
|
32
|
+
if (process.env.NODE_ENV === "production" && !options.siteUrl) {
|
|
33
|
+
console.warn("[Nuxt Users] siteUrl is not configured: password reset and email confirmation requests will fail. Set nuxtUsers.siteUrl (or NUXT_NUXT_USERS_SITE_URL) to your public app URL.");
|
|
34
|
+
}
|
|
32
35
|
};
|
|
33
36
|
export default plugin;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createTransport } from "nodemailer";
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
3
|
import bcrypt from "bcrypt";
|
|
4
|
-
import { findUserByEmail, updateUserPassword, useDb } from "../utils/index.js";
|
|
4
|
+
import { findUserByEmail, updateUserPassword, revokeUserTokens, useDb } from "../utils/index.js";
|
|
5
|
+
import { escapeHtml } from "../utils/escape-html.js";
|
|
5
6
|
const TOKEN_EXPIRATION_HOURS = 1;
|
|
6
7
|
export const sendPasswordResetLink = async (email, options, baseUrl) => {
|
|
7
8
|
const user = await findUserByEmail(email, options);
|
|
@@ -35,6 +36,7 @@ export const sendPasswordResetLink = async (email, options, baseUrl) => {
|
|
|
35
36
|
resetUrl.searchParams.set("token", token);
|
|
36
37
|
resetUrl.searchParams.set("email", email);
|
|
37
38
|
const resetLink = resetUrl.toString();
|
|
39
|
+
const safeLink = escapeHtml(resetLink);
|
|
38
40
|
try {
|
|
39
41
|
await transporter.sendMail({
|
|
40
42
|
from: options.mailer.defaults?.from || '"Nuxt Users" <noreply@example.com>',
|
|
@@ -51,7 +53,7 @@ If you did not request this password reset, please ignore this email.`,
|
|
|
51
53
|
<p>You have requested to reset your password. Please click the button below to set a new password:</p>
|
|
52
54
|
|
|
53
55
|
<div style="text-align: center; margin: 30px 0;">
|
|
54
|
-
<a href="${
|
|
56
|
+
<a href="${safeLink}"
|
|
55
57
|
style="background-color: #007bff; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px; display: inline-block;">
|
|
56
58
|
Reset Password
|
|
57
59
|
</a>
|
|
@@ -59,7 +61,7 @@ If you did not request this password reset, please ignore this email.`,
|
|
|
59
61
|
|
|
60
62
|
<p>Or copy and paste the following link into your browser:</p>
|
|
61
63
|
<p style="word-break: break-all; background-color: #f8f9fa; padding: 10px; border-radius: 3px;">
|
|
62
|
-
<a href="${
|
|
64
|
+
<a href="${safeLink}">${safeLink}</a>
|
|
63
65
|
</p>
|
|
64
66
|
|
|
65
67
|
<p style="color: #6c757d; font-size: 14px;">
|
|
@@ -130,6 +132,10 @@ export const resetPassword = async (token, email, newPassword, options) => {
|
|
|
130
132
|
return false;
|
|
131
133
|
}
|
|
132
134
|
await updateUserPassword(email, newPassword, options);
|
|
135
|
+
const user = await findUserByEmail(email, options);
|
|
136
|
+
if (user) {
|
|
137
|
+
await revokeUserTokens(user.id, options);
|
|
138
|
+
}
|
|
133
139
|
await db.sql`DELETE FROM {${passwordResetTokensTable}} WHERE email = ${email}`;
|
|
134
140
|
console.log(`[Nuxt Users] Password reset successful for email: ${email}`);
|
|
135
141
|
return true;
|
|
@@ -2,6 +2,7 @@ import { createTransport } from "nodemailer";
|
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
3
|
import bcrypt from "bcrypt";
|
|
4
4
|
import { findUserByEmail, useDb, USER_EMAIL_ALREADY_EXISTS_MESSAGE } from "../utils/index.js";
|
|
5
|
+
import { escapeHtml } from "../utils/escape-html.js";
|
|
5
6
|
import { validatePassword, getPasswordValidationOptions } from "nuxt-users/utils";
|
|
6
7
|
const TOKEN_EXPIRATION_HOURS = 24;
|
|
7
8
|
export const registerUser = async (userData, options, baseUrl) => {
|
|
@@ -76,6 +77,8 @@ export const sendConfirmationEmail = async (email, name, token, options, baseUrl
|
|
|
76
77
|
confirmationUrl.searchParams.set("token", token);
|
|
77
78
|
confirmationUrl.searchParams.set("email", email);
|
|
78
79
|
const confirmationLink = confirmationUrl.toString();
|
|
80
|
+
const safeName = escapeHtml(name);
|
|
81
|
+
const safeLink = escapeHtml(confirmationLink);
|
|
79
82
|
try {
|
|
80
83
|
await transporter.sendMail({
|
|
81
84
|
from: options.mailer.defaults?.from || '"Nuxt Users" <noreply@example.com>',
|
|
@@ -92,11 +95,11 @@ This link will expire in ${TOKEN_EXPIRATION_HOURS} hours.
|
|
|
92
95
|
If you did not create an account, please ignore this email.`,
|
|
93
96
|
html: `
|
|
94
97
|
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
|
95
|
-
<h2>Welcome, ${
|
|
98
|
+
<h2>Welcome, ${safeName}!</h2>
|
|
96
99
|
<p>Thank you for registering! Please click the button below to confirm your email address and activate your account:</p>
|
|
97
100
|
|
|
98
101
|
<div style="text-align: center; margin: 30px 0;">
|
|
99
|
-
<a href="${
|
|
102
|
+
<a href="${safeLink}"
|
|
100
103
|
style="background-color: #28a745; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px; display: inline-block;">
|
|
101
104
|
Confirm Email Address
|
|
102
105
|
</a>
|
|
@@ -104,7 +107,7 @@ If you did not create an account, please ignore this email.`,
|
|
|
104
107
|
|
|
105
108
|
<p>Or copy and paste the following link into your browser:</p>
|
|
106
109
|
<p style="word-break: break-all; background-color: #f8f9fa; padding: 10px; border-radius: 3px;">
|
|
107
|
-
<a href="${
|
|
110
|
+
<a href="${safeLink}">${safeLink}</a>
|
|
108
111
|
</p>
|
|
109
112
|
|
|
110
113
|
<p style="color: #6c757d; font-size: 14px;">
|
|
@@ -14,7 +14,7 @@ export declare function createGoogleOAuth2Client(options: GoogleOAuthOptions, ca
|
|
|
14
14
|
/**
|
|
15
15
|
* Generate authorization URL for Google OAuth
|
|
16
16
|
*/
|
|
17
|
-
export declare function getGoogleAuthUrl(oauth2Client: OAuth2Client, options: GoogleOAuthOptions): string;
|
|
17
|
+
export declare function getGoogleAuthUrl(oauth2Client: OAuth2Client, options: GoogleOAuthOptions, state?: string): string;
|
|
18
18
|
/**
|
|
19
19
|
* Exchange authorization code for tokens and get user info
|
|
20
20
|
*/
|
|
@@ -9,12 +9,13 @@ export function createGoogleOAuth2Client(options, callbackUrl) {
|
|
|
9
9
|
callbackUrl
|
|
10
10
|
);
|
|
11
11
|
}
|
|
12
|
-
export function getGoogleAuthUrl(oauth2Client, options) {
|
|
12
|
+
export function getGoogleAuthUrl(oauth2Client, options, state) {
|
|
13
13
|
const scopes = options.scopes || ["openid", "profile", "email"];
|
|
14
14
|
return oauth2Client.generateAuthUrl({
|
|
15
15
|
access_type: "offline",
|
|
16
16
|
scope: scopes,
|
|
17
|
-
include_granted_scopes: true
|
|
17
|
+
include_granted_scopes: true,
|
|
18
|
+
...state && { state }
|
|
18
19
|
});
|
|
19
20
|
}
|
|
20
21
|
export async function getGoogleUserFromCode(oauth2Client, code) {
|
|
@@ -8,3 +8,4 @@ export { createMigrationsTable } from './create-migrations-table.js';
|
|
|
8
8
|
export { runMigrations, getAppliedMigrations } from './migrate.js';
|
|
9
9
|
export { createGoogleOAuth2Client, getGoogleAuthUrl, getGoogleUserFromCode, findOrCreateGoogleUser, createAuthTokenForUser, generateSecurePassword } from './google-oauth.js';
|
|
10
10
|
export { handleUnauthenticatedRequest, handleInvalidToken, handleUnauthorizedRequest } from './authorization.js';
|
|
11
|
+
export { getSiteUrl, SITE_URL_REQUIRED_MESSAGE } from './site-url.js';
|
|
@@ -8,3 +8,4 @@ export { createMigrationsTable } from "./create-migrations-table.js";
|
|
|
8
8
|
export { runMigrations, getAppliedMigrations } from "./migrate.js";
|
|
9
9
|
export { createGoogleOAuth2Client, getGoogleAuthUrl, getGoogleUserFromCode, findOrCreateGoogleUser, createAuthTokenForUser, generateSecurePassword } from "./google-oauth.js";
|
|
10
10
|
export { handleUnauthenticatedRequest, handleInvalidToken, handleUnauthorizedRequest } from "./authorization.js";
|
|
11
|
+
export { getSiteUrl, SITE_URL_REQUIRED_MESSAGE } from "./site-url.js";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type H3Event } from 'h3';
|
|
2
|
+
import type { ModuleOptions } from 'nuxt-users/utils';
|
|
3
|
+
export declare const SITE_URL_REQUIRED_MESSAGE = "[Nuxt Users] siteUrl is not configured. Set nuxtUsers.siteUrl (or NUXT_NUXT_USERS_SITE_URL) to your public app URL, e.g. https://example.com, so email links are not built from the untrusted Host header.";
|
|
4
|
+
/**
|
|
5
|
+
* Resolves the base URL used for links in emails (password reset, email confirmation).
|
|
6
|
+
*
|
|
7
|
+
* The request's Host / X-Forwarded-Proto headers are attacker-controlled, so they are only
|
|
8
|
+
* used as a fallback outside production. In production `siteUrl` must be configured.
|
|
9
|
+
*/
|
|
10
|
+
export declare const getSiteUrl: (event: H3Event, options: ModuleOptions) => string;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { getHeader } from "h3";
|
|
2
|
+
export const SITE_URL_REQUIRED_MESSAGE = "[Nuxt Users] siteUrl is not configured. Set nuxtUsers.siteUrl (or NUXT_NUXT_USERS_SITE_URL) to your public app URL, e.g. https://example.com, so email links are not built from the untrusted Host header.";
|
|
3
|
+
export const getSiteUrl = (event, options) => {
|
|
4
|
+
if (options.siteUrl) {
|
|
5
|
+
return options.siteUrl.replace(/\/+$/, "");
|
|
6
|
+
}
|
|
7
|
+
if (process.env.NODE_ENV === "production") {
|
|
8
|
+
throw new Error(SITE_URL_REQUIRED_MESSAGE);
|
|
9
|
+
}
|
|
10
|
+
const host = getHeader(event, "host") || "localhost:3000";
|
|
11
|
+
const protocol = getHeader(event, "x-forwarded-proto") || "http";
|
|
12
|
+
return `${protocol}://${host}`;
|
|
13
|
+
};
|
|
@@ -59,8 +59,9 @@ export declare const cleanupPersonalAccessTokens: (options: ModuleOptions, inclu
|
|
|
59
59
|
}>;
|
|
60
60
|
/**
|
|
61
61
|
* Revokes all tokens for a specific user.
|
|
62
|
+
* Pass `exceptToken` to keep one session (e.g. the one that just changed the password).
|
|
62
63
|
*/
|
|
63
|
-
export declare const revokeUserTokens: (userId: number, options: ModuleOptions) => Promise<void>;
|
|
64
|
+
export declare const revokeUserTokens: (userId: number, options: ModuleOptions, exceptToken?: string) => Promise<void>;
|
|
64
65
|
/**
|
|
65
66
|
* Gets the last login time for a user based on their most recent personal access token creation.
|
|
66
67
|
* This represents when the user last successfully logged in.
|
|
@@ -226,9 +226,17 @@ export const cleanupPersonalAccessTokens = async (options, includeNoExpiration =
|
|
|
226
226
|
console.log(`[Nuxt Users] Token cleanup completed: ${expiredCount} expired + ${noExpirationCount} without expiration = ${totalCount} total tokens removed.`);
|
|
227
227
|
return { expiredCount, noExpirationCount, totalCount };
|
|
228
228
|
};
|
|
229
|
-
export const revokeUserTokens = async (userId, options) => {
|
|
229
|
+
export const revokeUserTokens = async (userId, options, exceptToken) => {
|
|
230
230
|
const db = await useDb(options);
|
|
231
231
|
const personalAccessTokensTable = options.tables.personalAccessTokens;
|
|
232
|
+
if (exceptToken) {
|
|
233
|
+
await db.sql`
|
|
234
|
+
DELETE FROM {${personalAccessTokensTable}}
|
|
235
|
+
WHERE tokenable_type = 'user' AND tokenable_id = ${userId} AND token <> ${exceptToken}
|
|
236
|
+
`;
|
|
237
|
+
console.log(`[Nuxt Users] Other tokens revoked for user ID: ${userId}`);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
232
240
|
await db.sql`
|
|
233
241
|
DELETE FROM {${personalAccessTokensTable}}
|
|
234
242
|
WHERE tokenable_type = 'user' AND tokenable_id = ${userId}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
const patternToRegexSource = (pattern) => pattern.split(/(\*\*|\*)/).map((part) => {
|
|
2
|
+
if (part === "**") return ".*";
|
|
3
|
+
if (part === "*") return "[^/]*";
|
|
4
|
+
return part.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
5
|
+
}).join("");
|
|
1
6
|
export const pathMatchesPattern = (path, pattern) => {
|
|
2
7
|
if (path === pattern) {
|
|
3
8
|
return true;
|
|
@@ -5,21 +10,13 @@ export const pathMatchesPattern = (path, pattern) => {
|
|
|
5
10
|
if (pattern === "*") {
|
|
6
11
|
return true;
|
|
7
12
|
}
|
|
8
|
-
if (
|
|
9
|
-
|
|
10
|
-
const regex = new RegExp(`^${regexPattern}`);
|
|
11
|
-
return regex.test(path);
|
|
12
|
-
}
|
|
13
|
-
if (pattern.endsWith("/*")) {
|
|
14
|
-
const basePattern = pattern.slice(0, -2);
|
|
15
|
-
return path === basePattern || path.startsWith(basePattern + "/");
|
|
13
|
+
if (!pattern.includes("*")) {
|
|
14
|
+
return false;
|
|
16
15
|
}
|
|
17
|
-
if (pattern.
|
|
18
|
-
|
|
19
|
-
const regex = new RegExp(`^${regexPattern}`);
|
|
20
|
-
return regex.test(path);
|
|
16
|
+
if (pattern.endsWith("/*") && !pattern.endsWith("/**")) {
|
|
17
|
+
return new RegExp(`^${patternToRegexSource(pattern.slice(0, -2))}(?:/.*)?$`).test(path);
|
|
21
18
|
}
|
|
22
|
-
return
|
|
19
|
+
return new RegExp(`^${patternToRegexSource(pattern)}$`).test(path);
|
|
23
20
|
};
|
|
24
21
|
export const hasPermission = (userRole, path, method, permissions) => {
|
|
25
22
|
const safeMethods = ["OPTIONS", "HEAD", "TRACE"];
|
|
@@ -98,6 +98,12 @@ interface RuntimeModuleOptions {
|
|
|
98
98
|
* Uses nodemailer
|
|
99
99
|
*/
|
|
100
100
|
mailer?: MailerOptions;
|
|
101
|
+
/**
|
|
102
|
+
* Public base URL of the app (e.g. 'https://example.com'), used to build links in emails
|
|
103
|
+
* (password reset, email confirmation). Required in production: the request Host header
|
|
104
|
+
* is attacker-controlled and is only used as a fallback in development.
|
|
105
|
+
*/
|
|
106
|
+
siteUrl?: string;
|
|
101
107
|
/**
|
|
102
108
|
* URL path for password reset page
|
|
103
109
|
* @default '/reset-password'
|
|
@@ -225,6 +231,7 @@ interface ModuleOptions {
|
|
|
225
231
|
passwordResetTokens: string;
|
|
226
232
|
};
|
|
227
233
|
mailer?: MailerOptions;
|
|
234
|
+
siteUrl?: string;
|
|
228
235
|
passwordResetUrl: string;
|
|
229
236
|
emailConfirmationUrl: string;
|
|
230
237
|
auth: {
|
|
@@ -98,6 +98,12 @@ interface RuntimeModuleOptions {
|
|
|
98
98
|
* Uses nodemailer
|
|
99
99
|
*/
|
|
100
100
|
mailer?: MailerOptions;
|
|
101
|
+
/**
|
|
102
|
+
* Public base URL of the app (e.g. 'https://example.com'), used to build links in emails
|
|
103
|
+
* (password reset, email confirmation). Required in production: the request Host header
|
|
104
|
+
* is attacker-controlled and is only used as a fallback in development.
|
|
105
|
+
*/
|
|
106
|
+
siteUrl?: string;
|
|
101
107
|
/**
|
|
102
108
|
* URL path for password reset page
|
|
103
109
|
* @default '/reset-password'
|
|
@@ -225,6 +231,7 @@ interface ModuleOptions {
|
|
|
225
231
|
passwordResetTokens: string;
|
|
226
232
|
};
|
|
227
233
|
mailer?: MailerOptions;
|
|
234
|
+
siteUrl?: string;
|
|
228
235
|
passwordResetUrl: string;
|
|
229
236
|
emailConfirmationUrl: string;
|
|
230
237
|
auth: {
|
|
@@ -24,6 +24,9 @@ const defaultOptions = {
|
|
|
24
24
|
from: '"Nuxt Users Module" <noreply@example.com>'
|
|
25
25
|
}
|
|
26
26
|
},
|
|
27
|
+
// Empty by default so NUXT_NUXT_USERS_SITE_URL can override it (Nuxt only applies env vars to existing keys);
|
|
28
|
+
// empty falls back to the request host in development and is rejected in production
|
|
29
|
+
siteUrl: "",
|
|
27
30
|
passwordResetUrl: "/reset-password",
|
|
28
31
|
emailConfirmationUrl: "/email-confirmation",
|
|
29
32
|
auth: {
|
package/dist/utils.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { P as Permission } from './shared/nuxt-users.
|
|
2
|
-
export { D as DatabaseConfig, a as DatabaseType, b as DisplayFieldsProps, G as GoogleOAuthOptions, L as LocaleMessages, c as LocaleOptions, d as LoginFormData, e as LoginFormProps, M as ModuleOptions, f as PasswordResetToken, g as PersonalAccessToken, h as ResetPasswordFormProps, R as RuntimeModuleOptions, U as User, i as UserWithoutPassword, j as defaultDisplayFields, k as defaultFieldLabels } from './shared/nuxt-users.
|
|
1
|
+
import { P as Permission } from './shared/nuxt-users.Bday8snh.mjs';
|
|
2
|
+
export { D as DatabaseConfig, a as DatabaseType, b as DisplayFieldsProps, G as GoogleOAuthOptions, L as LocaleMessages, c as LocaleOptions, d as LoginFormData, e as LoginFormProps, M as ModuleOptions, f as PasswordResetToken, g as PersonalAccessToken, h as ResetPasswordFormProps, R as RuntimeModuleOptions, U as User, i as UserWithoutPassword, j as defaultDisplayFields, k as defaultFieldLabels } from './shared/nuxt-users.Bday8snh.mjs';
|
|
3
3
|
export { defaultLocaleMessages, en, hu, huFormal } from '../dist/runtime/locales/index.js';
|
|
4
4
|
export { getNestedValue, getTranslation } from '../dist/runtime/utils/locale.js';
|
|
5
5
|
export { deepMerge } from '../dist/runtime/utils/deep-merge.js';
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { P as Permission } from './shared/nuxt-users.
|
|
2
|
-
export { D as DatabaseConfig, a as DatabaseType, b as DisplayFieldsProps, G as GoogleOAuthOptions, L as LocaleMessages, c as LocaleOptions, d as LoginFormData, e as LoginFormProps, M as ModuleOptions, f as PasswordResetToken, g as PersonalAccessToken, h as ResetPasswordFormProps, R as RuntimeModuleOptions, U as User, i as UserWithoutPassword, j as defaultDisplayFields, k as defaultFieldLabels } from './shared/nuxt-users.
|
|
1
|
+
import { P as Permission } from './shared/nuxt-users.Bday8snh.js';
|
|
2
|
+
export { D as DatabaseConfig, a as DatabaseType, b as DisplayFieldsProps, G as GoogleOAuthOptions, L as LocaleMessages, c as LocaleOptions, d as LoginFormData, e as LoginFormProps, M as ModuleOptions, f as PasswordResetToken, g as PersonalAccessToken, h as ResetPasswordFormProps, R as RuntimeModuleOptions, U as User, i as UserWithoutPassword, j as defaultDisplayFields, k as defaultFieldLabels } from './shared/nuxt-users.Bday8snh.js';
|
|
3
3
|
export { defaultLocaleMessages, en, hu, huFormal } from '../dist/runtime/locales/index.js';
|
|
4
4
|
export { getNestedValue, getTranslation } from '../dist/runtime/utils/locale.js';
|
|
5
5
|
export { deepMerge } from '../dist/runtime/utils/deep-merge.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxt-users",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.50.0",
|
|
4
4
|
"description": "A comprehensive user management module for Nuxt applications with authentication, authorization, database support, and CLI tools",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"nuxt",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"scripts": {
|
|
56
56
|
"prepack": "nuxt-module-build build && node scripts/post-build.js",
|
|
57
57
|
"postinstall": "node -e \"try { require('fs').chmodSync('./dist/cli.mjs', '755') } catch(e) {}\" && node scripts/check-peer-dependencies.js",
|
|
58
|
-
"predev": "node -v | grep -
|
|
58
|
+
"predev": "node -v | grep -qE '^v(22|24)\\.' || (echo 'Please use Node.js v22 or v24 for development' && exit 1)",
|
|
59
59
|
"dev": "yarn dev:prepare && nuxi dev playground",
|
|
60
60
|
"dev:build": "nuxi build playground",
|
|
61
61
|
"dev:prepare": "yarn nuxt-module-build build --stub && yarn nuxt-module-build prepare && nuxi prepare playground",
|
|
@@ -65,13 +65,13 @@
|
|
|
65
65
|
"check:locale-consistency": "tsx scripts/check-locale-consistency.ts",
|
|
66
66
|
"check:npm_token": "dotenv -- sh -c 'if [ -z \"$NPM_TOKEN\" ]; then echo \"❌ NPM_TOKEN environment variable is not set in .env file. Please set it to your npm token.\"; exit 1; fi'",
|
|
67
67
|
"check:npm_auth": "dotenv -- sh -c 'npm whoami --registry https://registry.npmjs.org/ --//registry.npmjs.org/:_authToken=$NPM_TOKEN'",
|
|
68
|
-
"release:publish:dry": "
|
|
68
|
+
"release:publish:dry": "npm pack --dry-run --ignore-scripts",
|
|
69
69
|
"release:preflight": "yarn prerelease && yarn check:npm_token && yarn check:npm_auth && sh scripts/release-preflight.sh",
|
|
70
|
-
"prerelease": "node -v | grep -
|
|
70
|
+
"prerelease": "node -v | grep -qE '^v(22|24)\\.' || (echo 'Please use Node.js v22 or v24 for deployment' && exit 1)",
|
|
71
71
|
"release": "yarn release:preflight && yarn lint && yarn check:translations && yarn check:locale-consistency && yarn test && yarn prepack && yarn release:publish:dry && dotenv -- changelogen --release && dotenv -- sh -c 'npm publish --//registry.npmjs.org/:_authToken=$NPM_TOKEN --registry https://registry.npmjs.org/' && git push --follow-tags",
|
|
72
72
|
"lint": "eslint .",
|
|
73
73
|
"lint:fix": "eslint . --fix",
|
|
74
|
-
"pretest": "node -v | grep -
|
|
74
|
+
"pretest": "node -v | grep -qE '^v(22|24)\\.' || (echo 'Please use Node.js v22 or v24 for testing' && exit 1)",
|
|
75
75
|
"test:quick": "yarn test:unit && yarn test:sqlite",
|
|
76
76
|
"test:release": "yarn test && yarn build && yarn lint:fix",
|
|
77
77
|
"test:types": "yarn dev:prepare && vue-tsc --noEmit --project tsconfig.vitest.json && cd playground && vue-tsc --noEmit",
|
package/scripts/deploy-docs.sh
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
|
|
3
3
|
# Configuration
|
|
4
|
-
SERVER_HOST="
|
|
5
|
-
REMOTE_DOCS_PATH="/var/www/nuxt-users.webmania.cc"
|
|
4
|
+
SERVER_HOST="root_balarama"
|
|
5
|
+
REMOTE_DOCS_PATH="/var/www/nuxt-users.webmania.cc/web/"
|
|
6
6
|
LOCAL_BUILD_PATH="docs/.vitepress/dist"
|
|
7
7
|
|
|
8
8
|
# Colors for output
|
|
@@ -65,4 +65,4 @@ EOF
|
|
|
65
65
|
rm "$TEMP_TAR"
|
|
66
66
|
|
|
67
67
|
echo -e "${GREEN}✅ Documentation deployment completed!${NC}"
|
|
68
|
-
echo -e "${YELLOW}🌐 Your docs should now be available on your server${NC}"
|
|
68
|
+
echo -e "${YELLOW}🌐 Your docs should now be available on your server${NC}"
|
|
@@ -84,6 +84,17 @@ else
|
|
|
84
84
|
exit 1
|
|
85
85
|
fi
|
|
86
86
|
|
|
87
|
+
# Test 1b: A dot in the query string must not skip authentication
|
|
88
|
+
print_status $YELLOW "🔐 Testing user creation with a dot in the query string requires authentication..."
|
|
89
|
+
response=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:3000/api/nuxt-users?x=a.b" -X POST -H "Content-Type: application/json" -d '{"email":"bypass@example.com","name":"Bypass","password":"Bypass-123456","role":"admin"}' || echo "000")
|
|
90
|
+
|
|
91
|
+
if [ "$response" = "401" ]; then
|
|
92
|
+
print_status $GREEN "✅ Query string with a dot does not bypass authentication (401)"
|
|
93
|
+
else
|
|
94
|
+
print_status $RED "❌ POST /api/nuxt-users?x=a.b should return 401, got $response - authentication bypass!"
|
|
95
|
+
exit 1
|
|
96
|
+
fi
|
|
97
|
+
|
|
87
98
|
# Test 2: Session endpoint should be accessible (not 401)
|
|
88
99
|
print_status $YELLOW "📝 Testing session endpoint /api/nuxt-users/session..."
|
|
89
100
|
response=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:3000/api/nuxt-users/session" -X POST || echo "000")
|