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:
@@ -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!';
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
const { token, email: submittedEmail, justification } = await request.json();
|
||||
try {
|
||||
@@ -16,11 +26,12 @@ 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 }));
|
||||
} catch (_err) {
|
||||
return new Response(JSON.stringify({ error: 'Invalid or expired token.' }), { status: 400 });
|
||||
}
|
||||
};
|
||||
};
|
||||
|
Reference in New Issue
Block a user