nuxt-users 1.50.0 → 1.50.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/dist/module.json +1 -1
- package/dist/runtime/server/services/password.d.ts +0 -4
- package/dist/runtime/server/services/password.js +5 -43
- package/dist/runtime/server/services/registration.js +5 -45
- package/dist/runtime/server/utils/token-expiry.d.ts +27 -0
- package/dist/runtime/server/utils/token-expiry.js +24 -0
- package/package.json +2 -2
package/dist/module.json
CHANGED
|
@@ -8,7 +8,3 @@ export declare const sendPasswordResetLink: (email: string, options: ModuleOptio
|
|
|
8
8
|
*/
|
|
9
9
|
export declare const resetPassword: (token: string, email: string, // Added email to quickly find the token
|
|
10
10
|
newPassword: string, options: ModuleOptions) => Promise<boolean>;
|
|
11
|
-
/**
|
|
12
|
-
* Deletes expired password reset tokens from the database.
|
|
13
|
-
*/
|
|
14
|
-
export declare const deleteExpiredPasswordResetTokens: (options: ModuleOptions) => Promise<void>;
|
|
@@ -3,6 +3,7 @@ import crypto from "node:crypto";
|
|
|
3
3
|
import bcrypt from "bcrypt";
|
|
4
4
|
import { findUserByEmail, updateUserPassword, revokeUserTokens, useDb } from "../utils/index.js";
|
|
5
5
|
import { escapeHtml } from "../utils/escape-html.js";
|
|
6
|
+
import { isTokenExpired, findTokenRecordsByEmail } from "../utils/token-expiry.js";
|
|
6
7
|
const TOKEN_EXPIRATION_HOURS = 1;
|
|
7
8
|
export const sendPasswordResetLink = async (email, options, baseUrl) => {
|
|
8
9
|
const user = await findUserByEmail(email, options);
|
|
@@ -82,17 +83,13 @@ If you did not request this password reset, please ignore this email.`,
|
|
|
82
83
|
export const resetPassword = async (token, email, newPassword, options) => {
|
|
83
84
|
const db = await useDb(options);
|
|
84
85
|
const passwordResetTokensTable = options.tables.passwordResetTokens;
|
|
85
|
-
const tokenRecords = await db
|
|
86
|
-
|
|
87
|
-
WHERE email = ${email}
|
|
88
|
-
ORDER BY created_at DESC
|
|
89
|
-
`;
|
|
90
|
-
if (tokenRecords.rows.length === 0) {
|
|
86
|
+
const tokenRecords = await findTokenRecordsByEmail(db, passwordResetTokensTable, email);
|
|
87
|
+
if (tokenRecords.length === 0) {
|
|
91
88
|
console.log(`[Nuxt Users] No password reset tokens found for email: ${email}`);
|
|
92
89
|
return false;
|
|
93
90
|
}
|
|
94
91
|
let validTokenRecord = null;
|
|
95
|
-
for (const record of tokenRecords
|
|
92
|
+
for (const record of tokenRecords) {
|
|
96
93
|
const tokenMatch = await bcrypt.compare(token, record.token);
|
|
97
94
|
if (tokenMatch) {
|
|
98
95
|
validTokenRecord = record;
|
|
@@ -103,30 +100,7 @@ export const resetPassword = async (token, email, newPassword, options) => {
|
|
|
103
100
|
console.log(`[Nuxt Users] Invalid password reset token provided for email: ${email}`);
|
|
104
101
|
return false;
|
|
105
102
|
}
|
|
106
|
-
|
|
107
|
-
const currentTimeString = now.toISOString().slice(0, 19).replace("T", " ");
|
|
108
|
-
const createdAtString = validTokenRecord.created_at instanceof Date ? validTokenRecord.created_at.toISOString().slice(0, 19).replace("T", " ") : String(validTokenRecord.created_at);
|
|
109
|
-
const [datePart, timePart] = createdAtString.split(/[ T]/);
|
|
110
|
-
if (!datePart || !timePart) {
|
|
111
|
-
console.log(`[Nuxt Users] Invalid timestamp format for token: ${createdAtString}`);
|
|
112
|
-
return false;
|
|
113
|
-
}
|
|
114
|
-
const [year, month, day] = datePart.split("-").map(Number);
|
|
115
|
-
const [hour, minute, second] = timePart.split(":").map(Number);
|
|
116
|
-
if (!year || !month || !day || hour === void 0 || minute === void 0 || second === void 0) {
|
|
117
|
-
console.log("[Nuxt Users] Invalid timestamp components");
|
|
118
|
-
return false;
|
|
119
|
-
}
|
|
120
|
-
let expirationHour = hour + TOKEN_EXPIRATION_HOURS;
|
|
121
|
-
let expirationDay = day;
|
|
122
|
-
const expirationMonth = month;
|
|
123
|
-
const expirationYear = year;
|
|
124
|
-
if (expirationHour >= 24) {
|
|
125
|
-
expirationHour -= 24;
|
|
126
|
-
expirationDay++;
|
|
127
|
-
}
|
|
128
|
-
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")}`;
|
|
129
|
-
if (currentTimeString > expirationTimeString) {
|
|
103
|
+
if (isTokenExpired(validTokenRecord.created_at, validTokenRecord.db_now, TOKEN_EXPIRATION_HOURS)) {
|
|
130
104
|
console.log(`[Nuxt Users] Expired password reset token for email: ${email}`);
|
|
131
105
|
await db.sql`DELETE FROM {${passwordResetTokensTable}} WHERE id = ${validTokenRecord.id}`;
|
|
132
106
|
return false;
|
|
@@ -140,15 +114,3 @@ export const resetPassword = async (token, email, newPassword, options) => {
|
|
|
140
114
|
console.log(`[Nuxt Users] Password reset successful for email: ${email}`);
|
|
141
115
|
return true;
|
|
142
116
|
};
|
|
143
|
-
export const deleteExpiredPasswordResetTokens = async (options) => {
|
|
144
|
-
const db = await useDb(options);
|
|
145
|
-
const passwordResetTokensTable = options.tables.passwordResetTokens;
|
|
146
|
-
const expirationDate = /* @__PURE__ */ new Date();
|
|
147
|
-
expirationDate.setHours(expirationDate.getHours() - TOKEN_EXPIRATION_HOURS);
|
|
148
|
-
const expirationDateString = expirationDate.toISOString().slice(0, 19).replace("T", " ");
|
|
149
|
-
await db.sql`
|
|
150
|
-
DELETE FROM {${passwordResetTokensTable}}
|
|
151
|
-
WHERE created_at < ${expirationDateString}
|
|
152
|
-
`;
|
|
153
|
-
console.log(`[Nuxt Users] Expired password reset tokens older than ${expirationDateString} deleted.`);
|
|
154
|
-
};
|
|
@@ -3,6 +3,7 @@ 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
5
|
import { escapeHtml } from "../utils/escape-html.js";
|
|
6
|
+
import { isTokenExpired, findTokenRecordsByEmail } from "../utils/token-expiry.js";
|
|
6
7
|
import { validatePassword, getPasswordValidationOptions } from "nuxt-users/utils";
|
|
7
8
|
const TOKEN_EXPIRATION_HOURS = 24;
|
|
8
9
|
export const registerUser = async (userData, options, baseUrl) => {
|
|
@@ -130,17 +131,13 @@ export const confirmUserEmail = async (token, email, options) => {
|
|
|
130
131
|
const db = await useDb(options);
|
|
131
132
|
const passwordResetTokensTable = options.tables.passwordResetTokens;
|
|
132
133
|
const usersTable = options.tables.users;
|
|
133
|
-
const tokenRecords = await db
|
|
134
|
-
|
|
135
|
-
WHERE email = ${email}
|
|
136
|
-
ORDER BY created_at DESC
|
|
137
|
-
`;
|
|
138
|
-
if (tokenRecords.rows.length === 0) {
|
|
134
|
+
const tokenRecords = await findTokenRecordsByEmail(db, passwordResetTokensTable, email);
|
|
135
|
+
if (tokenRecords.length === 0) {
|
|
139
136
|
console.log(`[Nuxt Users] No confirmation tokens found for email: ${email}`);
|
|
140
137
|
return false;
|
|
141
138
|
}
|
|
142
139
|
let validTokenRecord = null;
|
|
143
|
-
for (const record of tokenRecords
|
|
140
|
+
for (const record of tokenRecords) {
|
|
144
141
|
const tokenMatch = await bcrypt.compare(token, record.token);
|
|
145
142
|
if (tokenMatch) {
|
|
146
143
|
validTokenRecord = record;
|
|
@@ -151,44 +148,7 @@ export const confirmUserEmail = async (token, email, options) => {
|
|
|
151
148
|
console.log(`[Nuxt Users] Invalid confirmation token provided for email: ${email}`);
|
|
152
149
|
return false;
|
|
153
150
|
}
|
|
154
|
-
|
|
155
|
-
const currentTimeString = now.toISOString().slice(0, 19).replace("T", " ");
|
|
156
|
-
const createdAtString = validTokenRecord.created_at instanceof Date ? validTokenRecord.created_at.toISOString().slice(0, 19).replace("T", " ") : String(validTokenRecord.created_at);
|
|
157
|
-
const [datePart, timePart] = createdAtString.split(/[ T]/);
|
|
158
|
-
if (!datePart || !timePart) {
|
|
159
|
-
console.log(`[Nuxt Users] Invalid timestamp format for token: ${createdAtString}`);
|
|
160
|
-
return false;
|
|
161
|
-
}
|
|
162
|
-
const [year, month, day] = datePart.split("-").map(Number);
|
|
163
|
-
const [hour, minute, second] = timePart.split(":").map(Number);
|
|
164
|
-
if (!year || !month || !day || hour === void 0 || minute === void 0 || second === void 0) {
|
|
165
|
-
console.log("[Nuxt Users] Invalid timestamp components");
|
|
166
|
-
return false;
|
|
167
|
-
}
|
|
168
|
-
let expirationHour = hour + TOKEN_EXPIRATION_HOURS;
|
|
169
|
-
let expirationDay = day;
|
|
170
|
-
let expirationMonth = month;
|
|
171
|
-
let expirationYear = year;
|
|
172
|
-
if (expirationHour >= 24) {
|
|
173
|
-
expirationHour -= 24;
|
|
174
|
-
expirationDay++;
|
|
175
|
-
const daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
176
|
-
if (expirationYear % 4 === 0 && (expirationYear % 100 !== 0 || expirationYear % 400 === 0)) {
|
|
177
|
-
daysInMonth[1] = 29;
|
|
178
|
-
}
|
|
179
|
-
const monthIndex = expirationMonth - 1;
|
|
180
|
-
const daysInCurrentMonth = daysInMonth[monthIndex];
|
|
181
|
-
if (daysInCurrentMonth && expirationDay > daysInCurrentMonth) {
|
|
182
|
-
expirationDay = 1;
|
|
183
|
-
expirationMonth++;
|
|
184
|
-
if (expirationMonth > 12) {
|
|
185
|
-
expirationMonth = 1;
|
|
186
|
-
expirationYear++;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
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")}`;
|
|
191
|
-
if (currentTimeString > expirationTimeString) {
|
|
151
|
+
if (isTokenExpired(validTokenRecord.created_at, validTokenRecord.db_now, TOKEN_EXPIRATION_HOURS)) {
|
|
192
152
|
console.log(`[Nuxt Users] Expired confirmation token for email: ${email}`);
|
|
193
153
|
await db.sql`DELETE FROM {${passwordResetTokensTable}} WHERE id = ${validTokenRecord.id}`;
|
|
194
154
|
return false;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Database } from 'db0';
|
|
2
|
+
/**
|
|
3
|
+
* A DB timestamp as returned by the driver: a Date (MySQL, PostgreSQL)
|
|
4
|
+
* or a 'YYYY-MM-DD HH:MM:SS' string (SQLite).
|
|
5
|
+
*/
|
|
6
|
+
export type DbTimestamp = Date | string;
|
|
7
|
+
/**
|
|
8
|
+
* Decides whether a token is older than `hours`.
|
|
9
|
+
*
|
|
10
|
+
* `dbNow` must be the database's own CURRENT_TIMESTAMP, read in the same query as `createdAt`:
|
|
11
|
+
* both values then share the database's and the driver's time zone handling, so the difference
|
|
12
|
+
* is correct whatever time zones the database and the Node process use.
|
|
13
|
+
*/
|
|
14
|
+
export declare const isTokenExpired: (createdAt: DbTimestamp, dbNow: DbTimestamp, hours: number) => boolean;
|
|
15
|
+
/** A row of the password reset / confirmation token table, with the database's own clock alongside */
|
|
16
|
+
export interface TokenRecord {
|
|
17
|
+
id: number;
|
|
18
|
+
email: string;
|
|
19
|
+
token: string;
|
|
20
|
+
created_at: DbTimestamp;
|
|
21
|
+
db_now: DbTimestamp;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Finds the tokens for an email, newest first. `db_now` is read in the same query so that
|
|
25
|
+
* expiry can be judged on the database's own clock with `isTokenExpired` (issue #220).
|
|
26
|
+
*/
|
|
27
|
+
export declare const findTokenRecordsByEmail: (db: Database, table: string, email: string) => Promise<TokenRecord[]>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const toMilliseconds = (value) => {
|
|
2
|
+
if (value instanceof Date) {
|
|
3
|
+
return value.getTime();
|
|
4
|
+
}
|
|
5
|
+
if (typeof value !== "string") {
|
|
6
|
+
return Number.NaN;
|
|
7
|
+
}
|
|
8
|
+
return Date.parse(`${value.replace(" ", "T")}Z`);
|
|
9
|
+
};
|
|
10
|
+
export const isTokenExpired = (createdAt, dbNow, hours) => {
|
|
11
|
+
const ageMs = toMilliseconds(dbNow) - toMilliseconds(createdAt);
|
|
12
|
+
if (Number.isNaN(ageMs)) {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
return ageMs > hours * 60 * 60 * 1e3;
|
|
16
|
+
};
|
|
17
|
+
export const findTokenRecordsByEmail = async (db, table, email) => {
|
|
18
|
+
const result = await db.sql`
|
|
19
|
+
SELECT *, CURRENT_TIMESTAMP AS db_now FROM {${table}}
|
|
20
|
+
WHERE email = ${email}
|
|
21
|
+
ORDER BY created_at DESC
|
|
22
|
+
`;
|
|
23
|
+
return result.rows;
|
|
24
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxt-users",
|
|
3
|
-
"version": "1.50.
|
|
3
|
+
"version": "1.50.1",
|
|
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",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
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
70
|
"prerelease": "node -v | grep -qE '^v(22|24)\\.' || (echo 'Please use Node.js v22 or v24 for deployment' && exit 1)",
|
|
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",
|
|
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 --no-github && dotenv -- sh -c 'npm publish --//registry.npmjs.org/:_authToken=$NPM_TOKEN --registry https://registry.npmjs.org/' && git push --follow-tags && dotenv -- changelogen gh release",
|
|
72
72
|
"lint": "eslint .",
|
|
73
73
|
"lint:fix": "eslint . --fix",
|
|
74
74
|
"pretest": "node -v | grep -qE '^v(22|24)\\.' || (echo 'Please use Node.js v22 or v24 for testing' && exit 1)",
|