import express from 'express'; import helmet from 'helmet'; import cors from 'cors'; import rateLimit from 'express-rate-limit'; import { initDb, getAllCars, addCar, updateCar, deleteCar, replaceAllCars, verifyPassword } from './db.js'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const app = express(); const PORT = process.env.PORT || 3456; app.use(helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"], fontSrc: ["'self'", "https://fonts.gstatic.com", "data:"], imgSrc: ["'self'", "data:", "blob:"], connectSrc: ["'self'"], }, }, })); app.use(cors()); app.use(express.json({ limit: '1mb' })); const apiLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 200, standardHeaders: true, legacyHeaders: false, }); app.use('/api/', apiLimiter); const authLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 10, standardHeaders: true, legacyHeaders: false, }); initDb(); // Serve static frontend files const distPath = join(__dirname, '..', 'dist'); app.use(express.static(distPath)); // API routes app.get('/api/cars', (_req, res) => { const cars = getAllCars(); res.json(cars); }); app.post('/api/cars', (req, res) => { const { name, width, height, rearWidth, rearHeight } = req.body; if (!name || typeof width !== 'number' || typeof height !== 'number') { return res.status(400).json({ error: 'name, width en height zijn verplicht' }); } if (width <= 0 || height <= 0) { return res.status(400).json({ error: 'width en height moeten positief zijn' }); } if (rearWidth !== undefined && (typeof rearWidth !== 'number' || rearWidth <= 0)) { return res.status(400).json({ error: 'rearWidth moet een positief getal zijn' }); } if (rearHeight !== undefined && (typeof rearHeight !== 'number' || rearHeight <= 0)) { return res.status(400).json({ error: 'rearHeight moet een positief getal zijn' }); } const car = addCar({ name: String(name).trim(), width, height, rearWidth: rearWidth || null, rearHeight: rearHeight || null }); res.status(201).json(car); }); app.put('/api/cars/:id', (req, res) => { const id = parseInt(req.params.id, 10); const { name, width, height, rearWidth, rearHeight } = req.body; if (!name || typeof width !== 'number' || typeof height !== 'number') { return res.status(400).json({ error: 'name, width en height zijn verplicht' }); } const car = updateCar(id, { name: String(name).trim(), width, height, rearWidth: rearWidth || null, rearHeight: rearHeight || null }); if (!car) { return res.status(404).json({ error: 'Auto niet gevonden' }); } res.json(car); }); app.delete('/api/cars/:id', (req, res) => { const id = parseInt(req.params.id, 10); const deleted = deleteCar(id); if (!deleted) { return res.status(404).json({ error: 'Auto niet gevonden' }); } res.json({ success: true }); }); app.put('/api/cars', (req, res) => { const cars = req.body; if (!Array.isArray(cars)) { return res.status(400).json({ error: 'Array van auto\'s verwacht' }); } replaceAllCars(cars); res.json(getAllCars()); }); app.post('/api/auth/verify', authLimiter, (req, res) => { const { password } = req.body; if (!password) { return res.status(400).json({ error: 'Wachtwoord is verplicht' }); } const valid = verifyPassword(password); res.json({ authenticated: valid }); }); // SPA fallback - serve index.html for all non-API routes app.get('*', (_req, res) => { res.sendFile(join(distPath, 'index.html')); }); app.listen(PORT, '127.0.0.1', () => { console.log(`Kenteken Gen server draait op poort ${PORT}`); });