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:
@@ -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 });
|
||||
}
|
||||
};
|
||||
|
||||
26
src/pages/api/contact/manual-review.ts
Normal file
26
src/pages/api/contact/manual-review.ts
Normal 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 });
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user