import { FastifyPluginAsync } from 'fastify'; import { z } from 'zod'; import { db } from '../config/database.js'; import { contentSections } from '../db/schema.js'; import { eq } from 'drizzle-orm'; import { saveUploadedImage } from '../services/upload.service.js'; import { dropImageIfUnused, extractImageUrls } from '../services/image-refs.service.js'; import { env } from '../config/env.js'; // Fastify JSON schema for content section body const contentBodyJsonSchema = { type: 'object', required: ['contentJson'], properties: { contentJson: {}, // allow any JSON }, } as const; const contentRoute: FastifyPluginAsync = async (fastify) => { // Get content section fastify.get('/content/:section', { preHandler: [fastify.authenticate], }, async (request, reply) => { const { section } = request.params as { section: string }; const [content] = await db .select() .from(contentSections) .where(eq(contentSections.sectionName, section)) .limit(1); if (!content) { return reply.code(404).send({ error: 'Content section not found' }); } return { section: content.sectionName, content: content.contentJson, updatedAt: content.updatedAt, }; }); // Update content section fastify.put('/content/:section', { schema: { body: contentBodyJsonSchema, }, preHandler: [fastify.authenticate], }, async (request, reply) => { const { section } = request.params as { section: string }; const { contentJson } = request.body as any; // Check if section exists const [existing] = await db .select() .from(contentSections) .where(eq(contentSections.sectionName, section)) .limit(1); let result; if (existing) { // Update existing [result] = await db .update(contentSections) .set({ contentJson, updatedAt: new Date(), }) .where(eq(contentSections.sectionName, section)) .returning(); } else { // Create new [result] = await db .insert(contentSections) .values({ sectionName: section, contentJson, }) .returning(); } // Bilder, die durch die Aenderung herausgefallen sind, wegraeumen const before = extractImageUrls((existing as any)?.contentJson); const after = extractImageUrls(result.contentJson); for (const url of before) { if (after.has(url)) continue; try { if (await dropImageIfUnused(url)) { fastify.log.info(`Removed unused content image ${url}`); } } catch (err) { fastify.log.warn({ err }, 'Could not remove unused content image'); } } return { section: result.sectionName, content: result.contentJson, updatedAt: result.updatedAt, }; }); // Bild fuer einen Textbereich hochladen (Welcome-Bild, Monatshit, Whiskey) fastify.post('/content/upload', { preHandler: [fastify.authenticate], }, async (request, reply) => { try { 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' }); } const preferredName = (file.fields?.name?.value as string | undefined) || ''; const saved = await saveUploadedImage(file, 'content', { 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' }); } }); // List all content sections fastify.get('/content', { preHandler: [fastify.authenticate], }, async (request, reply) => { const sections = await db.select().from(contentSections); return { sections: (sections as any[]).map((s: any) => ({ section: s.sectionName, content: s.contentJson, updatedAt: s.updatedAt, })), }; }); }; export default contentRoute;