124 lines
4.1 KiB
JavaScript
124 lines
4.1 KiB
JavaScript
import express from 'express'
|
|
import rateLimit from 'express-rate-limit'
|
|
import { body, validationResult } from 'express-validator'
|
|
import { sendAuditRequestEmail, sendQuickcheckRequestEmail } 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('formType').optional().equals('audit'),
|
|
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
|
|
]
|
|
|
|
const validateQuickcheckRequest = [
|
|
body('formType').equals('quickcheck'),
|
|
body('name').optional().trim().isLength({ max: 100 }).escape(),
|
|
body('email').trim().isEmail().normalizeEmail().isLength({ max: 254 }),
|
|
body('businessConfirmation')
|
|
.equals('true')
|
|
.withMessage('Business (B2B) confirmation required'),
|
|
body('gdprConsent').equals('true').withMessage('GDPR consent required'),
|
|
body('website').optional().isEmpty(), // Honeypot
|
|
]
|
|
|
|
const validateRequest = (req, _res, next) => {
|
|
const validators = req.body.formType === 'quickcheck'
|
|
? validateQuickcheckRequest
|
|
: validateAuditRequest
|
|
|
|
Promise.all(validators.map((validator) => validator.run(req))).then(() => next())
|
|
}
|
|
|
|
router.post('/', auditLimiter, validateRequest, 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 { formType = 'audit', name, email, 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.',
|
|
})
|
|
}
|
|
|
|
if (formType === 'quickcheck') {
|
|
await sendQuickcheckRequestEmail({ name: name || '', email })
|
|
|
|
return res.json({
|
|
success: true,
|
|
message: 'Thank you, your quick check request arrived.',
|
|
})
|
|
}
|
|
|
|
const { company, url, role, motivation } = req.body
|
|
|
|
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 }
|