import { FastifyPluginAsync } from 'fastify'; import { db } from '../config/database.js'; import { events } from '../db/schema.js'; import { eq } from 'drizzle-orm'; import { dropImageIfUnused } from '../services/image-refs.service.js'; import { saveUploadedImage } from '../services/upload.service.js'; import { env } from '../config/env.js'; /** Raeumt eine Bilddatei weg, ohne dass ein Fehler die Antwort kippt. */ async function dropUnusedImage(fastify: any, url: string | null | undefined, reason: string) { try { if (await dropImageIfUnused(url)) { fastify.log.info(`Removed ${reason} ${url}`); } } catch (err) { fastify.log.warn({ err }, `Could not remove ${reason}`); } } // Fastify JSON schema for event body const eventBodyJsonSchema = { type: 'object', required: ['title', 'date', 'description', 'imageUrl', 'displayOrder'], properties: { title: { type: 'string', minLength: 1, maxLength: 200 }, date: { type: 'string', minLength: 1, maxLength: 100 }, description: { type: 'string', minLength: 1 }, imageUrl: { type: 'string', minLength: 1 }, displayOrder: { type: 'integer', minimum: 0 }, isPublished: { type: 'boolean' }, }, } as const; const reorderBodyJsonSchema = { type: 'object', required: ['orders'], properties: { orders: { type: 'array', items: { type: 'object', required: ['id', 'displayOrder'], properties: { id: { type: 'string' }, displayOrder: { type: 'integer', minimum: 0 }, }, }, }, }, } as const; const eventsRoute: FastifyPluginAsync = async (fastify) => { // PUBLIC: List published events (no auth required) fastify.get('/events/public', async () => { const all = await db.select().from(events) .where(eq(events.isPublished, true)) .orderBy(events.displayOrder); return { events: all }; }); // List all events (by displayOrder) - admin only fastify.get('/events', { preHandler: [fastify.authenticate] }, async () => { const all = await db.select().from(events).orderBy(events.displayOrder); return { events: all }; }); // Get single event fastify.get('/events/:id', { preHandler: [fastify.authenticate] }, async (request, reply) => { const { id } = request.params as { id: string }; const rows = await db.select().from(events).where(eq(events.id, id)).limit(1); if (rows.length === 0) return reply.code(404).send({ error: 'Event not found' }); return { event: rows[0] }; }); // Create event fastify.post('/events', { schema: { body: eventBodyJsonSchema }, preHandler: [fastify.authenticate] }, async (request, reply) => { const data = request.body as any; const [row] = await db.insert(events).values(data).returning(); return reply.code(201).send({ event: row }); }); // Update event fastify.put('/events/:id', { schema: { body: eventBodyJsonSchema }, preHandler: [fastify.authenticate] }, async (request, reply) => { const { id } = request.params as { id: string }; const data = request.body as any; const [previous] = await db.select().from(events).where(eq(events.id, id)).limit(1); const [row] = await db.update(events).set({ ...data, updatedAt: new Date() }).where(eq(events.id, id)).returning(); if (!row) return reply.code(404).send({ error: 'Event not found' }); // Ausgetauschtes Bild wegraeumen, sonst bleibt es fuer immer liegen if (previous && previous.imageUrl !== row.imageUrl) { await dropUnusedImage(fastify, previous.imageUrl, 'replaced event image'); } return { event: row }; }); // Upload event image file (multipart) fastify.post('/events/upload', { preHandler: [fastify.authenticate], }, async (request, reply) => { try { // Expect a single file field named "file" const file = await (request as any).file({ limits: { fileSize: env.MAX_FILE_SIZE } }); if (!file) { return reply.code(400).send({ error: 'No file uploaded' }); } const mime = file.mimetype as string | undefined; if (!mime || !mime.startsWith('image/')) { return reply.code(400).send({ error: 'Only image uploads are allowed' }); } // Der Titel kommt als Formularfeld VOR der Datei, sonst ist er hier // noch nicht geparst const preferredName = (file.fields?.title?.value as string | undefined) || ''; const saved = await saveUploadedImage(file, 'events', { preferredName, log: fastify.log, }); return reply.code(201).send({ imageUrl: saved.imageUrl }); } catch (err: any) { if (err?.statusCode === 413) { return reply.code(413).send({ error: err.message }); } fastify.log.error({ err }, 'Upload failed'); return reply.code(500).send({ error: 'Failed to upload image' }); } }); // Delete event fastify.delete('/events/:id', { preHandler: [fastify.authenticate] }, async (request, reply) => { const { id } = request.params as { id: string }; const [row] = await db.delete(events).where(eq(events.id, id)).returning(); if (!row) return reply.code(404).send({ error: 'Event not found' }); // Zugehoerige Bilddatei mitnehmen await dropUnusedImage(fastify, row.imageUrl, 'event image'); return { message: 'Event deleted successfully' }; }); // Reorder events (synchronous transaction for better-sqlite3) fastify.put('/events/reorder', { schema: { body: reorderBodyJsonSchema }, preHandler: [fastify.authenticate] }, async (request) => { const { orders } = request.body as { orders: Array<{ id: string; displayOrder: number }> }; db.transaction((tx: any) => { for (const { id, displayOrder } of orders) { tx.update(events).set({ displayOrder }).where(eq(events.id, id)).run?.(); } }); return { message: 'Events reordered successfully' }; }); }; export default eventsRoute;