From 49fabddc9687fcbea40711cefa2d75cb222a4243 Mon Sep 17 00:00:00 2001 From: Richard Bergsma Date: Thu, 26 Jun 2025 22:54:02 +0200 Subject: [PATCH] Refactor CookieBanner and Contact API for improved functionality and security - Removed localStorage fallback from CookieBanner, simplifying consent management. - Refactored manual review email handling in the Contact API to utilize HTML templates for better structure and security. - Enhanced email content generation by escaping HTML special characters and using template files for dynamic data insertion. --- src/components/CookieBanner.astro | 23 +-- src/pages/api/contact.ts | 20 +- src/pages/api/contact/manual-review.ts | 28 ++- src/templates/email/admin-notification.html | 95 +++++++++ src/templates/email/manual-review.html | 1 + src/templates/email/user-confirmation.html | 81 ++++++++ src/utils/email-handler.ts | 213 +++----------------- 7 files changed, 226 insertions(+), 235 deletions(-) create mode 100644 src/templates/email/admin-notification.html create mode 100644 src/templates/email/manual-review.html create mode 100644 src/templates/email/user-confirmation.html diff --git a/src/components/CookieBanner.astro b/src/components/CookieBanner.astro index 7e7d547..2e4aa8e 100644 --- a/src/components/CookieBanner.astro +++ b/src/components/CookieBanner.astro @@ -69,19 +69,6 @@ const t = getTranslation(lang); return; } - // Also check localStorage as a fallback - try { - if (localStorage && localStorage.getItem('cookieConsentAccepted') === 'true') { - cookieBanner.style.display = 'none'; - // Also set the cookie for future visits - setCookie('cookieConsentAccepted', 'true', 365); - return; - } - } catch (e) { - console.error('Error accessing localStorage:', e); - // Continue checking cookies - } - // Show the banner cookieBanner.style.display = 'block'; @@ -93,17 +80,9 @@ const t = getTranslation(lang); // Handle accept button click acceptButton.addEventListener('click', () => { - // Store consent in cookie (primary storage) + // Store consent in cookie setCookie('cookieConsentAccepted', 'true', 365); - // Also store in localStorage as backup - try { - localStorage.setItem('cookieConsentAccepted', 'true'); - } catch (e) { - console.error('Error setting localStorage:', e); - // Continue with cookie storage - } - // Hide the banner with animation cookieBanner.classList.add('translate-y-full'); diff --git a/src/pages/api/contact.ts b/src/pages/api/contact.ts index e4716a1..2cb4ae4 100644 --- a/src/pages/api/contact.ts +++ b/src/pages/api/contact.ts @@ -311,22 +311,4 @@ export const POST: APIRoute = async ({ request, clientAddress }) => { } }; -export const manualReviewPOST: APIRoute = async ({ request }) => { - const { token, email: submittedEmail, justification } = await request.json(); - try { - const payload = jwt.verify(token, MANUAL_REVIEW_SECRET) as { email: string, message: string }; - if (payload.email !== submittedEmail) { - return new Response(JSON.stringify({ error: 'Email does not match original submission.' }), { status: 403 }); - } - // Send to manual review mailbox - await sendEmail( - MANUAL_REVIEW_EMAIL, - 'Manual Review Requested: Contact Form Submission', - `

Email: ${submittedEmail}

Message: ${payload.message}

Justification: ${justification || 'None provided'}

`, - `Email: ${submittedEmail}\nMessage: ${payload.message}\nJustification: ${justification || 'None provided'}` - ); - return new Response(JSON.stringify({ success: true })); - } catch (_err) { - return new Response(JSON.stringify({ error: 'Invalid or expired token.' }), { status: 400 }); - } -}; + diff --git a/src/pages/api/contact/manual-review.ts b/src/pages/api/contact/manual-review.ts index e3aa42f..e5cba98 100644 --- a/src/pages/api/contact/manual-review.ts +++ b/src/pages/api/contact/manual-review.ts @@ -1,18 +1,19 @@ import type { APIRoute } from 'astro'; import jwt from 'jsonwebtoken'; -import { sendEmail } from '../../../utils/email-handler'; +import { sendEmail, escapeHtml } from '../../../utils/email-handler'; +import fs from 'fs/promises'; +import path from 'path'; const MANUAL_REVIEW_SECRET = process.env.MANUAL_REVIEW_SECRET; const MANUAL_REVIEW_EMAIL = 'manual-review@365devnet.eu'; -// Utility to escape HTML special characters -function escapeHtml(str: string): string { - return str - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); +async function getTemplate(templateName: string, data: Record): Promise { + const templatePath = path.join(process.cwd(), 'src', 'templates', 'email', `${templateName}.html`); + let template = await fs.readFile(templatePath, 'utf-8'); + for (const key in data) { + template = template.replace(new RegExp(`{{${key}}}`, 'g'), data[key]); + } + return template; } export const POST: APIRoute = async ({ request }) => { @@ -23,10 +24,15 @@ export const POST: APIRoute = async ({ request }) => { return new Response(JSON.stringify({ error: 'Email does not match original submission.' }), { status: 403 }); } // Send to manual review mailbox + const html = await getTemplate('manual-review', { + email: escapeHtml(submittedEmail), + message: escapeHtml(payload.message), + justification: escapeHtml(justification || 'None provided'), + }); await sendEmail( MANUAL_REVIEW_EMAIL, 'Manual Review Requested: Contact Form Submission', - `

Email: ${escapeHtml(submittedEmail)}

Message: ${escapeHtml(payload.message)}

Justification: ${escapeHtml(justification || 'None provided')}

`, + html, `Email: ${submittedEmail}\nMessage: ${payload.message}\nJustification: ${justification || 'None provided'}` ); return new Response(JSON.stringify({ success: true })); @@ -34,4 +40,6 @@ export const POST: APIRoute = async ({ request }) => { return new Response(JSON.stringify({ error: 'Invalid or expired token.' }), { status: 400 }); } }; + + \ No newline at end of file diff --git a/src/templates/email/admin-notification.html b/src/templates/email/admin-notification.html new file mode 100644 index 0000000..6360d99 --- /dev/null +++ b/src/templates/email/admin-notification.html @@ -0,0 +1,95 @@ + + + + + + New Contact Form Submission + + + +
+

New Contact Form Submission

+
+
+
+
Name
+
{{name}}
+
+
+
Email
+
{{email}}
+
+
+
Message
+
{{message}}
+
+
+
IP Address: {{ipAddress}}
+
User Agent: {{userAgent}}
+
Time: {{time}}
+
+ +
+ + \ No newline at end of file diff --git a/src/templates/email/manual-review.html b/src/templates/email/manual-review.html new file mode 100644 index 0000000..1a6de4f --- /dev/null +++ b/src/templates/email/manual-review.html @@ -0,0 +1 @@ +

Email: {{email}}

Message: {{message}}

Justification: {{justification}}

\ No newline at end of file diff --git a/src/templates/email/user-confirmation.html b/src/templates/email/user-confirmation.html new file mode 100644 index 0000000..eb111e8 --- /dev/null +++ b/src/templates/email/user-confirmation.html @@ -0,0 +1,81 @@ + + + + + + Thank you for your message + + + +
+

Thank you for your message

+
+
+

Dear {{name}},

+

Thank you for contacting {{websiteName}}. We have received your message and will get back to you as soon as possible.

+ +
+

Your Message:

+

{{message}}

+
+ +

If you have any additional information to share, please don't hesitate to reply to this email.

+ + Visit Our Website + + +
+ + \ No newline at end of file diff --git a/src/utils/email-handler.ts b/src/utils/email-handler.ts index 41c6756..bbc209a 100644 --- a/src/utils/email-handler.ts +++ b/src/utils/email-handler.ts @@ -2,6 +2,8 @@ import nodemailer from 'nodemailer'; import { RateLimiterMemory } from 'rate-limiter-flexible'; import { createHash } from 'crypto'; import 'dotenv/config'; +import fs from 'fs/promises'; +import path from 'path'; // Environment variables const { @@ -14,8 +16,7 @@ const { } = process.env; // Email configuration -// Force production mode for testing -const isProduction = true; // NODE_ENV === 'production'; +const isProduction = process.env.NODE_ENV === 'production'; // Create a transporter for sending emails let transporter: nodemailer.Transporter; @@ -172,7 +173,7 @@ export async function sendEmail(to: string, subject: string, html: string, text: } // Utility to escape HTML special characters -function escapeHtml(str: string): string { +export function escapeHtml(str: string): string { return str .replace(/&/g, '&') .replace(/): Promise { + const templatePath = path.join(process.cwd(), 'src', 'templates', 'email', `${templateName}.html`); + let template = await fs.readFile(templatePath, 'utf-8'); + for (const key in data) { + template = template.replace(new RegExp(`{{${key}}}`, 'g'), data[key]); + } + return template; +} + // Send admin notification email export async function sendAdminNotification( name: string, @@ -206,103 +216,15 @@ export async function sendAdminNotification( } const subject = `New Contact Form Submission from ${escapeHtml(name)}`; - const html = ` - - - - - - New Contact Form Submission - - - -
-

New Contact Form Submission

-
-
-
-
Name
-
${escapeHtml(name)}
-
-
-
Email
-
${escapeHtml(email)}
-
-
-
Message
-
${escapeHtml(message).replace(/\n/g, '
')}
-
-
- ${ipAddress ? `
IP Address: ${escapeHtml(ipAddress)}
` : ''} - ${userAgent ? `
User Agent: ${escapeHtml(userAgent)}
` : ''} -
Time: ${new Date().toLocaleString()}
-
- -
- - - `; + const html = await getTemplate('admin-notification', { + name: escapeHtml(name), + email: escapeHtml(email), + message: escapeHtml(message).replace(/\n/g, '
'), + ipAddress: escapeHtml(ipAddress || ''), + userAgent: escapeHtml(userAgent || ''), + time: new Date().toLocaleString(), + websiteName: WEBSITE_NAME, + }); const text = ` New Contact Form Submission @@ -334,89 +256,11 @@ export async function sendUserConfirmation(name: string, email: string, message: } const subject = `Thank you for contacting ${WEBSITE_NAME}`; - const html = ` - - - - - - Thank you for your message - - - -
-

Thank you for your message

-
-
-

Dear ${escapeHtml(name)},

-

Thank you for contacting ${WEBSITE_NAME}. We have received your message and will get back to you as soon as possible.

- -
-

Your Message:

-

${escapeHtml(message).replace(/\n/g, '
')}

-
- -

If you have any additional information to share, please don't hesitate to reply to this email.

- - Visit Our Website - - -
- - - `; + const html = await getTemplate('user-confirmation', { + name: escapeHtml(name), + message: escapeHtml(message).replace(/\n/g, '
'), + websiteName: WEBSITE_NAME, + }); const text = ` Thank you for your message @@ -457,4 +301,5 @@ export async function testEmailConfiguration(): Promise { console.error('Email configuration test failed:', error); return false; } -} \ No newline at end of file +} + \ No newline at end of file