nuxt-users
Version:
A comprehensive user management module for Nuxt 3 and Nuxt 4 applications with authentication, authorization, database support, and CLI tools
202 lines (193 loc) • 8.55 kB
JavaScript
import { createTransport } from "nodemailer";
import crypto from "node:crypto";
import bcrypt from "bcrypt";
import { findUserByEmail, useDb } from "../utils/index.js";
import { validatePassword, getPasswordValidationOptions } from "nuxt-users/utils";
const TOKEN_EXPIRATION_HOURS = 24;
export const registerUser = async (userData, options, baseUrl) => {
const existingUser = await findUserByEmail(userData.email, options);
if (existingUser) {
throw new Error("A user with this email already exists");
}
const passwordOptions = getPasswordValidationOptions(options);
const passwordValidation = validatePassword(userData.password, passwordOptions);
if (!passwordValidation.isValid) {
throw new Error(`Password validation failed: ${passwordValidation.errors.join(", ")}`);
}
const db = await useDb(options);
const usersTable = options.tables.users;
const hashedPassword = await bcrypt.hash(userData.password, 10);
const confirmationToken = crypto.randomBytes(32).toString("hex");
const hashedToken = await bcrypt.hash(confirmationToken, 10);
await db.sql`
INSERT INTO {${usersTable}} (email, name, password, role, active, created_at, updated_at)
VALUES (${userData.email}, ${userData.name}, ${hashedPassword}, 'user', false, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
`;
const result = await db.sql`
SELECT id, email, name, role, created_at, updated_at
FROM {${usersTable}}
WHERE email = ${userData.email}
`;
if (result.rows.length === 0) {
throw new Error("Failed to create user.");
}
const user = result.rows[0];
if (!user) {
throw new Error("Failed to retrieve created user.");
}
const passwordResetTokensTable = options.tables.passwordResetTokens;
await db.sql`
INSERT INTO {${passwordResetTokensTable}} (email, token, created_at)
VALUES (${userData.email}, ${hashedToken}, CURRENT_TIMESTAMP)
`;
try {
await sendConfirmationEmail(userData.email, userData.name, confirmationToken, options, baseUrl);
} catch (emailError) {
console.error("[Nuxt Users] Failed to send confirmation email, but user was created successfully:", emailError);
}
return {
user: {
id: user.id,
email: user.email,
name: user.name,
role: user.role,
created_at: user.created_at instanceof Date ? user.created_at.toISOString() : user.created_at,
updated_at: user.updated_at instanceof Date ? user.updated_at.toISOString() : user.updated_at
},
message: "Registration successful! Please check your email to confirm your account."
};
};
export const sendConfirmationEmail = async (email, name, token, options, baseUrl) => {
if (!options.mailer) {
console.error("[Nuxt Users] Mailer configuration is missing. Cannot send confirmation 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 appBaseUrl = baseUrl || "http://localhost:3000";
const confirmationUrl = new URL(`${options.apiBasePath}/confirm-email`, appBaseUrl);
confirmationUrl.searchParams.set("token", token);
confirmationUrl.searchParams.set("email", email);
const confirmationLink = confirmationUrl.toString();
try {
await transporter.sendMail({
from: options.mailer.defaults?.from || '"Nuxt Users" <noreply@example.com>',
to: email,
subject: "Confirm your email address",
text: `Hi ${name},
Welcome! Please click the following link to confirm your email address and activate your account:
${confirmationLink}
This link will expire in ${TOKEN_EXPIRATION_HOURS} hours.
If you did not create an account, please ignore this email.`,
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h2>Welcome, ${name}!</h2>
<p>Thank you for registering! Please click the button below to confirm your email address and activate your account:</p>
<div style="text-align: center; margin: 30px 0;">
<a href="${confirmationLink}"
style="background-color: #28a745; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px; display: inline-block;">
Confirm Email Address
</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="${confirmationLink}">${confirmationLink}</a>
</p>
<p style="color: #6c757d; font-size: 14px;">
This link will expire in ${TOKEN_EXPIRATION_HOURS} hours.
</p>
<p style="color: #6c757d; font-size: 14px;">
If you did not create an account, please ignore this email. Your account will remain inactive.
</p>
</div>
`
});
console.log(`[Nuxt Users] Confirmation email sent to ${email}`);
} catch (error) {
console.error(`[Nuxt Users] Failed to send confirmation email to ${email}:`, error);
throw error;
}
};
export const confirmUserEmail = async (token, email, options) => {
const db = await useDb(options);
const passwordResetTokensTable = options.tables.passwordResetTokens;
const usersTable = options.tables.users;
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 confirmation 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 confirmation token provided for email: ${email}`);
return false;
}
const now = /* @__PURE__ */ new Date();
const currentTimeString = now.toISOString().slice(0, 19).replace("T", " ");
const createdAtString = validTokenRecord.created_at instanceof Date ? validTokenRecord.created_at.toISOString().slice(0, 19).replace("T", " ") : String(validTokenRecord.created_at);
const [datePart, timePart] = createdAtString.split(/[ T]/);
if (!datePart || !timePart) {
console.log(`[Nuxt Users] Invalid timestamp format for token: ${createdAtString}`);
return false;
}
const [year, month, day] = datePart.split("-").map(Number);
const [hour, minute, second] = timePart.split(":").map(Number);
if (!year || !month || !day || hour === void 0 || minute === void 0 || second === void 0) {
console.log("[Nuxt Users] Invalid timestamp components");
return false;
}
let expirationHour = hour + TOKEN_EXPIRATION_HOURS;
let expirationDay = day;
let expirationMonth = month;
let expirationYear = year;
if (expirationHour >= 24) {
expirationHour -= 24;
expirationDay++;
const daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
if (expirationYear % 4 === 0 && (expirationYear % 100 !== 0 || expirationYear % 400 === 0)) {
daysInMonth[1] = 29;
}
const monthIndex = expirationMonth - 1;
const daysInCurrentMonth = daysInMonth[monthIndex];
if (daysInCurrentMonth && expirationDay > daysInCurrentMonth) {
expirationDay = 1;
expirationMonth++;
if (expirationMonth > 12) {
expirationMonth = 1;
expirationYear++;
}
}
}
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 confirmation token for email: ${email}`);
await db.sql`DELETE FROM {${passwordResetTokensTable}} WHERE id = ${validTokenRecord.id}`;
return false;
}
await db.sql`
UPDATE {${usersTable}}
SET active = true, updated_at = CURRENT_TIMESTAMP
WHERE email = ${email}
`;
await db.sql`DELETE FROM {${passwordResetTokensTable}} WHERE id = ${validTokenRecord.id}`;
console.log(`[Nuxt Users] Email confirmed and user activated for: ${email}`);
return true;
};