Add spam detection and manual review request feature in contact form
- Integrated Gemini AI for spam detection in the contact form API, returning a token for manual review requests if spam is detected. - Implemented a manual review UI in the Form.astro component, allowing users to submit their email and justification for review. - Updated email handler to send manual review requests to a designated email address. - Enhanced rate limiter configuration to allow more attempts in a shorter duration for better user experience. - Added new dependencies: jsonwebtoken and @types/jsonwebtoken for handling JWTs in the spam detection process.
This commit is contained in:
84
src/components/ContactForm.astro
Normal file
84
src/components/ContactForm.astro
Normal file
@@ -0,0 +1,84 @@
|
||||
<form id="contact-form">
|
||||
<input type="email" name="email" id="email" required />
|
||||
<textarea name="message" id="message" required></textarea>
|
||||
<button type="submit">Send</button>
|
||||
</form>
|
||||
|
||||
<div id="spam-warning" style="display:none;">
|
||||
<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.
|
||||
</p>
|
||||
<form id="manual-review-form">
|
||||
<input type="email" id="manual-email" readonly />
|
||||
<textarea id="manual-justification" placeholder="Why is this not spam? (optional)"></textarea>
|
||||
<input type="hidden" id="manual-token" />
|
||||
<button type="submit">Request Manual Review</button>
|
||||
</form>
|
||||
<div id="manual-review-result"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const contactForm = document.getElementById('contact-form');
|
||||
if (contactForm) {
|
||||
contactForm.onsubmit = async function (e) {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(this as HTMLFormElement);
|
||||
let res, data;
|
||||
try {
|
||||
res = await fetch('/api/contact', { method: 'POST', body: formData });
|
||||
data = await res.json();
|
||||
console.log('Contact API response:', data);
|
||||
} catch (_err) {
|
||||
alert('Unexpected server response.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.spam && data.token) {
|
||||
console.log('Spam detected, showing manual review UI.');
|
||||
const spamWarning = document.getElementById('spam-warning');
|
||||
const manualEmail = document.getElementById('manual-email') as HTMLInputElement | null;
|
||||
const manualToken = document.getElementById('manual-token') as HTMLInputElement | null;
|
||||
if (spamWarning && manualEmail && manualToken) {
|
||||
spamWarning.style.display = 'block';
|
||||
manualEmail.value = String(formData.get('email'));
|
||||
manualToken.value = data.token;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
alert(data.error || 'There was an error sending your message.');
|
||||
} else {
|
||||
alert('Your message was sent successfully!');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const manualReviewForm = document.getElementById('manual-review-form');
|
||||
if (manualReviewForm) {
|
||||
manualReviewForm.onsubmit = async function (e) {
|
||||
e.preventDefault();
|
||||
const manualEmail = document.getElementById('manual-email') as HTMLInputElement | null;
|
||||
const manualJustification = document.getElementById('manual-justification') as HTMLTextAreaElement | null;
|
||||
const manualToken = document.getElementById('manual-token') as HTMLInputElement | null;
|
||||
const resultDiv = document.getElementById('manual-review-result');
|
||||
if (!manualEmail || !manualJustification || !manualToken || !resultDiv) return;
|
||||
const email = manualEmail.value;
|
||||
const justification = manualJustification.value;
|
||||
const token = manualToken.value;
|
||||
|
||||
const res = await fetch('/api/contact/manual-review', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, justification, token }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
resultDiv.textContent = 'Your request for manual review has been submitted. Thank you!';
|
||||
} else {
|
||||
resultDiv.textContent = data.error || 'There was an error submitting your manual review request.';
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
@@ -128,6 +128,23 @@ const { inputs, textarea, disclaimer, button = 'Contact us', description = '' }
|
||||
}
|
||||
</form>
|
||||
|
||||
<!-- Manual Review UI -->
|
||||
<div id="spam-warning" style="display:none;" class="max-w-xl mx-auto rounded-lg backdrop-blur-sm bg-white/15 dark:bg-slate-900 border border-gray-200 dark:border-gray-700 shadow-md p-4 sm:p-6 lg:p-8 w-full mb-6">
|
||||
<p class="mb-4 text-lg font-medium text-gray-800 dark:text-gray-100">
|
||||
Your message was detected as spam and was not sent.<br>
|
||||
<span class="text-base font-normal text-gray-600 dark:text-gray-300">If you believe this is a mistake, you can request a manual review.</span>
|
||||
</p>
|
||||
<form id="manual-review-form" class="space-y-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="manual-email">Please re-enter your email address for confirmation</label>
|
||||
<input type="email" id="manual-email" required placeholder="Enter your email address again" class="py-3 px-4 block w-full text-md rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-slate-900" />
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="manual-justification">Why is this not spam? <span class="text-gray-400">(optional)</span></label>
|
||||
<textarea id="manual-justification" placeholder="Explain why your message is legitimate..." class="py-3 px-4 block w-full text-md rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-slate-900"></textarea>
|
||||
<input type="hidden" id="manual-token" />
|
||||
<button type="submit" class="mt-2 w-full py-3 px-4 rounded-lg bg-blue-600 hover:bg-blue-700 text-white font-semibold transition-colors">Request Manual Review</button>
|
||||
</form>
|
||||
<div id="manual-review-result" class="mt-4 text-center text-green-700 dark:text-green-400 font-medium"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function setCsrfToken() {
|
||||
try {
|
||||
@@ -146,55 +163,97 @@ const { inputs, textarea, disclaimer, button = 'Contact us', description = '' }
|
||||
|
||||
document.addEventListener('DOMContentLoaded', setCsrfToken);
|
||||
|
||||
const form = document.getElementById('contact-form');
|
||||
const form = document.getElementById('contact-form') as HTMLFormElement | null;
|
||||
|
||||
if (form) {
|
||||
form.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const formData = new FormData(form);
|
||||
let response, result;
|
||||
try {
|
||||
const response = await fetch('/api/contact', {
|
||||
response = await fetch('/api/contact', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
console.log(result.message); // Log success message
|
||||
const successElement = document.getElementById('form-success');
|
||||
if (successElement) {
|
||||
successElement.classList.remove('hidden');
|
||||
}
|
||||
const errorElement = document.getElementById('form-error');
|
||||
if (errorElement) {
|
||||
errorElement.classList.add('hidden');
|
||||
}
|
||||
form.reset(); // Clear the form
|
||||
// Re-fetch CSRF token after successful submission
|
||||
setCsrfToken();
|
||||
} else {
|
||||
console.error('Error:', response.status);
|
||||
const errorElement = document.getElementById('form-error');
|
||||
if (errorElement) {
|
||||
errorElement.classList.remove('hidden');
|
||||
}
|
||||
const successElement = document.getElementById('form-success');
|
||||
if (successElement) {
|
||||
successElement.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
result = await response.json();
|
||||
console.log('Contact API response:', result);
|
||||
} catch (_err) {
|
||||
const errorElement = document.getElementById('form-error');
|
||||
if (errorElement) {
|
||||
errorElement.classList.remove('hidden');
|
||||
}
|
||||
if (errorElement) errorElement.classList.remove('hidden');
|
||||
const successElement = document.getElementById('form-success');
|
||||
if (successElement) {
|
||||
successElement.classList.add('hidden');
|
||||
}
|
||||
if (successElement) successElement.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show manual review UI if spam detected, even if status is 422
|
||||
if (result.spam && result.token) {
|
||||
console.log('Spam detected, showing manual review UI.');
|
||||
form.style.display = 'none';
|
||||
const spamWarning = document.getElementById('spam-warning');
|
||||
const manualEmail = document.getElementById('manual-email') as HTMLInputElement | null;
|
||||
const manualToken = document.getElementById('manual-token') as HTMLInputElement | null;
|
||||
if (spamWarning && manualEmail && manualToken) {
|
||||
spamWarning.style.display = 'block';
|
||||
manualEmail.value = String(formData.get('email'));
|
||||
manualToken.value = result.token;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorElement = document.getElementById('form-error');
|
||||
if (errorElement) errorElement.classList.remove('hidden');
|
||||
const successElement = document.getElementById('form-success');
|
||||
if (successElement) successElement.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
// Success
|
||||
const successElement = document.getElementById('form-success');
|
||||
if (successElement) successElement.classList.remove('hidden');
|
||||
const errorElement = document.getElementById('form-error');
|
||||
if (errorElement) errorElement.classList.add('hidden');
|
||||
form.reset();
|
||||
setCsrfToken();
|
||||
});
|
||||
}
|
||||
|
||||
// Manual Review Form Logic
|
||||
const manualReviewForm = document.getElementById('manual-review-form');
|
||||
if (manualReviewForm) {
|
||||
manualReviewForm.onsubmit = async function (e) {
|
||||
e.preventDefault();
|
||||
const manualEmail = document.getElementById('manual-email') as HTMLInputElement | null;
|
||||
const manualJustification = document.getElementById('manual-justification') as HTMLTextAreaElement | null;
|
||||
const manualToken = document.getElementById('manual-token') as HTMLInputElement | null;
|
||||
const resultDiv = document.getElementById('manual-review-result');
|
||||
const form = document.getElementById('contact-form') as HTMLFormElement | null;
|
||||
const spamWarning = document.getElementById('spam-warning');
|
||||
if (!manualEmail || !manualJustification || !manualToken || !resultDiv) return;
|
||||
const email = manualEmail.value;
|
||||
const justification = manualJustification.value;
|
||||
const token = manualToken.value;
|
||||
|
||||
const res = await fetch('/api/contact/manual-review', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, justification, token }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
resultDiv.textContent = 'Your request for manual review has been submitted. Thank you!';
|
||||
// Hide spam-warning and show the normal form again after a short delay
|
||||
setTimeout(() => {
|
||||
if (spamWarning) spamWarning.style.display = 'none';
|
||||
if (form) {
|
||||
form.style.display = '';
|
||||
form.reset();
|
||||
}
|
||||
resultDiv.textContent = '';
|
||||
}, 2000);
|
||||
} else {
|
||||
resultDiv.textContent = data.error || 'There was an error submitting your manual review request.';
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
Reference in New Issue
Block a user