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:
becarta
2025-06-12 23:43:12 +02:00
parent 1d80c4156c
commit 5ceb3491c7
8 changed files with 406 additions and 40 deletions

138
package-lock.json generated
View File

@@ -15,6 +15,7 @@
"@astrolib/analytics": "^0.6.1",
"@astrolib/seo": "^1.0.0-beta.8",
"@fontsource-variable/inter": "^5.1.1",
"@google/generative-ai": "^0.24.1",
"@tippyjs/react": "^4.2.6",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
@@ -24,6 +25,7 @@
"csrf": "^3.1.0",
"dotenv": "^16.4.7",
"form-data": "^4.0.2",
"jsonwebtoken": "^9.0.2",
"limax": "4.1.0",
"lodash.merge": "^4.6.2",
"luxon": "^3.6.1",
@@ -47,6 +49,7 @@
"@tailwindcss/typography": "^0.5.16",
"@types/eslint__js": "^8.42.3",
"@types/js-yaml": "^4.0.9",
"@types/jsonwebtoken": "^9.0.9",
"@types/lodash.merge": "^4.6.9",
"@types/mdx": "^2.0.13",
"@typescript-eslint/eslint-plugin": "^8.21.0",
@@ -1536,6 +1539,15 @@
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@google/generative-ai": {
"version": "0.24.1",
"resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.24.1.tgz",
"integrity": "sha512-MqO+MLfM6kjxcKoy0p1wRzG3b4ZZXtPI+z2IE26UogS2Cm/XHO+7gGRBh6gcJsOiIVoH93UwKvW4HdgiOZCy9Q==",
"license": "Apache-2.0",
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@@ -2907,6 +2919,17 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/jsonwebtoken": {
"version": "9.0.9",
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz",
"integrity": "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/ms": "*",
"@types/node": "*"
}
},
"node_modules/@types/lodash": {
"version": "4.17.17",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.17.tgz",
@@ -4034,6 +4057,12 @@
"node": "*"
}
},
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
"license": "BSD-3-Clause"
},
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@@ -4981,6 +5010,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -7026,6 +7064,49 @@
"dev": true,
"license": "MIT"
},
"node_modules/jsonwebtoken": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
"integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
"license": "MIT",
"dependencies": {
"jws": "^3.2.2",
"lodash.includes": "^4.3.0",
"lodash.isboolean": "^3.0.3",
"lodash.isinteger": "^4.0.4",
"lodash.isnumber": "^3.0.3",
"lodash.isplainobject": "^4.0.6",
"lodash.isstring": "^4.0.1",
"lodash.once": "^4.0.0",
"ms": "^2.1.1",
"semver": "^7.5.4"
},
"engines": {
"node": ">=12",
"npm": ">=6"
}
},
"node_modules/jwa": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz",
"integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==",
"license": "MIT",
"dependencies": {
"buffer-equal-constant-time": "^1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jws": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
"license": "MIT",
"dependencies": {
"jwa": "^1.4.1",
"safe-buffer": "^5.0.1"
}
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -7441,11 +7522,40 @@
"integrity": "sha512-m/M1U1f3ddMCs6Hq2tAsYThTBDaAKFDX3dwDo97GEYzamXi9SqUpjWi/Rrj/gf3X2n8ktwgZrlP1z6E3v/IExQ==",
"license": "MIT"
},
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
"license": "MIT"
},
"node_modules/lodash.isboolean": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
"license": "MIT"
},
"node_modules/lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
"license": "MIT"
},
"node_modules/lodash.isnumber": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
"license": "MIT"
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
"dev": true,
"license": "MIT"
},
"node_modules/lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
"license": "MIT"
},
"node_modules/lodash.merge": {
@@ -7454,6 +7564,12 @@
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"license": "MIT"
},
"node_modules/lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
"license": "MIT"
},
"node_modules/longest-streak": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
@@ -10288,6 +10404,26 @@
"dev": true,
"license": "MIT"
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",

View File

@@ -29,6 +29,7 @@
"@astrolib/analytics": "^0.6.1",
"@astrolib/seo": "^1.0.0-beta.8",
"@fontsource-variable/inter": "^5.1.1",
"@google/generative-ai": "^0.24.1",
"@tippyjs/react": "^4.2.6",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
@@ -38,6 +39,7 @@
"csrf": "^3.1.0",
"dotenv": "^16.4.7",
"form-data": "^4.0.2",
"jsonwebtoken": "^9.0.2",
"limax": "4.1.0",
"lodash.merge": "^4.6.2",
"luxon": "^3.6.1",
@@ -61,6 +63,7 @@
"@tailwindcss/typography": "^0.5.16",
"@types/eslint__js": "^8.42.3",
"@types/js-yaml": "^4.0.9",
"@types/jsonwebtoken": "^9.0.9",
"@types/lodash.merge": "^4.6.9",
"@types/mdx": "^2.0.13",
"@typescript-eslint/eslint-plugin": "^8.21.0",

View 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>

View File

@@ -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>

View File

@@ -5,7 +5,13 @@ import {
checkRateLimit,
sendAdminNotification,
sendUserConfirmation,
sendEmail,
} from '../../utils/email-handler';
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_EMAIL = 'manual-review@365devnet.eu';
// Enhanced email validation with more comprehensive regex
const isValidEmail = (email: string): boolean => {
@@ -210,6 +216,23 @@ export const POST: APIRoute = async ({ request, clientAddress }) => {
errors.spam = 'Your message was flagged as potential spam. Please revise your message and try again.';
}
// Gemini AI spam detection
if (await isSpamWithGemini(message)) {
const token = jwt.sign({ email, message }, MANUAL_REVIEW_SECRET, { expiresIn: '1h' });
console.warn(
`[SPAM DETECTED by Gemini]`,
{ name, email, message, ip: request.headers.get('x-forwarded-for') }
);
return new Response(
JSON.stringify({
error: "Your message was detected as spam and was not sent. If this is a mistake, you can request a manual review.",
spam: true,
token
}),
{ status: 422 }
);
}
// If there are validation errors, return them
if (Object.keys(errors).length > 0) {
return new Response(
@@ -283,3 +306,23 @@ 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 });
}
};

View File

@@ -0,0 +1,26 @@
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_EMAIL = 'manual-review@365devnet.eu';
export const POST: 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 });
}
};

View File

@@ -54,8 +54,8 @@ function initializeTransporter() {
// Rate limiter configuration
const rateLimiter = new RateLimiterMemory({
points: 5, // 5 attempts
duration: 3600, // per hour
points: 20, // 20 attempts
duration: 300, // per 5 minutes
});
// CSRF protection

View File

@@ -0,0 +1,15 @@
import { GoogleGenerativeAI } from "@google/generative-ai";
const GEMINI_API_KEY = process.env.GEMINI_API_KEY;
if (!GEMINI_API_KEY) {
throw new Error("GEMINI_API_KEY environment variable is not set.");
}
const genAI = new GoogleGenerativeAI(GEMINI_API_KEY);
export async function isSpamWithGemini(message: string): Promise<boolean> {
const model = genAI.getGenerativeModel({ model: "gemini-1.5-flash" });
const prompt = `Is the following message spam? Reply with only 'yes' or 'no'.\n\nMessage:\n${message}`;
const result = await model.generateContent(prompt);
const response = result.response.text().trim().toLowerCase();
return response.startsWith("yes");
}