import express from 'express' import rateLimit from 'express-rate-limit' import { body, validationResult } from 'express-validator' import { sendAuditRequestEmail } from '../services/emailService.js' const router = express.Router() const auditLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 3, // per IP message: { success: false, message: 'Too many audit requests from this IP, please try again later.', }, standardHeaders: true, legacyHeaders: false, }) // Server-side validation is the real gate. The client-side checks exist only // so people are not punished with a round trip for a typo. const validateAuditRequest = [ body('name').trim().isLength({ min: 1, max: 100 }).escape(), body('company').trim().isLength({ min: 1, max: 150 }).escape(), body('email').trim().isEmail().normalizeEmail().isLength({ max: 254 }), // No require_protocol: people paste bare domains ("example.com") too. // protocols still pins an explicit scheme, if given, to http/https. body('url') .trim() .isURL({ protocols: ['http', 'https'] }) .isLength({ max: 2048 }), body('role').isIn(['agency', 'client']), body('motivation').trim().isLength({ min: 1, max: 2000 }).escape(), body('businessConfirmation') .equals('true') .withMessage('Business (B2B) confirmation required'), body('gdprConsent').equals('true').withMessage('GDPR consent required'), body('website').optional().isEmpty(), // Honeypot ] router.post('/', auditLimiter, validateAuditRequest, async (req, res) => { try { const errors = validationResult(req) if (!errors.isEmpty()) { // Field names only - never echo the submitted values back into logs. return res.status(400).json({ success: false, message: 'Invalid form data.', fields: errors.array().map((error) => error.path), }) } const { name, company, email, url, role, motivation, businessConfirmation, gdprConsent, website, } = req.body if (website) { console.warn('Honeypot triggered, discarding submission.') // Answer 200 so the bot does not learn anything and does not retry. return res.json({ success: true, message: 'Thank you.' }) } // The offer is B2B only (ยง 14 BGB). Without the explicit confirmation the // request is refused rather than quietly accepted. if (businessConfirmation !== 'true') { return res.status(400).json({ success: false, message: 'This offer is available to businesses only.', }) } if (gdprConsent !== 'true') { return res.status(400).json({ success: false, message: 'GDPR consent required.', }) } await sendAuditRequestEmail({ name, company, email, url, role, motivation }) res.json({ success: true, message: 'Thank you, your audit request arrived.', }) } catch (error) { // Message only. The submitted data must never end up in server logs. console.error('Audit request failed:', error.message) res.status(500).json({ success: false, message: 'Failed to send the request. Please try again later.', }) } }) export { router as auditRouter }