Updated site completely
Some checks failed
GitHub Actions / build (18) (push) Has been cancelled
GitHub Actions / build (20) (push) Has been cancelled
GitHub Actions / build (22) (push) Has been cancelled
GitHub Actions / check (push) Has been cancelled

This commit is contained in:
becarta
2025-03-29 22:32:31 +01:00
parent a9adf1bb4f
commit 890d7b8670
56 changed files with 1807 additions and 1299 deletions

View File

@@ -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();
}
}