Enhance ContactForm and CookieBanner components for improved accessibility and user feedback

- Added CSRF token handling in the ContactForm for enhanced security.
- Introduced a feedback div for displaying form submission results instead of alerts.
- Updated the CookieBanner to include ARIA roles and improved focus management for better accessibility.
- Refactored manual review email handling to escape HTML special characters, enhancing security.
This commit is contained in:
2025-06-26 22:38:06 +02:00
parent dde3fb1923
commit cb64f7f76c
4 changed files with 48 additions and 10 deletions

View File

@@ -1,4 +1,5 @@
<form id="contact-form">
<input type="hidden" name="csrf_token" id="csrf_token" />
<label for="email">Email</label>
<input type="email" name="email" id="email" required aria-describedby="email-help" />
<div id="email-help" class="sr-only">Enter your email address</div>
@@ -8,7 +9,9 @@
<button type="submit">Send</button>
</form>
<div id="spam-warning" style="display:none;">
<div id="form-feedback" role="alert" aria-live="assertive"></div>
<div id="spam-warning" style="display:none;" tabindex="-1">
<p>
Your message was detected as spam and was not sent.<br>
If you believe this is a mistake, you can request a manual review.
@@ -16,7 +19,7 @@
<form id="manual-review-form">
<label for="manual-email">Email</label>
<input type="email" id="manual-email" readonly aria-describedby="manual-email-help" />
<div id="manual-email-help" class="sr-only">Re-enter your email address for confirmation</div>
<div id="manual-email-help" class="sr-only">Your email address.</div>
<label for="manual-justification">Why is this not spam? (optional)</label>
<textarea id="manual-justification" placeholder="Why is this not spam? (optional)" aria-describedby="manual-justification-help"></textarea>
<div id="manual-justification-help" class="sr-only">Explain why your message is legitimate</div>
@@ -27,8 +30,25 @@
</div>
<script>
async function fetchCsrfToken() {
try {
const response = await fetch('/api/contact?csrf=true');
const data = await response.json();
const csrfTokenInput = document.getElementById('csrf_token');
if (csrfTokenInput) {
(csrfTokenInput as HTMLInputElement).value = data.csrfToken;
}
} catch (error) {
console.error('Error fetching CSRF token:', error);
}
}
document.addEventListener('DOMContentLoaded', fetchCsrfToken);
const contactForm = document.getElementById('contact-form');
if (contactForm) {
const feedbackDiv = document.getElementById('form-feedback');
if (contactForm && feedbackDiv) {
contactForm.onsubmit = async function (e) {
e.preventDefault();
const formData = new FormData(this as HTMLFormElement);
@@ -38,7 +58,7 @@ if (contactForm) {
data = await res.json();
console.log('Contact API response:', data);
} catch (_err) {
alert('Unexpected server response.');
feedbackDiv.textContent = 'Unexpected server response.';
return;
}
@@ -51,14 +71,15 @@ if (contactForm) {
spamWarning.style.display = 'block';
manualEmail.value = String(formData.get('email'));
manualToken.value = data.token;
spamWarning.focus();
}
return;
}
if (!res.ok) {
alert(data.error || 'There was an error sending your message.');
feedbackDiv.textContent = data.error || 'There was an error sending your message.';
} else {
alert('Your message was sent successfully!');
feedbackDiv.textContent = 'Your message was sent successfully!';
}
};
}

View File

@@ -9,9 +9,14 @@ const t = getTranslation(lang);
id="cookie-banner"
class="fixed bottom-0 left-0 right-0 z-50 p-4 content-backdrop shadow-lg transform transition-transform duration-300 translate-y-full"
style="display: none;"
role="dialog"
aria-modal="true"
aria-labelledby="cookie-banner-title"
tabindex="-1"
>
<div class="container mx-auto max-w-6xl flex flex-col sm:flex-row items-center justify-between gap-4">
<div class="text-sm text-gray-800 dark:text-gray-200 font-medium">
<h2 id="cookie-banner-title" class="sr-only">Cookie Consent</h2>
<p>
{t.cookies.message}
<a href={`/${lang}/privacy#cookie-usage`} class="text-blue-600 dark:text-blue-400 hover:underline"
@@ -83,6 +88,7 @@ const t = getTranslation(lang);
// Show the banner with a slight delay for better UX
setTimeout(() => {
cookieBanner.classList.remove('translate-y-full');
cookieBanner.focus();
}, 500);
// Handle accept button click

View File

@@ -10,7 +10,7 @@ import {
import { isSpamWithGemini } from "../../utils/gemini-spam-check";
import jwt from 'jsonwebtoken';
const MANUAL_REVIEW_SECRET = process.env.MANUAL_REVIEW_SECRET || 'dev-secret';
const MANUAL_REVIEW_SECRET = process.env.MANUAL_REVIEW_SECRET;
const MANUAL_REVIEW_EMAIL = 'manual-review@365devnet.eu';
// Enhanced email validation with more comprehensive regex

View File

@@ -2,9 +2,19 @@ import type { APIRoute } from 'astro';
import jwt from 'jsonwebtoken';
import { sendEmail } from '../../../utils/email-handler';
const MANUAL_REVIEW_SECRET = process.env.MANUAL_REVIEW_SECRET || 'dev-secret';
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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
export const POST: APIRoute = async ({ request }) => {
const { token, email: submittedEmail, justification } = await request.json();
try {
@@ -16,7 +26,7 @@ export const POST: APIRoute = async ({ request }) => {
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>`,
`<p><strong>Email:</strong> ${escapeHtml(submittedEmail)}</p><p><strong>Message:</strong> ${escapeHtml(payload.message)}</p><p><strong>Justification:</strong> ${escapeHtml(justification || 'None provided')}</p>`,
`Email: ${submittedEmail}\nMessage: ${payload.message}\nJustification: ${justification || 'None provided'}`
);
return new Response(JSON.stringify({ success: true }));
@@ -24,3 +34,4 @@ export const POST: APIRoute = async ({ request }) => {
return new Response(JSON.stringify({ error: 'Invalid or expired token.' }), { status: 400 });
}
};