nuxt-users
Version:
A comprehensive user management module for Nuxt 3 and Nuxt 4 applications with authentication, authorization, database support, and CLI tools
139 lines (132 loc) • 6.03 kB
JavaScript
import { createTransport } from "nodemailer";
import crypto from "node:crypto";
import bcrypt from "bcrypt";
import { findUserByEmail, updateUserPassword, useDb } from "../utils/index.js";
const TOKEN_EXPIRATION_HOURS = 1;
export const sendPasswordResetLink = async (email, options) => {
const user = await findUserByEmail(email, options);
if (!user) {
console.log(`[Nuxt Users] Password reset requested for non-existent email: ${email}`);
return;
}
const db = await useDb(options);
const token = crypto.randomBytes(32).toString("hex");
const hashedToken = await bcrypt.hash(token, 10);
const passwordResetTokensTable = options.tables.passwordResetTokens;
await db.sql`
INSERT INTO {${passwordResetTokensTable}} (email, token, created_at)
VALUES (${email}, ${hashedToken}, CURRENT_TIMESTAMP)
`;
if (!options.mailer) {
console.error("[Nuxt Users] Mailer configuration is missing. Cannot send password reset email.");
return;
}
const transporter = createTransport({
host: options.mailer.host,
port: options.mailer.port,
secure: options.mailer.secure,
auth: {
user: options.mailer.auth.user,
pass: options.mailer.auth.pass
}
});
const resetUrl = new URL("/reset-password", options.passwordResetBaseUrl || "http://localhost:3000");
resetUrl.searchParams.set("token", token);
resetUrl.searchParams.set("email", email);
const resetLink = resetUrl.toString();
try {
await transporter.sendMail({
from: options.mailer.defaults?.from || '"Nuxt Users" <noreply@example.com>',
to: email,
subject: "Password Reset Request",
text: `Please click the following link to reset your password: ${resetLink}
This link will expire in ${TOKEN_EXPIRATION_HOURS} hour(s).
If you did not request this password reset, please ignore this email.`,
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h2>Password Reset Request</h2>
<p>You have requested to reset your password. Please click the button below to set a new password:</p>
<div style="text-align: center; margin: 30px 0;">
<a href="${resetLink}"
style="background-color: #007bff; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px; display: inline-block;">
Reset Password
</a>
</div>
<p>Or copy and paste the following link into your browser:</p>
<p style="word-break: break-all; background-color: #f8f9fa; padding: 10px; border-radius: 3px;">
<a href="${resetLink}">${resetLink}</a>
</p>
<p style="color: #6c757d; font-size: 14px;">
This link will expire in ${TOKEN_EXPIRATION_HOURS} hour(s).
</p>
<p style="color: #6c757d; font-size: 14px;">
If you did not request this password reset, please ignore this email. Your password will remain unchanged.
</p>
</div>
`
});
console.log(`[Nuxt Users] Password reset email sent to ${email}`);
} catch (error) {
console.error(`[Nuxt Users] Failed to send password reset email to ${email}:`, error);
}
};
export const resetPassword = async (token, email, newPassword, options) => {
const db = await useDb(options);
const passwordResetTokensTable = options.tables.passwordResetTokens;
const tokenRecords = await db.sql`
SELECT * FROM {${passwordResetTokensTable}}
WHERE email = ${email}
ORDER BY created_at DESC
`;
if (tokenRecords.rows.length === 0) {
console.log(`[Nuxt Users] No password reset tokens found for email: ${email}`);
return false;
}
let validTokenRecord = null;
for (const record of tokenRecords.rows) {
const tokenMatch = await bcrypt.compare(token, record.token);
if (tokenMatch) {
validTokenRecord = record;
break;
}
}
if (!validTokenRecord || !validTokenRecord.created_at) {
console.log(`[Nuxt Users] Invalid password reset token provided for email: ${email}`);
return false;
}
const now = /* @__PURE__ */ new Date();
const currentTimeString = now.toISOString().slice(0, 19).replace("T", " ");
const [datePart, timePart] = validTokenRecord.created_at.split(/[ T]/);
const [year, month, day] = datePart.split("-").map(Number);
const [hour, minute, second] = timePart.split(":").map(Number);
let expirationHour = hour + TOKEN_EXPIRATION_HOURS;
let expirationDay = day;
const expirationMonth = month;
const expirationYear = year;
if (expirationHour >= 24) {
expirationHour -= 24;
expirationDay++;
}
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")}`;
if (currentTimeString > expirationTimeString) {
console.log(`[Nuxt Users] Expired password reset token for email: ${email}`);
await db.sql`DELETE FROM {${passwordResetTokensTable}} WHERE id = ${validTokenRecord.id}`;
return false;
}
await updateUserPassword(email, newPassword, options);
await db.sql`DELETE FROM {${passwordResetTokensTable}} WHERE email = ${email}`;
console.log(`[Nuxt Users] Password reset successful for email: ${email}`);
return true;
};
export const deleteExpiredPasswordResetTokens = async (options) => {
const db = await useDb(options);
const passwordResetTokensTable = options.tables.passwordResetTokens;
const expirationDate = /* @__PURE__ */ new Date();
expirationDate.setHours(expirationDate.getHours() - TOKEN_EXPIRATION_HOURS);
const expirationDateString = expirationDate.toISOString();
await db.sql`
DELETE FROM {${passwordResetTokensTable}}
WHERE created_at < ${expirationDateString}
`;
console.log(`[Nuxt Users] Expired password reset tokens older than ${expirationDateString} deleted.`);
};