import { Router, Request, Response } from 'express'; import { v4 as uuidv4 } from 'uuid'; import crypto from 'crypto'; import multer, { FileFilterCallback } from 'multer'; import path from 'path'; import fs from 'fs'; import { fileURLToPath } from 'url'; import { userOps, questionnaireOps, activityOps, participantOps } from '../database.js'; import { requireAuth } from '../middleware/auth.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Configure multer for image uploads const uploadsDir = process.env.NODE_ENV === 'production' ? '/app/data/uploads' : path.join(__dirname, '..', '..', 'data', 'uploads'); // Ensure uploads directory exists if (!fs.existsSync(uploadsDir)) { fs.mkdirSync(uploadsDir, { recursive: true }); } const storage = multer.diskStorage({ destination: (_req: Request, _file: Express.Multer.File, cb: (error: Error | null, destination: string) => void) => { cb(null, uploadsDir); }, filename: (_req: Request, file: Express.Multer.File, cb: (error: Error | null, filename: string) => void) => { const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); const ext = path.extname(file.originalname); cb(null, 'og-' + uniqueSuffix + ext); } }); const upload = multer({ storage, limits: { fileSize: 5 * 1024 * 1024 }, // 5MB max fileFilter: (_req: Request, file: Express.Multer.File, cb: FileFilterCallback) => { const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; if (allowedTypes.includes(file.mimetype)) { cb(null, true); } else { cb(new Error('Alleen JPEG, PNG, GIF en WebP afbeeldingen zijn toegestaan')); } } }); const router = Router(); // Apply auth middleware to all admin routes router.use(requireAuth); // Upload image router.post('/upload', upload.single('image'), (req: Request, res: Response) => { const file = req.file as Express.Multer.File | undefined; if (!file) { res.status(400).json({ error: 'Geen bestand geüpload' }); return; } // Return the URL to access the uploaded file const imageUrl = `/uploads/${file.filename}`; res.json({ success: true, url: imageUrl }); }); // Get all questionnaires router.get('/questionnaires', (_req: Request, res: Response) => { const questionnaires = questionnaireOps.getAll(); res.json(questionnaires); }); // Validate slug format function isValidSlug(slug: string): boolean { return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug); } // Create questionnaire router.post('/questionnaires', (req: Request, res: Response) => { const { title, description, slug, ogImage, isPrivate } = req.body; if (!title?.trim()) { res.status(400).json({ error: 'Titel is verplicht' }); return; } if (!slug?.trim()) { res.status(400).json({ error: 'Slug is verplicht' }); return; } const cleanSlug = slug.trim().toLowerCase(); if (!isValidSlug(cleanSlug)) { res.status(400).json({ error: 'Slug mag alleen kleine letters, cijfers en koppeltekens bevatten' }); return; } if (cleanSlug.length < 3 || cleanSlug.length > 50) { res.status(400).json({ error: 'Slug moet tussen 3 en 50 tekens zijn' }); return; } if (!questionnaireOps.isSlugAvailable(cleanSlug)) { res.status(400).json({ error: 'Deze slug is al in gebruik' }); return; } const uuid = uuidv4(); const id = questionnaireOps.create(uuid, cleanSlug, title.trim(), description?.trim() || null, ogImage?.trim() || null, !!isPrivate, req.session.user!.id); const questionnaire = questionnaireOps.findById(id); res.json({ success: true, questionnaire }); }); // Get questionnaire by ID router.get('/questionnaires/:id', (req: Request, res: Response) => { const questionnaire = questionnaireOps.findById(parseInt(req.params.id)); if (!questionnaire) { res.status(404).json({ error: 'Vragenlijst niet gevonden' }); return; } const activities = activityOps.getByQuestionnaire(questionnaire.id); res.json({ questionnaire, activities }); }); // Update questionnaire router.put('/questionnaires/:id', (req: Request, res: Response) => { const { title, description, slug, ogImage, isPrivate } = req.body; const questionnaire = questionnaireOps.findById(parseInt(req.params.id)); if (!questionnaire) { res.status(404).json({ error: 'Vragenlijst niet gevonden' }); return; } if (!title?.trim()) { res.status(400).json({ error: 'Titel is verplicht' }); return; } if (!slug?.trim()) { res.status(400).json({ error: 'Slug is verplicht' }); return; } const cleanSlug = slug.trim().toLowerCase(); if (!isValidSlug(cleanSlug)) { res.status(400).json({ error: 'Slug mag alleen kleine letters, cijfers en koppeltekens bevatten' }); return; } if (cleanSlug.length < 3 || cleanSlug.length > 50) { res.status(400).json({ error: 'Slug moet tussen 3 en 50 tekens zijn' }); return; } if (!questionnaireOps.isSlugAvailable(cleanSlug, questionnaire.id)) { res.status(400).json({ error: 'Deze slug is al in gebruik' }); return; } questionnaireOps.update(questionnaire.id, cleanSlug, title.trim(), description?.trim() || null, ogImage?.trim() || null, !!isPrivate); res.json({ success: true }); }); // Check slug availability router.get('/questionnaires/check-slug/:slug', (req: Request, res: Response) => { const { slug } = req.params; const { excludeId } = req.query; const cleanSlug = slug.trim().toLowerCase(); const available = questionnaireOps.isSlugAvailable(cleanSlug, excludeId ? parseInt(excludeId as string) : undefined); res.json({ available }); }); // Get participants for a questionnaire router.get('/questionnaires/:id/participants', (req: Request, res: Response) => { const questionnaire = questionnaireOps.findById(parseInt(req.params.id)); if (!questionnaire) { res.status(404).json({ error: 'Vragenlijst niet gevonden' }); return; } const participants = participantOps.getByQuestionnaire(questionnaire.id); res.json(participants); }); // Generate unique token function generateToken(): string { return crypto.randomBytes(16).toString('hex'); } // Add participant to questionnaire router.post('/questionnaires/:id/participants', (req: Request, res: Response) => { const { name } = req.body; const questionnaire = questionnaireOps.findById(parseInt(req.params.id)); if (!questionnaire) { res.status(404).json({ error: 'Vragenlijst niet gevonden' }); return; } if (!name?.trim()) { res.status(400).json({ error: 'Naam is verplicht' }); return; } // Generate unique token let token = generateToken(); while (!participantOps.isTokenAvailable(token)) { token = generateToken(); } const id = participantOps.create(questionnaire.id, name.trim(), token); const participant = participantOps.findById(id); res.json({ success: true, participant }); }); // Delete participant router.delete('/participants/:id', (req: Request, res: Response) => { participantOps.delete(parseInt(req.params.id)); res.json({ success: true }); }); // Delete questionnaire router.delete('/questionnaires/:id', (req: Request, res: Response) => { questionnaireOps.delete(parseInt(req.params.id)); res.json({ success: true }); }); // Update activity router.put('/activities/:id', (req: Request, res: Response) => { const { name, description } = req.body; const activity = activityOps.findById(parseInt(req.params.id)); if (!activity) { res.status(404).json({ error: 'Activiteit niet gevonden' }); return; } if (!name?.trim()) { res.status(400).json({ error: 'Naam activiteit is verplicht' }); return; } activityOps.update(activity.id, name.trim(), description?.trim() || null); const updatedActivity = activityOps.findById(activity.id); res.json({ success: true, activity: updatedActivity }); }); // Delete activity router.delete('/activities/:id', (req: Request, res: Response) => { activityOps.delete(parseInt(req.params.id)); res.json({ success: true }); }); // Get all users router.get('/users', (_req: Request, res: Response) => { const users = userOps.getAll(); res.json(users); }); // Create user router.post('/users', (req: Request, res: Response) => { const { username, password } = req.body; if (!username || !password) { res.status(400).json({ error: 'Gebruikersnaam en wachtwoord zijn verplicht' }); return; } if (password.length < 6) { res.status(400).json({ error: 'Wachtwoord moet minimaal 6 tekens zijn' }); return; } const existingUser = userOps.findByUsername(username); if (existingUser) { res.status(400).json({ error: 'Gebruikersnaam bestaat al' }); return; } const id = userOps.create(username, password); const user = userOps.findById(id); res.json({ success: true, user }); }); // Delete user router.delete('/users/:id', (req: Request, res: Response) => { const userId = parseInt(req.params.id); if (userId === req.session.user!.id) { res.status(400).json({ error: 'Je kunt jezelf niet verwijderen' }); return; } if (userOps.count() <= 1) { res.status(400).json({ error: 'Je kunt de laatste beheerder niet verwijderen' }); return; } userOps.delete(userId); res.json({ success: true }); }); // Change password router.post('/change-password', (req: Request, res: Response) => { const { currentPassword, newPassword } = req.body; const user = userOps.findByUsername(req.session.user!.username); if (!user) { res.status(404).json({ error: 'Gebruiker niet gevonden' }); return; } if (!userOps.verifyPassword(user, currentPassword)) { res.status(400).json({ error: 'Huidig wachtwoord is onjuist' }); return; } if (newPassword.length < 6) { res.status(400).json({ error: 'Wachtwoord moet minimaal 6 tekens zijn' }); return; } userOps.updatePassword(req.session.user!.id, newPassword); res.json({ success: true }); }); export default router;