email verification flow updated

This commit is contained in:
jackiettran
2025-12-15 22:45:55 -05:00
parent 5e01bb8cff
commit 372ab093ef
19 changed files with 1214 additions and 246 deletions

View File

@@ -34,7 +34,6 @@ const dbConfig = {
// Configuration for Sequelize CLI (supports multiple environments)
// All environments use the same configuration from environment variables
const cliConfig = {
development: dbConfig,
dev: dbConfig,
test: dbConfig,
qa: dbConfig,

View File

@@ -162,6 +162,18 @@ const authRateLimiters = {
legacyHeaders: false,
}),
// Email verification rate limiter - protect against brute force on 6-digit codes
emailVerification: rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 10, // 10 verification attempts per 15 minutes per IP
message: {
error: "Too many verification attempts. Please try again later.",
retryAfter: 900,
},
standardHeaders: true,
legacyHeaders: false,
}),
// General API rate limiter
general: rateLimit({
windowMs: 60 * 1000, // 1 minute
@@ -186,6 +198,7 @@ module.exports = {
registerLimiter: authRateLimiters.register,
passwordResetLimiter: authRateLimiters.passwordReset,
alphaCodeValidationLimiter: authRateLimiters.alphaCodeValidation,
emailVerificationLimiter: authRateLimiters.emailVerification,
generalLimiter: authRateLimiters.general,
// Burst protection

View File

@@ -0,0 +1,15 @@
"use strict";
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.addColumn("Users", "verificationAttempts", {
type: Sequelize.INTEGER,
defaultValue: 0,
allowNull: true,
});
},
down: async (queryInterface, Sequelize) => {
await queryInterface.removeColumn("Users", "verificationAttempts");
},
};

View File

@@ -146,6 +146,11 @@ const User = sequelize.define(
max: 100,
},
},
verificationAttempts: {
type: DataTypes.INTEGER,
defaultValue: 0,
allowNull: true,
},
},
{
hooks: {
@@ -208,31 +213,64 @@ User.prototype.resetLoginAttempts = async function () {
};
// Email verification methods
// Maximum verification attempts before requiring a new code
const MAX_VERIFICATION_ATTEMPTS = 5;
User.prototype.generateVerificationToken = async function () {
const crypto = require("crypto");
const token = crypto.randomBytes(32).toString("hex");
// Generate 6-digit numeric code (100000-999999)
const code = crypto.randomInt(100000, 999999).toString();
const expiry = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24 hours
return this.update({
verificationToken: token,
verificationToken: code,
verificationTokenExpiry: expiry,
verificationAttempts: 0, // Reset attempts on new code
});
};
User.prototype.isVerificationTokenValid = function (token) {
const crypto = require("crypto");
if (!this.verificationToken || !this.verificationTokenExpiry) {
return false;
}
if (this.verificationToken !== token) {
return false;
}
// Check if token is expired
if (new Date() > new Date(this.verificationTokenExpiry)) {
return false;
}
return true;
// Validate 6-digit format
if (!/^\d{6}$/.test(token)) {
return false;
}
// Use timing-safe comparison to prevent timing attacks
try {
const inputBuffer = Buffer.from(token);
const storedBuffer = Buffer.from(this.verificationToken);
if (inputBuffer.length !== storedBuffer.length) {
return false;
}
return crypto.timingSafeEqual(inputBuffer, storedBuffer);
} catch {
return false;
}
};
// Check if too many verification attempts
User.prototype.isVerificationLocked = function () {
return (this.verificationAttempts || 0) >= MAX_VERIFICATION_ATTEMPTS;
};
// Increment verification attempts
User.prototype.incrementVerificationAttempts = async function () {
const newAttempts = (this.verificationAttempts || 0) + 1;
await this.update({ verificationAttempts: newAttempts });
return newAttempts;
};
User.prototype.verifyEmail = async function () {
@@ -241,6 +279,7 @@ User.prototype.verifyEmail = async function () {
verifiedAt: new Date(),
verificationToken: null,
verificationTokenExpiry: null,
verificationAttempts: 0,
});
};

View File

@@ -20,7 +20,9 @@ const {
loginLimiter,
registerLimiter,
passwordResetLimiter,
emailVerificationLimiter,
} = require("../middleware/rateLimiter");
const { authenticateToken } = require("../middleware/auth");
const router = express.Router();
const googleClient = new OAuth2Client(
@@ -43,8 +45,7 @@ router.post(
validateRegistration,
async (req, res) => {
try {
const { email, password, firstName, lastName, phone } =
req.body;
const { email, password, firstName, lastName, phone } = req.body;
const existingUser = await User.findOne({
where: { email },
@@ -64,7 +65,7 @@ router.post(
// Alpha access validation
let alphaInvitation = null;
if (process.env.ALPHA_TESTING_ENABLED === 'true') {
if (process.env.ALPHA_TESTING_ENABLED === "true") {
if (req.cookies && req.cookies.alphaAccessCode) {
const { code } = req.cookies.alphaAccessCode;
if (code) {
@@ -88,7 +89,8 @@ router.post(
if (!alphaInvitation) {
return res.status(403).json({
error: "Alpha access required. Please enter your invitation code first.",
error:
"Alpha access required. Please enter your invitation code first.",
});
}
}
@@ -116,7 +118,10 @@ router.post(
// Send verification email (don't block registration if email fails)
let verificationEmailSent = false;
try {
await emailServices.auth.sendVerificationEmail(user, user.verificationToken);
await emailServices.auth.sendVerificationEmail(
user,
user.verificationToken
);
verificationEmailSent = true;
} catch (emailError) {
const reqLogger = logger.withRequestId(req.id);
@@ -200,7 +205,10 @@ router.post(
const user = await User.findOne({ where: { email } });
if (!user) {
return res.status(401).json({ error: "Invalid credentials" });
return res.status(401).json({
error:
"Unable to log in. Please check your email and password, or create an account.",
});
}
// Check if account is locked
@@ -217,7 +225,10 @@ router.post(
if (!isPasswordValid) {
// Increment login attempts
await user.incLoginAttempts();
return res.status(401).json({ error: "Invalid credentials" });
return res.status(401).json({
error:
"Unable to log in. Please check your email and password, or create an account.",
});
}
// Reset login attempts on successful login
@@ -322,7 +333,8 @@ router.post(
if (!email) {
return res.status(400).json({
error: "Email permission is required to continue. Please grant email access when signing in with Google and try again."
error:
"Email permission is required to continue. Please grant email access when signing in with Google and try again.",
});
}
@@ -332,18 +344,22 @@ router.post(
let lastName = familyName;
if (!firstName || !lastName) {
const emailUsername = email.split('@')[0];
const emailUsername = email.split("@")[0];
// Try to split email username by common separators
const nameParts = emailUsername.split(/[._-]/);
if (!firstName) {
firstName = nameParts[0] ? nameParts[0].charAt(0).toUpperCase() + nameParts[0].slice(1) : 'Google';
firstName = nameParts[0]
? nameParts[0].charAt(0).toUpperCase() + nameParts[0].slice(1)
: "Google";
}
if (!lastName) {
lastName = nameParts.length > 1
? nameParts[nameParts.length - 1].charAt(0).toUpperCase() + nameParts[nameParts.length - 1].slice(1)
: 'User';
lastName =
nameParts.length > 1
? nameParts[nameParts.length - 1].charAt(0).toUpperCase() +
nameParts[nameParts.length - 1].slice(1)
: "User";
}
}
@@ -375,7 +391,7 @@ router.post(
});
// Check if there's an alpha invitation for this email
if (process.env.ALPHA_TESTING_ENABLED === 'true') {
if (process.env.ALPHA_TESTING_ENABLED === "true") {
const alphaInvitation = await AlphaInvitation.findOne({
where: { email: email.toLowerCase().trim() },
});
@@ -467,73 +483,121 @@ router.post(
);
// Email verification endpoint
router.post("/verify-email", sanitizeInput, async (req, res) => {
try {
const { token } = req.body;
router.post(
"/verify-email",
emailVerificationLimiter,
authenticateToken,
sanitizeInput,
async (req, res) => {
try {
const { code } = req.body;
if (!token) {
return res.status(400).json({
error: "Verification token required",
code: "TOKEN_REQUIRED",
});
}
if (!code) {
return res.status(400).json({
error: "Verification code required",
code: "CODE_REQUIRED",
});
}
// Find user with this verification token
const user = await User.findOne({
where: { verificationToken: token },
});
// Validate 6-digit format
if (!/^\d{6}$/.test(code)) {
return res.status(400).json({
error: "Verification code must be 6 digits",
code: "INVALID_CODE_FORMAT",
});
}
if (!user) {
return res.status(400).json({
error: "Invalid verification token",
code: "VERIFICATION_TOKEN_INVALID",
});
}
// Get the authenticated user
const user = await User.findByPk(req.user.id);
// Check if already verified
if (user.isVerified) {
return res.status(400).json({
error: "Email already verified",
code: "ALREADY_VERIFIED",
});
}
if (!user) {
return res.status(404).json({
error: "User not found",
code: "USER_NOT_FOUND",
});
}
// Check if token is valid (not expired)
if (!user.isVerificationTokenValid(token)) {
return res.status(400).json({
error: "Verification token has expired. Please request a new one.",
code: "VERIFICATION_TOKEN_EXPIRED",
});
}
// Check if already verified
if (user.isVerified) {
return res.status(400).json({
error: "Email already verified",
code: "ALREADY_VERIFIED",
});
}
// Verify the email
await user.verifyEmail();
// Check if too many failed attempts
if (user.isVerificationLocked()) {
return res.status(429).json({
error: "Too many verification attempts. Please request a new code.",
code: "TOO_MANY_ATTEMPTS",
});
}
const reqLogger = logger.withRequestId(req.id);
reqLogger.info("Email verified successfully", {
userId: user.id,
email: user.email,
});
// Check if user has a verification token
if (!user.verificationToken) {
return res.status(400).json({
error: "No verification code found. Please request a new one.",
code: "NO_CODE",
});
}
res.json({
message: "Email verified successfully",
user: {
id: user.id,
// Check if code is expired
if (
user.verificationTokenExpiry &&
new Date() > new Date(user.verificationTokenExpiry)
) {
return res.status(400).json({
error: "Verification code has expired. Please request a new one.",
code: "VERIFICATION_EXPIRED",
});
}
// Validate the code
if (!user.isVerificationTokenValid(input)) {
// Increment failed attempts
await user.incrementVerificationAttempts();
const reqLogger = logger.withRequestId(req.id);
reqLogger.warn("Invalid verification code attempt", {
userId: user.id,
attempts: user.verificationAttempts + 1,
});
return res.status(400).json({
error: "Invalid verification code",
code: "VERIFICATION_INVALID",
});
}
// Verify the email
await user.verifyEmail();
const reqLogger = logger.withRequestId(req.id);
reqLogger.info("Email verified successfully", {
userId: user.id,
email: user.email,
isVerified: true,
},
});
} catch (error) {
const reqLogger = logger.withRequestId(req.id);
reqLogger.error("Email verification error", {
error: error.message,
stack: error.stack,
});
res.status(500).json({
error: "Email verification failed. Please try again.",
});
});
res.json({
message: "Email verified successfully",
user: {
id: user.id,
email: user.email,
isVerified: true,
},
});
} catch (error) {
const reqLogger = logger.withRequestId(req.id);
reqLogger.error("Email verification error", {
error: error.message,
stack: error.stack,
});
res.status(500).json({
error: "Email verification failed. Please try again.",
});
}
}
});
);
// Resend verification email endpoint
router.post(
@@ -575,7 +639,10 @@ router.post(
// Send verification email
try {
await emailServices.auth.sendVerificationEmail(user, user.verificationToken);
await emailServices.auth.sendVerificationEmail(
user,
user.verificationToken
);
} catch (emailError) {
const reqLogger = logger.withRequestId(req.id);
reqLogger.error("Failed to resend verification email", {

View File

@@ -241,6 +241,14 @@ router.get('/posts/:id', optionalAuth, async (req, res, next) => {
// POST /api/forum/posts - Create new post
router.post('/posts', authenticateToken, async (req, res, next) => {
try {
// Require email verification
if (!req.user.isVerified) {
return res.status(403).json({
error: "Please verify your email address before creating forum posts.",
code: "EMAIL_NOT_VERIFIED"
});
}
let { title, content, category, tags, zipCode, latitude: providedLat, longitude: providedLng, imageFilenames: rawImageFilenames } = req.body;
// Ensure imageFilenames is an array and validate S3 keys
@@ -490,6 +498,14 @@ router.post('/posts', authenticateToken, async (req, res, next) => {
// PUT /api/forum/posts/:id - Update post
router.put('/posts/:id', authenticateToken, async (req, res, next) => {
try {
// Require email verification
if (!req.user.isVerified) {
return res.status(403).json({
error: "Please verify your email address before editing forum posts.",
code: "EMAIL_NOT_VERIFIED"
});
}
const post = await ForumPost.findByPk(req.params.id);
if (!post) {
@@ -934,6 +950,14 @@ router.patch('/posts/:id/accept-answer', authenticateToken, async (req, res, nex
// POST /api/forum/posts/:id/comments - Add comment/reply
router.post('/posts/:id/comments', authenticateToken, async (req, res, next) => {
try {
// Require email verification
if (!req.user.isVerified) {
return res.status(403).json({
error: "Please verify your email address before commenting.",
code: "EMAIL_NOT_VERIFIED"
});
}
// Support both parentId (new) and parentCommentId (legacy) for backwards compatibility
const { content, parentId, parentCommentId, imageFilenames: rawImageFilenames } = req.body;
const parentIdResolved = parentId || parentCommentId;
@@ -1111,6 +1135,14 @@ router.post('/posts/:id/comments', authenticateToken, async (req, res, next) =>
// PUT /api/forum/comments/:id - Edit comment
router.put('/comments/:id', authenticateToken, async (req, res, next) => {
try {
// Require email verification
if (!req.user.isVerified) {
return res.status(403).json({
error: "Please verify your email address before editing comments.",
code: "EMAIL_NOT_VERIFIED"
});
}
const { content, imageFilenames: rawImageFilenames } = req.body;
const comment = await ForumComment.findByPk(req.params.id);

View File

@@ -1,7 +1,10 @@
const express = require("express");
const { Op } = require("sequelize");
const { Rental, Item, User } = require("../models"); // Import from models/index.js to get models with associations
const { authenticateToken, requireVerifiedEmail } = require("../middleware/auth");
const {
authenticateToken,
requireVerifiedEmail,
} = require("../middleware/auth");
const FeeCalculator = require("../utils/feeCalculator");
const RentalDurationCalculator = require("../utils/rentalDurationCalculator");
const RefundService = require("../services/refundService");
@@ -304,7 +307,11 @@ router.post("/", authenticateToken, requireVerifiedEmail, async (req, res) => {
// Send rental request notification to owner
try {
await emailServices.rentalFlow.sendRentalRequestEmail(rentalWithDetails.owner, rentalWithDetails.renter, rentalWithDetails);
await emailServices.rentalFlow.sendRentalRequestEmail(
rentalWithDetails.owner,
rentalWithDetails.renter,
rentalWithDetails
);
const reqLogger = logger.withRequestId(req.id);
reqLogger.info("Rental request notification sent to owner", {
rentalId: rental.id,
@@ -322,7 +329,10 @@ router.post("/", authenticateToken, requireVerifiedEmail, async (req, res) => {
// Send rental request confirmation to renter
try {
await emailServices.rentalFlow.sendRentalRequestConfirmationEmail(rentalWithDetails.renter, rentalWithDetails);
await emailServices.rentalFlow.sendRentalRequestConfirmationEmail(
rentalWithDetails.renter,
rentalWithDetails
);
const reqLogger = logger.withRequestId(req.id);
reqLogger.info("Rental request confirmation sent to renter", {
rentalId: rental.id,
@@ -358,12 +368,7 @@ router.put("/:id/status", authenticateToken, async (req, res) => {
{
model: User,
as: "renter",
attributes: [
"id",
"firstName",
"lastName",
"stripeCustomerId",
],
attributes: ["id", "firstName", "lastName", "stripeCustomerId"],
},
],
});
@@ -445,7 +450,11 @@ router.put("/:id/status", authenticateToken, async (req, res) => {
// Send confirmation emails
// Send approval confirmation to owner with Stripe reminder
try {
await emailServices.rentalFlow.sendRentalApprovalConfirmationEmail(updatedRental.owner, updatedRental.renter, updatedRental);
await emailServices.rentalFlow.sendRentalApprovalConfirmationEmail(
updatedRental.owner,
updatedRental.renter,
updatedRental
);
const reqLogger = logger.withRequestId(req.id);
reqLogger.info("Rental approval confirmation sent to owner", {
rentalId: updatedRental.id,
@@ -453,11 +462,14 @@ router.put("/:id/status", authenticateToken, async (req, res) => {
});
} catch (emailError) {
const reqLogger = logger.withRequestId(req.id);
reqLogger.error("Failed to send rental approval confirmation email to owner", {
error: emailError.message,
rentalId: updatedRental.id,
ownerId: updatedRental.ownerId,
});
reqLogger.error(
"Failed to send rental approval confirmation email to owner",
{
error: emailError.message,
rentalId: updatedRental.id,
ownerId: updatedRental.ownerId,
}
);
}
// Send rental confirmation to renter with payment receipt
@@ -489,11 +501,14 @@ router.put("/:id/status", authenticateToken, async (req, res) => {
}
} catch (emailError) {
const reqLogger = logger.withRequestId(req.id);
reqLogger.error("Failed to send rental confirmation email to renter", {
error: emailError.message,
rentalId: updatedRental.id,
renterId: updatedRental.renterId,
});
reqLogger.error(
"Failed to send rental confirmation email to renter",
{
error: emailError.message,
rentalId: updatedRental.id,
renterId: updatedRental.renterId,
}
);
}
res.json(updatedRental);
@@ -537,7 +552,11 @@ router.put("/:id/status", authenticateToken, async (req, res) => {
// Send confirmation emails
// Send approval confirmation to owner (for free rentals, no Stripe reminder shown)
try {
await emailServices.rentalFlow.sendRentalApprovalConfirmationEmail(updatedRental.owner, updatedRental.renter, updatedRental);
await emailServices.rentalFlow.sendRentalApprovalConfirmationEmail(
updatedRental.owner,
updatedRental.renter,
updatedRental
);
const reqLogger = logger.withRequestId(req.id);
reqLogger.info("Rental approval confirmation sent to owner", {
rentalId: updatedRental.id,
@@ -545,11 +564,14 @@ router.put("/:id/status", authenticateToken, async (req, res) => {
});
} catch (emailError) {
const reqLogger = logger.withRequestId(req.id);
reqLogger.error("Failed to send rental approval confirmation email to owner", {
error: emailError.message,
rentalId: updatedRental.id,
ownerId: updatedRental.ownerId,
});
reqLogger.error(
"Failed to send rental approval confirmation email to owner",
{
error: emailError.message,
rentalId: updatedRental.id,
ownerId: updatedRental.ownerId,
}
);
}
// Send rental confirmation to renter
@@ -581,11 +603,14 @@ router.put("/:id/status", authenticateToken, async (req, res) => {
}
} catch (emailError) {
const reqLogger = logger.withRequestId(req.id);
reqLogger.error("Failed to send rental confirmation email to renter", {
error: emailError.message,
rentalId: updatedRental.id,
renterId: updatedRental.renterId,
});
reqLogger.error(
"Failed to send rental confirmation email to renter",
{
error: emailError.message,
rentalId: updatedRental.id,
renterId: updatedRental.renterId,
}
);
}
res.json(updatedRental);
@@ -687,7 +712,11 @@ router.put("/:id/decline", authenticateToken, async (req, res) => {
// Send decline notification email to renter
try {
await emailServices.rentalFlow.sendRentalDeclinedEmail(updatedRental.renter, updatedRental, reason);
await emailServices.rentalFlow.sendRentalDeclinedEmail(
updatedRental.renter,
updatedRental,
reason
);
const reqLogger = logger.withRequestId(req.id);
reqLogger.info("Rental decline notification sent to renter", {
rentalId: rental.id,
@@ -904,7 +933,7 @@ router.post("/cost-preview", authenticateToken, async (req, res) => {
// Validate date range
if (rentalEndDateTime <= rentalStartDateTime) {
return res.status(400).json({
error: "End date/time must be after start date/time",
error: "End must be after start",
});
}
@@ -987,44 +1016,50 @@ router.get("/:id/refund-preview", authenticateToken, async (req, res, next) => {
});
// Get late fee preview
router.get("/:id/late-fee-preview", authenticateToken, async (req, res, next) => {
try {
const { actualReturnDateTime } = req.query;
router.get(
"/:id/late-fee-preview",
authenticateToken,
async (req, res, next) => {
try {
const { actualReturnDateTime } = req.query;
if (!actualReturnDateTime) {
return res.status(400).json({ error: "actualReturnDateTime is required" });
if (!actualReturnDateTime) {
return res
.status(400)
.json({ error: "actualReturnDateTime is required" });
}
const rental = await Rental.findByPk(req.params.id, {
include: [{ model: Item, as: "item" }],
});
if (!rental) {
return res.status(404).json({ error: "Rental not found" });
}
// Check authorization
if (rental.ownerId !== req.user.id && rental.renterId !== req.user.id) {
return res.status(403).json({ error: "Unauthorized" });
}
const lateCalculation = LateReturnService.calculateLateFee(
rental,
actualReturnDateTime
);
res.json(lateCalculation);
} catch (error) {
const reqLogger = logger.withRequestId(req.id);
reqLogger.error("Error getting late fee preview", {
error: error.message,
stack: error.stack,
rentalId: req.params.id,
userId: req.user.id,
});
next(error);
}
const rental = await Rental.findByPk(req.params.id, {
include: [{ model: Item, as: "item" }],
});
if (!rental) {
return res.status(404).json({ error: "Rental not found" });
}
// Check authorization
if (rental.ownerId !== req.user.id && rental.renterId !== req.user.id) {
return res.status(403).json({ error: "Unauthorized" });
}
const lateCalculation = LateReturnService.calculateLateFee(
rental,
actualReturnDateTime
);
res.json(lateCalculation);
} catch (error) {
const reqLogger = logger.withRequestId(req.id);
reqLogger.error("Error getting late fee preview", {
error: error.message,
stack: error.stack,
rentalId: req.params.id,
userId: req.user.id,
});
next(error);
}
});
);
// Cancel rental with refund processing
router.post("/:id/cancel", authenticateToken, async (req, res, next) => {
@@ -1156,7 +1191,11 @@ router.post("/:id/mark-return", authenticateToken, async (req, res, next) => {
// Send completion emails to both renter and owner
try {
await emailServices.rentalFlow.sendRentalCompletionEmails(rentalWithDetails.owner, rentalWithDetails.renter, rentalWithDetails);
await emailServices.rentalFlow.sendRentalCompletionEmails(
rentalWithDetails.owner,
rentalWithDetails.renter,
rentalWithDetails
);
const reqLogger = logger.withRequestId(req.id);
reqLogger.info("Rental completion emails sent", {
rentalId,
@@ -1224,7 +1263,11 @@ router.post("/:id/mark-return", authenticateToken, async (req, res, next) => {
// Send notification to customer service
const owner = await User.findByPk(rental.ownerId);
const renter = await User.findByPk(rental.renterId);
await emailServices.customerService.sendLostItemToCustomerService(updatedRental, owner, renter);
await emailServices.customerService.sendLostItemToCustomerService(
updatedRental,
owner,
renter
);
break;
default:
@@ -1261,14 +1304,14 @@ router.post("/:id/mark-return", authenticateToken, async (req, res, next) => {
// Allowed fields for damage report (prevents mass assignment)
const ALLOWED_DAMAGE_REPORT_FIELDS = [
'description',
'canBeFixed',
'repairCost',
'needsReplacement',
'replacementCost',
'proofOfOwnership',
'actualReturnDateTime',
'imageFilenames',
"description",
"canBeFixed",
"repairCost",
"needsReplacement",
"replacementCost",
"proofOfOwnership",
"actualReturnDateTime",
"imageFilenames",
];
function extractAllowedDamageFields(body) {
@@ -1295,9 +1338,13 @@ router.post("/:id/report-damage", authenticateToken, async (req, res, next) => {
? damageInfo.imageFilenames
: [];
const keyValidation = validateS3Keys(imageFilenamesArray, 'damage-reports', {
maxKeys: IMAGE_LIMITS.damageReports,
});
const keyValidation = validateS3Keys(
imageFilenamesArray,
"damage-reports",
{
maxKeys: IMAGE_LIMITS.damageReports,
}
);
if (!keyValidation.valid) {
return res.status(400).json({
error: keyValidation.error,

View File

@@ -50,6 +50,7 @@ class AuthEmailService {
const variables = {
recipientName: user.firstName || "there",
verificationUrl: verificationUrl,
verificationCode: verificationToken, // 6-digit code for display in email
};
const htmlContent = await this.templateManager.renderTemplate(

View File

@@ -196,9 +196,29 @@
<h1>Verify Your Email Address</h1>
<p>Thank you for registering with RentAll! To complete your account setup and start renting items, please verify your email address by clicking the button below.</p>
<p>Thank you for registering with RentAll! Use the verification code below to complete your account setup.</p>
<div style="text-align: center;">
<!-- Verification Code Display -->
<div style="text-align: center; margin: 30px 0;">
<p style="margin-bottom: 10px; color: #6c757d; font-size: 14px;">Your verification code is:</p>
<div style="background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
border-radius: 12px;
padding: 20px 40px;
display: inline-block;
border: 2px dashed #28a745;">
<span style="font-size: 36px;
font-weight: 700;
letter-spacing: 8px;
color: #28a745;
font-family: 'Courier New', monospace;">{{verificationCode}}</span>
</div>
<p style="margin-top: 10px; font-size: 14px; color: #6c757d;">
Enter this code in the app to verify your email
</p>
</div>
<div style="text-align: center; margin: 20px 0;">
<p style="color: #6c757d; margin-bottom: 10px; font-size: 14px;">Or click the button below:</p>
<a href="{{verificationUrl}}" class="button">Verify Email Address</a>
</div>
@@ -210,7 +230,7 @@
<p style="word-break: break-all; color: #667eea;">{{verificationUrl}}</p>
<div class="warning-box">
<p><strong>This link will expire in 24 hours.</strong> If you need a new verification link, you can request one from your account settings.</p>
<p><strong>This code will expire in 24 hours.</strong> If you need a new verification code, you can request one from your account settings.</p>
</div>
<p><strong>Didn't create an account?</strong> If you didn't register for a RentAll account, you can safely ignore this email.</p>