UNPKG

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
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; };