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 CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=3.0.0"
6
6
  },
7
- "version": "1.50.0",
7
+ "version": "1.50.1",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.3",
10
10
  "unbuild": "3.6.1"
@@ -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.sql`
86
- SELECT * FROM {${passwordResetTokensTable}}
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.rows) {
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
- const now = /* @__PURE__ */ new Date();
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.sql`
134
- SELECT * FROM {${passwordResetTokensTable}}
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.rows) {
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
- const now = /* @__PURE__ */ new Date();
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.0",
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)",