refactor(emails): restructure email system and add recording notifications

- Move email templates to separate files in src/emails/templates/
- Create new email service architecture (service.js, index.js)
- Add recording suggestions email template for matching notifications
- Integrate email notifications with matching system (sends when suggestions created)
- Update controllers (auth.js, user.js) to use new email module
- Update tests to use new email module path
- Remove deprecated src/utils/email.js

Features:
- Template-based email system for easy editing
- Automatic email notifications when recording assignments are made
- Clean separation between template logic and sending logic
- Graceful error handling for AWS SES failures
This commit is contained in:
Radosław Gierwiało
2025-12-02 19:19:22 +01:00
parent 231d3d177c
commit b77ccab9d4
12 changed files with 605 additions and 325 deletions

View File

@@ -9,13 +9,13 @@ const { prisma } = require('../utils/db');
const { hashPassword, generateVerificationToken, generateVerificationCode } = require('../utils/auth');
// Mock email service
jest.mock('../utils/email', () => ({
jest.mock('../emails', () => ({
sendVerificationEmail: jest.fn().mockResolvedValue({ success: true }),
sendPasswordResetEmail: jest.fn().mockResolvedValue({ success: true }),
sendWelcomeEmail: jest.fn().mockResolvedValue({ success: true })
}));
const emailService = require('../utils/email');
const emailService = require('../emails');
// Clean up database before and after tests
beforeAll(async () => {

View File

@@ -17,7 +17,7 @@ const {
sendVerificationEmail,
sendPasswordResetEmail,
sendWelcomeEmail
} = require('../../utils/email');
} = require('../../emails');
// Set up test environment variables
beforeAll(() => {

View File

@@ -7,7 +7,7 @@ const {
generateVerificationCode,
getTokenExpiry
} = require('../utils/auth');
const { sendVerificationEmail, sendWelcomeEmail, sendPasswordResetEmail } = require('../utils/email');
const { sendVerificationEmail, sendWelcomeEmail, sendPasswordResetEmail } = require('../emails');
const { sanitizeForEmail, timingSafeEqual } = require('../utils/sanitize');
const securityConfig = require('../config/security');

View File

@@ -1,6 +1,6 @@
const { prisma } = require('../utils/db');
const { hashPassword, comparePassword, generateToken, generateVerificationToken, generateVerificationCode } = require('../utils/auth');
const { sendVerificationEmail } = require('../utils/email');
const { sendVerificationEmail } = require('../emails');
const { sanitizeForEmail } = require('../utils/sanitize');
/**

View File

@@ -0,0 +1,20 @@
/**
* Email module exports
* Provides clean API for sending emails throughout the application
*/
const {
sendEmail,
sendVerificationEmail,
sendPasswordResetEmail,
sendWelcomeEmail,
sendRecordingSuggestionsEmail,
} = require('./service');
module.exports = {
sendEmail,
sendVerificationEmail,
sendPasswordResetEmail,
sendWelcomeEmail,
sendRecordingSuggestionsEmail,
};

View File

@@ -0,0 +1,141 @@
/**
* Email Service using AWS SES
* Handles sending emails with template support
*/
const { SESClient, SendEmailCommand } = require('@aws-sdk/client-ses');
const { generateVerificationEmail } = require('./templates/verification');
const { generatePasswordResetEmail } = require('./templates/password-reset');
const { generateWelcomeEmail } = require('./templates/welcome');
const { generateRecordingSuggestionsEmail } = require('./templates/recording-suggestions');
// Configure AWS SES Client
const sesClient = new SESClient({
region: process.env.AWS_REGION || 'us-east-1',
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
},
});
/**
* Send email via AWS SES
* @param {Object} params - Email parameters
* @param {string} params.to - Recipient email address
* @param {string} params.subject - Email subject
* @param {string} params.htmlBody - HTML email body
* @param {string} params.textBody - Plain text email body (fallback)
* @returns {Promise<Object>} - SES response
*/
async function sendEmail({ to, subject, htmlBody, textBody }) {
const params = {
Source: `${process.env.SES_FROM_NAME} <${process.env.SES_FROM_EMAIL}>`,
Destination: {
ToAddresses: [to],
},
Message: {
Subject: {
Data: subject,
Charset: 'UTF-8',
},
Body: {
Html: {
Data: htmlBody,
Charset: 'UTF-8',
},
Text: {
Data: textBody,
Charset: 'UTF-8',
},
},
},
};
try {
const command = new SendEmailCommand(params);
const response = await sesClient.send(command);
console.log(`Email sent successfully to ${to}. MessageId: ${response.MessageId}`);
return { success: true, messageId: response.MessageId };
} catch (error) {
console.error('Error sending email:', error);
throw new Error(`Failed to send email: ${error.message}`);
}
}
/**
* Send verification email with link and PIN code
* @param {string} email - User email
* @param {string} firstName - User first name
* @param {string} verificationToken - Unique verification token
* @param {string} verificationCode - 6-digit PIN code
*/
async function sendVerificationEmail(email, firstName, verificationToken, verificationCode) {
const verificationLink = `${process.env.FRONTEND_URL}/verify-email?token=${verificationToken}`;
const { subject, htmlBody, textBody } = generateVerificationEmail({
firstName,
verificationLink,
verificationCode,
});
return sendEmail({ to: email, subject, htmlBody, textBody });
}
/**
* Send password reset email with link
* @param {string} email - User email
* @param {string} firstName - User first name
* @param {string} resetToken - Unique reset token
*/
async function sendPasswordResetEmail(email, firstName, resetToken) {
const resetLink = `${process.env.FRONTEND_URL}/reset-password?token=${resetToken}`;
const { subject, htmlBody, textBody } = generatePasswordResetEmail({
firstName,
resetLink,
});
return sendEmail({ to: email, subject, htmlBody, textBody });
}
/**
* Send welcome email after successful verification
* @param {string} email - User email
* @param {string} firstName - User first name
*/
async function sendWelcomeEmail(email, firstName) {
const { subject, htmlBody, textBody } = generateWelcomeEmail({
firstName,
frontendUrl: process.env.FRONTEND_URL,
});
return sendEmail({ to: email, subject, htmlBody, textBody });
}
/**
* Send recording suggestions notification email
* @param {string} email - User email
* @param {string} firstName - User first name
* @param {string} eventName - Name of the event
* @param {string} eventSlug - Event slug for URL
* @param {number} suggestionsCount - Number of new recording suggestions
*/
async function sendRecordingSuggestionsEmail(email, firstName, eventName, eventSlug, suggestionsCount) {
const { subject, htmlBody, textBody } = generateRecordingSuggestionsEmail({
firstName,
eventName,
eventSlug,
suggestionsCount,
frontendUrl: process.env.FRONTEND_URL,
});
return sendEmail({ to: email, subject, htmlBody, textBody });
}
module.exports = {
sendEmail,
sendVerificationEmail,
sendPasswordResetEmail,
sendWelcomeEmail,
sendRecordingSuggestionsEmail,
};

View File

@@ -0,0 +1,82 @@
/**
* Email template for password reset
* Sends secure password reset link to users
*/
/**
* Generate password reset email content
* @param {Object} data - Template data
* @param {string} data.firstName - User's first name
* @param {string} data.resetLink - Full password reset URL with token
* @returns {Object} - Email content with subject, htmlBody, textBody
*/
function generatePasswordResetEmail(data) {
const { firstName, resetLink } = data;
const subject = 'Reset your spotlight.cam password';
const htmlBody = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; text-align: center; border-radius: 8px 8px 0 0; }
.content { background: #f9fafb; padding: 30px; border-radius: 0 0 8px 8px; }
.button { display: inline-block; background: #667eea; color: white; padding: 14px 30px; text-decoration: none; border-radius: 6px; margin: 20px 0; font-weight: 600; }
.footer { text-align: center; color: #666; font-size: 14px; margin-top: 30px; }
.warning { background: #fef3c7; border-left: 4px solid #f59e0b; padding: 15px; margin: 20px 0; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🔐 Password Reset</h1>
</div>
<div class="content">
<h2>Hi ${firstName || 'there'}! 👋</h2>
<p>We received a request to reset your password for your spotlight.cam account.</p>
<a href="${resetLink}" class="button">Reset Password</a>
<p style="font-size: 14px; color: #666;">This link will expire in 1 hour.</p>
<div class="warning">
<strong>⚠️ Security Notice</strong><br>
If you didn't request this password reset, please ignore this email. Your password will remain unchanged.
</div>
</div>
<div class="footer">
<p>spotlight.cam - P2P video exchange for dance events</p>
<p>This is an automated email. Please do not reply.</p>
</div>
</div>
</body>
</html>
`;
const textBody = `
Hi ${firstName || 'there'}!
We received a request to reset your password for your spotlight.cam account.
Click this link to reset your password:
${resetLink}
This link will expire in 1 hour.
⚠️ Security Notice
If you didn't request this password reset, please ignore this email. Your password will remain unchanged.
---
spotlight.cam - P2P video exchange for dance events
This is an automated email. Please do not reply.
`;
return { subject, htmlBody, textBody };
}
module.exports = { generatePasswordResetEmail };

View File

@@ -0,0 +1,107 @@
/**
* Email template for recording suggestions notification
* Sent when the matching system assigns recording tasks to a user
*/
/**
* Generate recording suggestions notification email
* @param {Object} data - Template data
* @param {string} data.firstName - User's first name
* @param {string} data.eventName - Name of the event
* @param {string} data.eventSlug - Event slug for URL
* @param {number} data.suggestionsCount - Number of new recording suggestions
* @param {string} data.frontendUrl - Base frontend URL
* @returns {Object} - Email content with subject, htmlBody, textBody
*/
function generateRecordingSuggestionsEmail(data) {
const { firstName, eventName, eventSlug, suggestionsCount, frontendUrl } = data;
const eventUrl = `${frontendUrl}/events/${eventSlug}`;
const plural = suggestionsCount === 1 ? '' : 's';
const subject = `New recording assignment${plural} for ${eventName}`;
const htmlBody = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; text-align: center; border-radius: 8px 8px 0 0; }
.content { background: #f9fafb; padding: 30px; border-radius: 0 0 8px 8px; }
.button { display: inline-block; background: #667eea; color: white; padding: 14px 30px; text-decoration: none; border-radius: 6px; margin: 20px 0; font-weight: 600; }
.highlight-box { background: white; border: 2px solid #667eea; border-radius: 8px; padding: 20px; text-align: center; margin: 20px 0; }
.count { font-size: 48px; font-weight: bold; color: #667eea; }
.info-box { background: #e0e7ff; border-left: 4px solid #667eea; padding: 15px; margin: 20px 0; border-radius: 4px; }
.footer { text-align: center; color: #666; font-size: 14px; margin-top: 30px; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎥 New Recording Assignment${plural}</h1>
</div>
<div class="content">
<h2>Hi ${firstName || 'there'}! 👋</h2>
<p>Great news! The matching system has assigned you to record dance performances at <strong>${eventName}</strong>.</p>
<div class="highlight-box">
<div class="count">${suggestionsCount}</div>
<p style="margin: 5px 0; font-size: 18px; color: #666;">New recording assignment${plural}</p>
</div>
<div class="info-box">
<strong>📋 Next Steps:</strong><br>
1. Review your assigned heats in the event dashboard<br>
2. Accept or decline each recording suggestion<br>
3. Coordinate with dancers for video exchange
</div>
<p>You can review and manage your recording assignments by visiting the event page:</p>
<a href="${eventUrl}" class="button">View Recording Assignments</a>
<p style="font-size: 14px; color: #666;">
<strong>Reminder:</strong> Recording assignments help build our dance community!
Please review them at your earliest convenience.
</p>
</div>
<div class="footer">
<p>spotlight.cam - P2P video exchange for dance events</p>
<p>This is an automated email. Please do not reply.</p>
</div>
</div>
</body>
</html>
`;
const textBody = `
Hi ${firstName || 'there'}!
Great news! The matching system has assigned you to record dance performances at ${eventName}.
📊 New Recording Assignments: ${suggestionsCount}
📋 Next Steps:
1. Review your assigned heats in the event dashboard
2. Accept or decline each recording suggestion
3. Coordinate with dancers for video exchange
You can review and manage your recording assignments here:
${eventUrl}
Reminder: Recording assignments help build our dance community!
Please review them at your earliest convenience.
---
spotlight.cam - P2P video exchange for dance events
This is an automated email. Please do not reply.
`;
return { subject, htmlBody, textBody };
}
module.exports = { generateRecordingSuggestionsEmail };

View File

@@ -0,0 +1,95 @@
/**
* Email template for email verification
* Sends verification link and PIN code to new users
*/
/**
* Generate verification email content
* @param {Object} data - Template data
* @param {string} data.firstName - User's first name
* @param {string} data.verificationLink - Full verification URL with token
* @param {string} data.verificationCode - 6-digit PIN code
* @returns {Object} - Email content with subject, htmlBody, textBody
*/
function generateVerificationEmail(data) {
const { firstName, verificationLink, verificationCode } = data;
const subject = 'Verify your spotlight.cam email';
const htmlBody = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; text-align: center; border-radius: 8px 8px 0 0; }
.content { background: #f9fafb; padding: 30px; border-radius: 0 0 8px 8px; }
.code-box { background: white; border: 2px solid #667eea; border-radius: 8px; padding: 20px; text-align: center; margin: 20px 0; }
.code { font-size: 32px; font-weight: bold; letter-spacing: 8px; color: #667eea; font-family: monospace; }
.button { display: inline-block; background: #667eea; color: white; padding: 14px 30px; text-decoration: none; border-radius: 6px; margin: 20px 0; font-weight: 600; }
.footer { text-align: center; color: #666; font-size: 14px; margin-top: 30px; }
.divider { border-top: 1px solid #e5e7eb; margin: 20px 0; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎥 spotlight.cam</h1>
<p>Welcome to the dance community!</p>
</div>
<div class="content">
<h2>Hi ${firstName || 'there'}! 👋</h2>
<p>Thanks for joining spotlight.cam! To start sharing dance videos with your event partners, please verify your email address.</p>
<h3>Option 1: Click the button</h3>
<a href="${verificationLink}" class="button">Verify Email Address</a>
<div class="divider"></div>
<h3>Option 2: Enter this code</h3>
<div class="code-box">
<div class="code">${verificationCode}</div>
</div>
<p style="font-size: 14px; color: #666;">This code will expire in 24 hours.</p>
<div class="divider"></div>
<p><strong>Didn't create an account?</strong> You can safely ignore this email.</p>
</div>
<div class="footer">
<p>spotlight.cam - P2P video exchange for dance events</p>
<p>This is an automated email. Please do not reply.</p>
</div>
</div>
</body>
</html>
`;
const textBody = `
Hi ${firstName || 'there'}!
Thanks for joining spotlight.cam! To start sharing dance videos with your event partners, please verify your email address.
Option 1: Click this link to verify
${verificationLink}
Option 2: Enter this verification code
${verificationCode}
This code will expire in 24 hours.
Didn't create an account? You can safely ignore this email.
---
spotlight.cam - P2P video exchange for dance events
This is an automated email. Please do not reply.
`;
return { subject, htmlBody, textBody };
}
module.exports = { generateVerificationEmail };

View File

@@ -0,0 +1,101 @@
/**
* Email template for welcome message
* Sent after successful email verification
*/
/**
* Generate welcome email content
* @param {Object} data - Template data
* @param {string} data.firstName - User's first name
* @param {string} data.frontendUrl - Base frontend URL
* @returns {Object} - Email content with subject, htmlBody, textBody
*/
function generateWelcomeEmail(data) {
const { firstName, frontendUrl } = data;
const subject = 'Welcome to spotlight.cam! 🎉';
const htmlBody = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; text-align: center; border-radius: 8px 8px 0 0; }
.content { background: #f9fafb; padding: 30px; border-radius: 0 0 8px 8px; }
.button { display: inline-block; background: #667eea; color: white; padding: 14px 30px; text-decoration: none; border-radius: 6px; margin: 20px 0; font-weight: 600; }
.feature { background: white; padding: 15px; margin: 10px 0; border-radius: 6px; border-left: 4px solid #667eea; }
.footer { text-align: center; color: #666; font-size: 14px; margin-top: 30px; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎉 Welcome to spotlight.cam!</h1>
</div>
<div class="content">
<h2>Hi ${firstName || 'there'}! 👋</h2>
<p>Your email has been verified! You're all set to start using spotlight.cam.</p>
<h3>What you can do now:</h3>
<div class="feature">
<strong>🎪 Join Events</strong><br>
Browse upcoming dance events and join event chat rooms to meet other dancers.
</div>
<div class="feature">
<strong>💬 Match & Chat</strong><br>
Connect with event participants for video collaborations.
</div>
<div class="feature">
<strong>🎥 Share Videos P2P</strong><br>
Exchange dance videos directly with your partners using WebRTC - no server uploads!
</div>
<a href="${frontendUrl}/events" class="button">Explore Events</a>
<p>Happy dancing! 💃🕺</p>
</div>
<div class="footer">
<p>spotlight.cam - P2P video exchange for dance events</p>
<p>Questions? Check out our FAQ or contact support.</p>
</div>
</div>
</body>
</html>
`;
const textBody = `
Hi ${firstName || 'there'}!
Your email has been verified! You're all set to start using spotlight.cam.
What you can do now:
🎪 Join Events
Browse upcoming dance events and join event chat rooms to meet other dancers.
💬 Match & Chat
Connect with event participants for video collaborations.
🎥 Share Videos P2P
Exchange dance videos directly with your partners using WebRTC - no server uploads!
Visit: ${frontendUrl}/events
Happy dancing! 💃🕺
---
spotlight.cam - P2P video exchange for dance events
Questions? Check out our FAQ or contact support.
`;
return { subject, htmlBody, textBody };
}
module.exports = { generateWelcomeEmail };

View File

@@ -606,6 +606,60 @@ async function saveMatchingResults(eventId, suggestions, runId = null) {
// Log error but don't fail the matching operation
console.error('Failed to emit recording suggestion notifications:', socketError);
}
// Send email notifications to recorders who received new suggestions
try {
const { sendRecordingSuggestionsEmail } = require('../emails');
// Get event details (reuse from socket notification if available)
const event = await prisma.event.findUnique({
where: { id: eventId },
select: { slug: true, name: true },
});
// Group suggestions by recorder (same logic as socket notifications)
const suggestionsByRecorder = new Map();
for (const suggestion of newSuggestions) {
// Only notify for assigned suggestions (not NOT_FOUND)
if (suggestion.recorderId && suggestion.status === SUGGESTION_STATUS.PENDING) {
if (!suggestionsByRecorder.has(suggestion.recorderId)) {
suggestionsByRecorder.set(suggestion.recorderId, []);
}
suggestionsByRecorder.get(suggestion.recorderId).push(suggestion);
}
}
// Get recorder details (email, firstName)
if (suggestionsByRecorder.size > 0) {
const recorderIds = Array.from(suggestionsByRecorder.keys());
const recorders = await prisma.user.findMany({
where: { id: { in: recorderIds } },
select: { id: true, email: true, firstName: true, username: true },
});
// Send email to each recorder
for (const recorder of recorders) {
const recorderSuggestions = suggestionsByRecorder.get(recorder.id);
if (recorderSuggestions && recorderSuggestions.length > 0) {
try {
await sendRecordingSuggestionsEmail(
recorder.email,
recorder.firstName || recorder.username,
event?.name || 'Event',
event?.slug || '',
recorderSuggestions.length
);
} catch (emailError) {
// Log error but continue with other emails
console.error(`Failed to send recording suggestions email to ${recorder.email}:`, emailError);
}
}
}
}
} catch (emailError) {
// Log error but don't fail the matching operation
console.error('Failed to send recording suggestion email notifications:', emailError);
}
}
// Update event's matchingRunAt

View File

@@ -1,320 +0,0 @@
/**
* Email Service using AWS SES
* Handles sending emails for verification, password reset, etc.
*/
const { SESClient, SendEmailCommand } = require('@aws-sdk/client-ses');
// Configure AWS SES Client
const sesClient = new SESClient({
region: process.env.AWS_REGION || 'us-east-1',
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
},
});
/**
* Send email via AWS SES
* @param {Object} params - Email parameters
* @param {string} params.to - Recipient email address
* @param {string} params.subject - Email subject
* @param {string} params.htmlBody - HTML email body
* @param {string} params.textBody - Plain text email body (fallback)
* @returns {Promise<Object>} - SES response
*/
async function sendEmail({ to, subject, htmlBody, textBody }) {
const params = {
Source: `${process.env.SES_FROM_NAME} <${process.env.SES_FROM_EMAIL}>`,
Destination: {
ToAddresses: [to],
},
Message: {
Subject: {
Data: subject,
Charset: 'UTF-8',
},
Body: {
Html: {
Data: htmlBody,
Charset: 'UTF-8',
},
Text: {
Data: textBody,
Charset: 'UTF-8',
},
},
},
};
try {
const command = new SendEmailCommand(params);
const response = await sesClient.send(command);
console.log(`Email sent successfully to ${to}. MessageId: ${response.MessageId}`);
return { success: true, messageId: response.MessageId };
} catch (error) {
console.error('Error sending email:', error);
throw new Error(`Failed to send email: ${error.message}`);
}
}
/**
* Send verification email with link and PIN code
* @param {string} email - User email
* @param {string} firstName - User first name
* @param {string} verificationToken - Unique verification token
* @param {string} verificationCode - 6-digit PIN code
*/
async function sendVerificationEmail(email, firstName, verificationToken, verificationCode) {
const verificationLink = `${process.env.FRONTEND_URL}/verify-email?token=${verificationToken}`;
const subject = 'Verify your spotlight.cam email';
const htmlBody = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; text-align: center; border-radius: 8px 8px 0 0; }
.content { background: #f9fafb; padding: 30px; border-radius: 0 0 8px 8px; }
.code-box { background: white; border: 2px solid #667eea; border-radius: 8px; padding: 20px; text-align: center; margin: 20px 0; }
.code { font-size: 32px; font-weight: bold; letter-spacing: 8px; color: #667eea; font-family: monospace; }
.button { display: inline-block; background: #667eea; color: white; padding: 14px 30px; text-decoration: none; border-radius: 6px; margin: 20px 0; font-weight: 600; }
.footer { text-align: center; color: #666; font-size: 14px; margin-top: 30px; }
.divider { border-top: 1px solid #e5e7eb; margin: 20px 0; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎥 spotlight.cam</h1>
<p>Welcome to the dance community!</p>
</div>
<div class="content">
<h2>Hi ${firstName || 'there'}! 👋</h2>
<p>Thanks for joining spotlight.cam! To start sharing dance videos with your event partners, please verify your email address.</p>
<h3>Option 1: Click the button</h3>
<a href="${verificationLink}" class="button">Verify Email Address</a>
<div class="divider"></div>
<h3>Option 2: Enter this code</h3>
<div class="code-box">
<div class="code">${verificationCode}</div>
</div>
<p style="font-size: 14px; color: #666;">This code will expire in 24 hours.</p>
<div class="divider"></div>
<p><strong>Didn't create an account?</strong> You can safely ignore this email.</p>
</div>
<div class="footer">
<p>spotlight.cam - P2P video exchange for dance events</p>
<p>This is an automated email. Please do not reply.</p>
</div>
</div>
</body>
</html>
`;
const textBody = `
Hi ${firstName || 'there'}!
Thanks for joining spotlight.cam! To start sharing dance videos with your event partners, please verify your email address.
Option 1: Click this link to verify
${verificationLink}
Option 2: Enter this verification code
${verificationCode}
This code will expire in 24 hours.
Didn't create an account? You can safely ignore this email.
---
spotlight.cam - P2P video exchange for dance events
This is an automated email. Please do not reply.
`;
return sendEmail({ to: email, subject, htmlBody, textBody });
}
/**
* Send password reset email with link and code
* @param {string} email - User email
* @param {string} firstName - User first name
* @param {string} resetToken - Unique reset token
*/
async function sendPasswordResetEmail(email, firstName, resetToken) {
const resetLink = `${process.env.FRONTEND_URL}/reset-password?token=${resetToken}`;
const subject = 'Reset your spotlight.cam password';
const htmlBody = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; text-align: center; border-radius: 8px 8px 0 0; }
.content { background: #f9fafb; padding: 30px; border-radius: 0 0 8px 8px; }
.button { display: inline-block; background: #667eea; color: white; padding: 14px 30px; text-decoration: none; border-radius: 6px; margin: 20px 0; font-weight: 600; }
.footer { text-align: center; color: #666; font-size: 14px; margin-top: 30px; }
.warning { background: #fef3c7; border-left: 4px solid #f59e0b; padding: 15px; margin: 20px 0; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🔐 Password Reset</h1>
</div>
<div class="content">
<h2>Hi ${firstName || 'there'}! 👋</h2>
<p>We received a request to reset your password for your spotlight.cam account.</p>
<a href="${resetLink}" class="button">Reset Password</a>
<p style="font-size: 14px; color: #666;">This link will expire in 1 hour.</p>
<div class="warning">
<strong>⚠️ Security Notice</strong><br>
If you didn't request this password reset, please ignore this email. Your password will remain unchanged.
</div>
</div>
<div class="footer">
<p>spotlight.cam - P2P video exchange for dance events</p>
<p>This is an automated email. Please do not reply.</p>
</div>
</div>
</body>
</html>
`;
const textBody = `
Hi ${firstName || 'there'}!
We received a request to reset your password for your spotlight.cam account.
Click this link to reset your password:
${resetLink}
This link will expire in 1 hour.
⚠️ Security Notice
If you didn't request this password reset, please ignore this email. Your password will remain unchanged.
---
spotlight.cam - P2P video exchange for dance events
This is an automated email. Please do not reply.
`;
return sendEmail({ to: email, subject, htmlBody, textBody });
}
/**
* Send welcome email after successful verification
* @param {string} email - User email
* @param {string} firstName - User first name
*/
async function sendWelcomeEmail(email, firstName) {
const subject = 'Welcome to spotlight.cam! 🎉';
const htmlBody = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; text-align: center; border-radius: 8px 8px 0 0; }
.content { background: #f9fafb; padding: 30px; border-radius: 0 0 8px 8px; }
.button { display: inline-block; background: #667eea; color: white; padding: 14px 30px; text-decoration: none; border-radius: 6px; margin: 20px 0; font-weight: 600; }
.feature { background: white; padding: 15px; margin: 10px 0; border-radius: 6px; border-left: 4px solid #667eea; }
.footer { text-align: center; color: #666; font-size: 14px; margin-top: 30px; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎉 Welcome to spotlight.cam!</h1>
</div>
<div class="content">
<h2>Hi ${firstName || 'there'}! 👋</h2>
<p>Your email has been verified! You're all set to start using spotlight.cam.</p>
<h3>What you can do now:</h3>
<div class="feature">
<strong>🎪 Join Events</strong><br>
Browse upcoming dance events and join event chat rooms to meet other dancers.
</div>
<div class="feature">
<strong>💬 Match & Chat</strong><br>
Connect with event participants for video collaborations.
</div>
<div class="feature">
<strong>🎥 Share Videos P2P</strong><br>
Exchange dance videos directly with your partners using WebRTC - no server uploads!
</div>
<a href="${process.env.FRONTEND_URL}/events" class="button">Explore Events</a>
<p>Happy dancing! 💃🕺</p>
</div>
<div class="footer">
<p>spotlight.cam - P2P video exchange for dance events</p>
<p>Questions? Check out our FAQ or contact support.</p>
</div>
</div>
</body>
</html>
`;
const textBody = `
Hi ${firstName || 'there'}!
Your email has been verified! You're all set to start using spotlight.cam.
What you can do now:
🎪 Join Events
Browse upcoming dance events and join event chat rooms to meet other dancers.
💬 Match & Chat
Connect with event participants for video collaborations.
🎥 Share Videos P2P
Exchange dance videos directly with your partners using WebRTC - no server uploads!
Visit: ${process.env.FRONTEND_URL}/events
Happy dancing! 💃🕺
---
spotlight.cam - P2P video exchange for dance events
Questions? Check out our FAQ or contact support.
`;
return sendEmail({ to: email, subject, htmlBody, textBody });
}
module.exports = {
sendEmail,
sendVerificationEmail,
sendPasswordResetEmail,
sendWelcomeEmail,
};