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.
This commit is contained in:
@@ -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');
|
||||
|
||||
|
@@ -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',
|
||||
`<p><strong>Email:</strong> ${submittedEmail}</p><p><strong>Message:</strong> ${payload.message}</p><p><strong>Justification:</strong> ${justification || 'None provided'}</p>`,
|
||||
`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 });
|
||||
}
|
||||
};
|
||||
|
||||
|
@@ -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, '"')
|
||||
.replace(/'/g, ''');
|
||||
async function getTemplate(templateName: string, data: Record<string, string>): Promise<string> {
|
||||
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',
|
||||
`<p><strong>Email:</strong> ${escapeHtml(submittedEmail)}</p><p><strong>Message:</strong> ${escapeHtml(payload.message)}</p><p><strong>Justification:</strong> ${escapeHtml(justification || 'None provided')}</p>`,
|
||||
html,
|
||||
`Email: ${submittedEmail}\nMessage: ${payload.message}\nJustification: ${justification || 'None provided'}`
|
||||
);
|
||||
return new Response(JSON.stringify({ success: true }));
|
||||
@@ -35,3 +41,5 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
95
src/templates/email/admin-notification.html
Normal file
95
src/templates/email/admin-notification.html
Normal file
@@ -0,0 +1,95 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>New Contact Form Submission</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.header {
|
||||
background-color: #2563eb;
|
||||
color: white;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
.content {
|
||||
background-color: #f8fafc;
|
||||
padding: 20px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-top: none;
|
||||
border-radius: 0 0 8px 8px;
|
||||
}
|
||||
.field {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.field-label {
|
||||
font-weight: 600;
|
||||
color: #4b5563;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.field-value {
|
||||
background-color: white;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
.message-content {
|
||||
white-space: pre-wrap;
|
||||
background-color: white;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e2e8f0;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.footer {
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
font-size: 0.9em;
|
||||
color: #6b7280;
|
||||
}
|
||||
.meta-info {
|
||||
font-size: 0.85em;
|
||||
color: #6b7280;
|
||||
margin-top: 20px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>New Contact Form Submission</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="field">
|
||||
<div class="field-label">Name</div>
|
||||
<div class="field-value">{{name}}</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="field-label">Email</div>
|
||||
<div class="field-value">{{email}}</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="field-label">Message</div>
|
||||
<div class="message-content">{{message}}</div>
|
||||
</div>
|
||||
<div class="meta-info">
|
||||
<div><strong>IP Address:</strong> {{ipAddress}}</div>
|
||||
<div><strong>User Agent:</strong> {{userAgent}}</div>
|
||||
<div><strong>Time:</strong> {{time}}</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>This message was sent from the contact form on {{websiteName}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
1
src/templates/email/manual-review.html
Normal file
1
src/templates/email/manual-review.html
Normal file
@@ -0,0 +1 @@
|
||||
<p><strong>Email:</strong> {{email}}</p><p><strong>Message:</strong> {{message}}</p><p><strong>Justification:</strong> {{justification}}</p>
|
81
src/templates/email/user-confirmation.html
Normal file
81
src/templates/email/user-confirmation.html
Normal file
@@ -0,0 +1,81 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Thank you for your message</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.header {
|
||||
background-color: #2563eb;
|
||||
color: white;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
.content {
|
||||
background-color: #f8fafc;
|
||||
padding: 20px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-top: none;
|
||||
border-radius: 0 0 8px 8px;
|
||||
}
|
||||
.message {
|
||||
background-color: white;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e2e8f0;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.footer {
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
font-size: 0.9em;
|
||||
color: #6b7280;
|
||||
}
|
||||
.button {
|
||||
display: inline-block;
|
||||
background-color: #2563eb;
|
||||
color: white;
|
||||
padding: 12px 24px;
|
||||
text-decoration: none;
|
||||
border-radius: 6px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.button:hover {
|
||||
background-color: #1d4ed8;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>Thank you for your message</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>Dear {{name}},</p>
|
||||
<p>Thank you for contacting {{websiteName}}. We have received your message and will get back to you as soon as possible.</p>
|
||||
|
||||
<div class="message">
|
||||
<h3>Your Message:</h3>
|
||||
<p>{{message}}</p>
|
||||
</div>
|
||||
|
||||
<p>If you have any additional information to share, please don't hesitate to reply to this email.</p>
|
||||
|
||||
<a href="https://www.365devnet.eu" class="button">Visit Our Website</a>
|
||||
|
||||
<div class="footer">
|
||||
<p>Best regards,<br>{{websiteName}} Team</p>
|
||||
<p><small>This is an automated message, please do not reply directly to this email.</small></p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@@ -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(/</g, '<')
|
||||
@@ -181,6 +182,15 @@ function escapeHtml(str: string): string {
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
async function getTemplate(templateName: string, data: Record<string, string>): Promise<string> {
|
||||
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 = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>New Contact Form Submission</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.header {
|
||||
background-color: #2563eb;
|
||||
color: white;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
.content {
|
||||
background-color: #f8fafc;
|
||||
padding: 20px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-top: none;
|
||||
border-radius: 0 0 8px 8px;
|
||||
}
|
||||
.field {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.field-label {
|
||||
font-weight: 600;
|
||||
color: #4b5563;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.field-value {
|
||||
background-color: white;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
.message-content {
|
||||
white-space: pre-wrap;
|
||||
background-color: white;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e2e8f0;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.footer {
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
font-size: 0.9em;
|
||||
color: #6b7280;
|
||||
}
|
||||
.meta-info {
|
||||
font-size: 0.85em;
|
||||
color: #6b7280;
|
||||
margin-top: 20px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>New Contact Form Submission</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="field">
|
||||
<div class="field-label">Name</div>
|
||||
<div class="field-value">${escapeHtml(name)}</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="field-label">Email</div>
|
||||
<div class="field-value">${escapeHtml(email)}</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="field-label">Message</div>
|
||||
<div class="message-content">${escapeHtml(message).replace(/\n/g, '<br>')}</div>
|
||||
</div>
|
||||
<div class="meta-info">
|
||||
${ipAddress ? `<div><strong>IP Address:</strong> ${escapeHtml(ipAddress)}</div>` : ''}
|
||||
${userAgent ? `<div><strong>User Agent:</strong> ${escapeHtml(userAgent)}</div>` : ''}
|
||||
<div><strong>Time:</strong> ${new Date().toLocaleString()}</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>This message was sent from the contact form on ${WEBSITE_NAME}</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
const html = await getTemplate('admin-notification', {
|
||||
name: escapeHtml(name),
|
||||
email: escapeHtml(email),
|
||||
message: escapeHtml(message).replace(/\n/g, '<br>'),
|
||||
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 = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Thank you for your message</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.header {
|
||||
background-color: #2563eb;
|
||||
color: white;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
.content {
|
||||
background-color: #f8fafc;
|
||||
padding: 20px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-top: none;
|
||||
border-radius: 0 0 8px 8px;
|
||||
}
|
||||
.message {
|
||||
background-color: white;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e2e8f0;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.footer {
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
font-size: 0.9em;
|
||||
color: #6b7280;
|
||||
}
|
||||
.button {
|
||||
display: inline-block;
|
||||
background-color: #2563eb;
|
||||
color: white;
|
||||
padding: 12px 24px;
|
||||
text-decoration: none;
|
||||
border-radius: 6px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.button:hover {
|
||||
background-color: #1d4ed8;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>Thank you for your message</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>Dear ${escapeHtml(name)},</p>
|
||||
<p>Thank you for contacting ${WEBSITE_NAME}. We have received your message and will get back to you as soon as possible.</p>
|
||||
|
||||
<div class="message">
|
||||
<h3>Your Message:</h3>
|
||||
<p>${escapeHtml(message).replace(/\n/g, '<br>')}</p>
|
||||
</div>
|
||||
|
||||
<p>If you have any additional information to share, please don't hesitate to reply to this email.</p>
|
||||
|
||||
<a href="https://www.365devnet.eu" class="button">Visit Our Website</a>
|
||||
|
||||
<div class="footer">
|
||||
<p>Best regards,<br>${WEBSITE_NAME} Team</p>
|
||||
<p><small>This is an automated message, please do not reply directly to this email.</small></p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
const html = await getTemplate('user-confirmation', {
|
||||
name: escapeHtml(name),
|
||||
message: escapeHtml(message).replace(/\n/g, '<br>'),
|
||||
websiteName: WEBSITE_NAME,
|
||||
});
|
||||
const text = `
|
||||
Thank you for your message
|
||||
|
||||
@@ -458,3 +302,4 @@ export async function testEmailConfiguration(): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user