portfolio-page/backend/routes/audit.js

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 }