101 lines
3.1 KiB
JavaScript
101 lines
3.1 KiB
JavaScript
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 }),
|
|
body('url')
|
|
.trim()
|
|
.isURL({ protocols: ['http', 'https'], require_protocol: true })
|
|
.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 }
|