Updated site completely
This commit is contained in:
@@ -1,8 +1,16 @@
|
||||
import nodemailer from 'nodemailer';
|
||||
import { RateLimiterMemory } from 'rate-limiter-flexible';
|
||||
import { createHash } from 'crypto';
|
||||
import { getAdminNotificationHtml, getAdminNotificationText, getAdminNotificationSubject } from '../email-templates/admin-notification';
|
||||
import { getUserConfirmationHtml, getUserConfirmationText, getUserConfirmationSubject } from '../email-templates/user-confirmation';
|
||||
import {
|
||||
getAdminNotificationHtml,
|
||||
getAdminNotificationText,
|
||||
getAdminNotificationSubject,
|
||||
} from '../email-templates/admin-notification';
|
||||
import {
|
||||
getUserConfirmationHtml,
|
||||
getUserConfirmationText,
|
||||
getUserConfirmationSubject,
|
||||
} from '../email-templates/user-confirmation';
|
||||
import 'dotenv/config';
|
||||
|
||||
// Environment variables
|
||||
@@ -12,7 +20,7 @@ const {
|
||||
SMTP_USER = '',
|
||||
SMTP_PASS = '',
|
||||
ADMIN_EMAIL = '',
|
||||
WEBSITE_NAME = 'bergsma.it'
|
||||
WEBSITE_NAME = 'bergsma.it',
|
||||
} = process.env;
|
||||
|
||||
// Email configuration
|
||||
@@ -26,11 +34,11 @@ let transporter: nodemailer.Transporter;
|
||||
function initializeTransporter() {
|
||||
if (isProduction && SMTP_HOST && SMTP_USER && SMTP_PASS) {
|
||||
// Production: Use SMTP server
|
||||
|
||||
|
||||
// ProtonMail specific configuration
|
||||
// ProtonMail often requires using their Bridge application for SMTP
|
||||
const isProtonMail = SMTP_HOST.includes('protonmail');
|
||||
|
||||
|
||||
transporter = nodemailer.createTransport({
|
||||
host: SMTP_HOST,
|
||||
port: parseInt(SMTP_PORT, 10),
|
||||
@@ -45,13 +53,13 @@ function initializeTransporter() {
|
||||
// Do not fail on invalid certs
|
||||
rejectUnauthorized: false,
|
||||
// Specific ciphers for ProtonMail
|
||||
ciphers: 'SSLv3'
|
||||
}
|
||||
})
|
||||
ciphers: 'SSLv3',
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
// Verify SMTP connection configuration
|
||||
transporter.verify(function(error, _success) {
|
||||
transporter.verify(function (error, _success) {
|
||||
if (error) {
|
||||
console.error('SMTP connection error:', error);
|
||||
} else {
|
||||
@@ -79,39 +87,37 @@ const csrfTokens = new Map<string, { token: string; expires: Date }>();
|
||||
|
||||
// Generate a CSRF token
|
||||
export function generateCsrfToken(): string {
|
||||
const token = createHash('sha256')
|
||||
.update(Math.random().toString())
|
||||
.digest('hex');
|
||||
|
||||
const token = createHash('sha256').update(Math.random().toString()).digest('hex');
|
||||
|
||||
// Token expires after 1 hour
|
||||
const expires = new Date();
|
||||
expires.setHours(expires.getHours() + 1);
|
||||
|
||||
|
||||
csrfTokens.set(token, { token, expires });
|
||||
|
||||
|
||||
// Clean up expired tokens
|
||||
for (const [key, value] of csrfTokens.entries()) {
|
||||
if (value.expires < new Date()) {
|
||||
csrfTokens.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
// Validate a CSRF token
|
||||
export function validateCsrfToken(token: string): boolean {
|
||||
const storedToken = csrfTokens.get(token);
|
||||
|
||||
|
||||
if (!storedToken) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
if (storedToken.expires < new Date()) {
|
||||
csrfTokens.delete(token);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -140,18 +146,13 @@ export async function checkRateLimit(ipAddress: string): Promise<{ limited: bool
|
||||
}
|
||||
|
||||
// Log email sending attempts
|
||||
export function logEmailAttempt(
|
||||
success: boolean,
|
||||
recipient: string,
|
||||
subject: string,
|
||||
error?: Error
|
||||
): void {
|
||||
export function logEmailAttempt(success: boolean, recipient: string, subject: string, error?: Error): void {
|
||||
const timestamp = new Date().toISOString();
|
||||
const status = success ? 'SUCCESS' : 'FAILURE';
|
||||
const errorMessage = error ? `: ${error.message}` : '';
|
||||
|
||||
|
||||
const logMessage = `[${timestamp}] [EMAIL ${status}] To: ${recipient}, Subject: ${subject}${errorMessage}`;
|
||||
|
||||
|
||||
if (isProduction) {
|
||||
// In production, you might want to log to a file or a logging service
|
||||
console.log(logMessage);
|
||||
@@ -162,23 +163,16 @@ export function logEmailAttempt(
|
||||
}
|
||||
|
||||
// Send an email
|
||||
export async function sendEmail(
|
||||
to: string,
|
||||
subject: string,
|
||||
html: string,
|
||||
text: string
|
||||
): Promise<boolean> {
|
||||
export async function sendEmail(to: string, subject: string, html: string, text: string): Promise<boolean> {
|
||||
// Initialize transporter if not already done
|
||||
if (!transporter) {
|
||||
initializeTransporter();
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
// Ensure from address matches SMTP_USER for ProtonMail
|
||||
const fromAddress = isProduction ?
|
||||
`"${WEBSITE_NAME}" <${SMTP_USER}>` :
|
||||
`"${WEBSITE_NAME}" <${ADMIN_EMAIL}>`;
|
||||
|
||||
const fromAddress = isProduction ? `"${WEBSITE_NAME}" <${SMTP_USER}>` : `"${WEBSITE_NAME}" <${ADMIN_EMAIL}>`;
|
||||
|
||||
const mailOptions = {
|
||||
from: fromAddress,
|
||||
to,
|
||||
@@ -186,24 +180,23 @@ export async function sendEmail(
|
||||
html,
|
||||
text,
|
||||
};
|
||||
|
||||
|
||||
await transporter.sendMail(mailOptions);
|
||||
|
||||
|
||||
|
||||
logEmailAttempt(true, to, subject);
|
||||
return true;
|
||||
} catch (error) {
|
||||
logEmailAttempt(false, to, subject, error as Error);
|
||||
|
||||
|
||||
// Enhanced error logging for SMTP issues
|
||||
if (isProduction) {
|
||||
console.error('Error sending email:', error);
|
||||
|
||||
|
||||
// Log more detailed information for SMTP errors
|
||||
if (error instanceof Error) {
|
||||
console.error('Error name:', error.name);
|
||||
console.error('Error message:', error.message);
|
||||
|
||||
|
||||
// Log additional details for specific error types
|
||||
if (error.name === 'Error' && error.message.includes('ECONNREFUSED')) {
|
||||
console.error('SMTP Connection Refused: Check if the SMTP server is reachable and the port is correct');
|
||||
@@ -214,7 +207,7 @@ export async function sendEmail(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -232,22 +225,22 @@ export async function sendAdminNotification(
|
||||
console.error('Cannot send admin notification: name is empty');
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
if (!email || email.trim() === '') {
|
||||
console.error('Cannot send admin notification: email is empty');
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
if (!message || message.trim() === '') {
|
||||
console.error('Cannot send admin notification: message is empty');
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
if (!ADMIN_EMAIL || ADMIN_EMAIL.trim() === '') {
|
||||
console.error('Cannot send admin notification: ADMIN_EMAIL is not configured');
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
const submittedAt = new Date().toLocaleString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
@@ -255,7 +248,7 @@ export async function sendAdminNotification(
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
|
||||
|
||||
const props = {
|
||||
name,
|
||||
email,
|
||||
@@ -264,30 +257,26 @@ export async function sendAdminNotification(
|
||||
ipAddress,
|
||||
userAgent,
|
||||
};
|
||||
|
||||
|
||||
const subject = getAdminNotificationSubject();
|
||||
const html = getAdminNotificationHtml(props);
|
||||
const text = getAdminNotificationText(props);
|
||||
|
||||
|
||||
// Add a backup email address to ensure delivery
|
||||
const recipients = ADMIN_EMAIL;
|
||||
// Uncomment and modify the line below to add a backup email address
|
||||
// const recipients = `${ADMIN_EMAIL}, your-backup-email@example.com`;
|
||||
|
||||
|
||||
return sendEmail(recipients, subject, html, text);
|
||||
}
|
||||
|
||||
// Send user confirmation email
|
||||
export async function sendUserConfirmation(
|
||||
name: string,
|
||||
email: string,
|
||||
message: string
|
||||
): Promise<boolean> {
|
||||
export async function sendUserConfirmation(name: string, email: string, message: string): Promise<boolean> {
|
||||
if (!email || email.trim() === '') {
|
||||
console.error('Cannot send user confirmation: email is empty');
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
const submittedAt = new Date().toLocaleString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
@@ -295,7 +284,7 @@ export async function sendUserConfirmation(
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
|
||||
|
||||
const props = {
|
||||
name,
|
||||
email,
|
||||
@@ -304,11 +293,11 @@ export async function sendUserConfirmation(
|
||||
websiteName: WEBSITE_NAME,
|
||||
contactEmail: ADMIN_EMAIL,
|
||||
};
|
||||
|
||||
|
||||
const subject = getUserConfirmationSubject(WEBSITE_NAME);
|
||||
const html = getUserConfirmationHtml(props);
|
||||
const text = getUserConfirmationText(props);
|
||||
|
||||
|
||||
return sendEmail(email, subject, html, text);
|
||||
}
|
||||
|
||||
@@ -325,16 +314,16 @@ export async function testEmailConfiguration(): Promise<boolean> {
|
||||
if (!isProduction) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
// Initialize transporter if not already done
|
||||
if (!transporter) {
|
||||
initializeTransporter();
|
||||
}
|
||||
|
||||
|
||||
// Verify connection to SMTP server
|
||||
const connectionResult = await new Promise<boolean>((resolve) => {
|
||||
transporter.verify(function(error, _success) {
|
||||
transporter.verify(function (error, _success) {
|
||||
if (error) {
|
||||
resolve(false);
|
||||
} else {
|
||||
@@ -342,11 +331,11 @@ export async function testEmailConfiguration(): Promise<boolean> {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
if (!connectionResult) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
@@ -356,4 +345,4 @@ export async function testEmailConfiguration(): Promise<boolean> {
|
||||
// Run a test of the email configuration
|
||||
if (isProduction) {
|
||||
testEmailConfiguration();
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user