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

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 });
}
};