feat: Add email service for authentication flows, including email verification and password reset.
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
import nodemailer from 'nodemailer';
|
||||
import { config } from '../config/config';
|
||||
|
||||
export class EmailService {
|
||||
private transporter: nodemailer.Transporter;
|
||||
|
||||
constructor() {
|
||||
this.transporter = nodemailer.createTransport({
|
||||
host: config.email.host,
|
||||
port: config.email.port,
|
||||
secure: config.email.port === 465,
|
||||
auth: {
|
||||
user: config.email.user,
|
||||
pass: config.email.password,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async sendVerificationEmail(to: string, token: string) {
|
||||
const verificationLink = `${config.frontendUrl}/verify/${token}`;
|
||||
|
||||
const content = `
|
||||
<p>Please click the button below to verify your email address:</p>
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<a href="${verificationLink}" style="background-color: #4CAF50; color: white; padding: 14px 24px; text-align: center; text-decoration: none; display: inline-block; border-radius: 4px; font-weight: bold; font-size: 16px;">Verify Email</a>
|
||||
</div>
|
||||
<p>Or copy and paste this link into your browser:</p>
|
||||
<p style="word-break: break-all; color: #666;">${verificationLink}</p>
|
||||
<p>This link will expire in 24 hours.</p>
|
||||
`;
|
||||
|
||||
const html = this.getHtmlTemplate('Verify your email address', content);
|
||||
|
||||
await this.sendEmail(to, 'Verify your email', html);
|
||||
}
|
||||
|
||||
async sendPasswordResetEmail(to: string, token: string) {
|
||||
const resetLink = `${config.frontendUrl}/reset-password/${token}`;
|
||||
|
||||
const content = `
|
||||
<p>You requested a password reset. Click the button below to reset your password:</p>
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<a href="${resetLink}" style="background-color: #008CBA; color: white; padding: 14px 24px; text-align: center; text-decoration: none; display: inline-block; border-radius: 4px; font-weight: bold; font-size: 16px;">Reset Password</a>
|
||||
</div>
|
||||
<p>Or copy and paste this link into your browser:</p>
|
||||
<p style="word-break: break-all; color: #666;">${resetLink}</p>
|
||||
<p>If you didn't request this, please ignore this email.</p>
|
||||
`;
|
||||
|
||||
const html = this.getHtmlTemplate('Reset your password', content);
|
||||
|
||||
await this.sendEmail(to, 'Reset your password', html);
|
||||
}
|
||||
|
||||
private getHtmlTemplate(title: string, content: string): string {
|
||||
const logoUrl = 'https://res.cloudinary.com/dmxoohiwo/image/upload/v1764315105/logo_fpczsd.png';
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>${title}</title>
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; background-color: #f4f4f4; color: #333; line-height: 1.6;">
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color: #f4f4f4; padding: 20px 0;">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="600" style="background-color: #ffffff; border-radius: 8px; overflow: hidden; box-shadow: 0 4px 6px rgba(0,0,0,0.05);">
|
||||
<!-- Header -->
|
||||
<tr>
|
||||
<td align="center" style="padding: 30px 0; background-color: #ffffff; border-bottom: 1px solid #eee;">
|
||||
<img src="${logoUrl}" alt="Zemenu Logo" width="150" style="display: block; border: 0;">
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Content -->
|
||||
<tr>
|
||||
<td style="padding: 40px 30px;">
|
||||
<h1 style="margin-top: 0; margin-bottom: 20px; font-size: 24px; color: #333; text-align: center;">${title}</h1>
|
||||
${content}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Footer -->
|
||||
<tr>
|
||||
<td style="background-color: #f8f9fa; padding: 20px 30px; text-align: center; border-top: 1px solid #eee;">
|
||||
<p style="margin: 0; font-size: 14px; color: #888;">© ${currentYear} Zemenu. All rights reserved.</p>
|
||||
<p style="margin: 10px 0 0; font-size: 12px; color: #aaa;">
|
||||
This is an automated message, please do not reply to this email.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
private async sendEmail(to: string, subject: string, html: string) {
|
||||
if (!config.email.user || !config.email.password) {
|
||||
console.warn('Email credentials not provided. Skipping email sending.');
|
||||
console.log(`To: ${to}, Subject: ${subject}`);
|
||||
// For development, we might want to log the link
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.transporter.sendMail({
|
||||
from: config.email.from,
|
||||
to,
|
||||
subject,
|
||||
html,
|
||||
});
|
||||
console.log(`Email sent to ${to}`);
|
||||
} catch (error) {
|
||||
console.error('Error sending email:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const emailService = new EmailService();
|
||||
Reference in New Issue
Block a user