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">
|
<form id="contact-form">
|
||||||
|
<input type="hidden" name="csrf_token" id="csrf_token" />
|
||||||
<label for="email">Email</label>
|
<label for="email">Email</label>
|
||||||
<input type="email" name="email" id="email" required aria-describedby="email-help" />
|
<input type="email" name="email" id="email" required aria-describedby="email-help" />
|
||||||
<div id="email-help" class="sr-only">Enter your email address</div>
|
<div id="email-help" class="sr-only">Enter your email address</div>
|
||||||
@@ -8,7 +9,9 @@
|
|||||||
<button type="submit">Send</button>
|
<button type="submit">Send</button>
|
||||||
</form>
|
</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>
|
<p>
|
||||||
Your message was detected as spam and was not sent.<br>
|
Your message was detected as spam and was not sent.<br>
|
||||||
If you believe this is a mistake, you can request a manual review.
|
If you believe this is a mistake, you can request a manual review.
|
||||||
@@ -16,7 +19,7 @@
|
|||||||
<form id="manual-review-form">
|
<form id="manual-review-form">
|
||||||
<label for="manual-email">Email</label>
|
<label for="manual-email">Email</label>
|
||||||
<input type="email" id="manual-email" readonly aria-describedby="manual-email-help" />
|
<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>
|
<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>
|
<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>
|
<div id="manual-justification-help" class="sr-only">Explain why your message is legitimate</div>
|
||||||
@@ -27,8 +30,25 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<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');
|
const contactForm = document.getElementById('contact-form');
|
||||||
if (contactForm) {
|
const feedbackDiv = document.getElementById('form-feedback');
|
||||||
|
|
||||||
|
if (contactForm && feedbackDiv) {
|
||||||
contactForm.onsubmit = async function (e) {
|
contactForm.onsubmit = async function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const formData = new FormData(this as HTMLFormElement);
|
const formData = new FormData(this as HTMLFormElement);
|
||||||
@@ -38,7 +58,7 @@ if (contactForm) {
|
|||||||
data = await res.json();
|
data = await res.json();
|
||||||
console.log('Contact API response:', data);
|
console.log('Contact API response:', data);
|
||||||
} catch (_err) {
|
} catch (_err) {
|
||||||
alert('Unexpected server response.');
|
feedbackDiv.textContent = 'Unexpected server response.';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,14 +71,15 @@ if (contactForm) {
|
|||||||
spamWarning.style.display = 'block';
|
spamWarning.style.display = 'block';
|
||||||
manualEmail.value = String(formData.get('email'));
|
manualEmail.value = String(formData.get('email'));
|
||||||
manualToken.value = data.token;
|
manualToken.value = data.token;
|
||||||
|
spamWarning.focus();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!res.ok) {
|
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 {
|
} 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"
|
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"
|
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;"
|
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="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">
|
<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>
|
<p>
|
||||||
{t.cookies.message}
|
{t.cookies.message}
|
||||||
<a href={`/${lang}/privacy#cookie-usage`} class="text-blue-600 dark:text-blue-400 hover:underline"
|
<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
|
// Show the banner with a slight delay for better UX
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
cookieBanner.classList.remove('translate-y-full');
|
cookieBanner.classList.remove('translate-y-full');
|
||||||
|
cookieBanner.focus();
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
// Handle accept button click
|
// Handle accept button click
|
||||||
|
@@ -10,7 +10,7 @@ import {
|
|||||||
import { isSpamWithGemini } from "../../utils/gemini-spam-check";
|
import { isSpamWithGemini } from "../../utils/gemini-spam-check";
|
||||||
import jwt from 'jsonwebtoken';
|
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';
|
const MANUAL_REVIEW_EMAIL = 'manual-review@365devnet.eu';
|
||||||
|
|
||||||
// Enhanced email validation with more comprehensive regex
|
// Enhanced email validation with more comprehensive regex
|
||||||
|
@@ -2,9 +2,19 @@ import type { APIRoute } from 'astro';
|
|||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import { sendEmail } from '../../../utils/email-handler';
|
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';
|
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 }) => {
|
export const POST: APIRoute = async ({ request }) => {
|
||||||
const { token, email: submittedEmail, justification } = await request.json();
|
const { token, email: submittedEmail, justification } = await request.json();
|
||||||
try {
|
try {
|
||||||
@@ -16,11 +26,12 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
await sendEmail(
|
await sendEmail(
|
||||||
MANUAL_REVIEW_EMAIL,
|
MANUAL_REVIEW_EMAIL,
|
||||||
'Manual Review Requested: Contact Form Submission',
|
'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'}`
|
`Email: ${submittedEmail}\nMessage: ${payload.message}\nJustification: ${justification || 'None provided'}`
|
||||||
);
|
);
|
||||||
return new Response(JSON.stringify({ success: true }));
|
return new Response(JSON.stringify({ success: true }));
|
||||||
} catch (_err) {
|
} catch (_err) {
|
||||||
return new Response(JSON.stringify({ error: 'Invalid or expired token.' }), { status: 400 });
|
return new Response(JSON.stringify({ error: 'Invalid or expired token.' }), { status: 400 });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
Reference in New Issue
Block a user