Files
kentekengen/server/index.js

123 lines
3.7 KiB
JavaScript

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, '0.0.0.0', () => {
console.log(`Kenteken Gen server draait op poort ${PORT}`);
});