From ca0b3a845a5f2cbebaf79255d64684eb22f23d5c Mon Sep 17 00:00:00 2001 From: s4luorth Date: Thu, 3 Sep 2026 10:14:28 +0200 Subject: [PATCH] skrift-orders: oeffentlicher Upload-Proxy /upload (FilesService, Base64) fuer Motiv-/Quelldatei-Upload durch Gaeste Co-Authored-By: Claude Opus 4.8 --- .../dist/index.js | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/extensions/directus-extension-skrift-orders/dist/index.js b/extensions/directus-extension-skrift-orders/dist/index.js index 1ba1018..6e5cb53 100644 --- a/extensions/directus-extension-skrift-orders/dist/index.js +++ b/extensions/directus-extension-skrift-orders/dist/index.js @@ -18,6 +18,7 @@ import crypto from 'node:crypto'; import fs from 'node:fs'; import path from 'node:path'; +import { Readable } from 'node:stream'; /** Basisordner der generierten Auftragsdateien (read-only in Directus gemountet). */ const OUTPUT_DIR = process.env.SKRIFT_OUTPUT_DIR || '/var/skrift-output'; @@ -155,7 +156,7 @@ async function paypalToken() { } const handler = (router, { services, getSchema, logger }) => { - const { ItemsService, MailService, UsersService, RolesService, AssetsService } = services; + const { ItemsService, MailService, UsersService, RolesService, AssetsService, FilesService } = services; // Signatur-Plotter-Service (intern über das npm-Netz per container_name erreichbar). const SIGN_URL = (process.env.SKRIFT_SIGN_URL || 'http://skrift-signature-service:8000').replace(/\/$/, ''); @@ -774,6 +775,41 @@ const handler = (router, { services, getSchema, logger }) => { } }); + /** + * Öffentlicher Datei-Upload (Motiv/Quelldatei). Gäste bestellen ohne Login und + * haben keinen Directus-Token für /files – daher hier serverseitig via FilesService + * (admin) ablegen. Datei kommt als Base64 im JSON. Nur Bilder + PDF, max. 20 MB. + */ + router.post('/upload', async (req, res) => { + const body = req.body || {}; + const roh = typeof body.data_base64 === 'string' ? body.data_base64.replace(/^data:[^;]+;base64,/, '') : ''; + if (!roh) return res.status(400).json({ error: 'Keine Datei übergeben.' }); + const type = String(body.content_type || 'application/octet-stream'); + if (!/^image\/(png|jpe?g|gif|webp|svg\+xml)$|^application\/pdf$/i.test(type)) { + return res.status(415).json({ error: 'Dateityp nicht erlaubt (nur Bilder oder PDF).' }); + } + let buffer; + try { buffer = Buffer.from(roh, 'base64'); } catch { return res.status(400).json({ error: 'Ungültige Datei.' }); } + if (!buffer.length) return res.status(400).json({ error: 'Leere Datei.' }); + if (buffer.length > 20 * 1024 * 1024) return res.status(413).json({ error: 'Datei zu groß (max. 20 MB).' }); + try { + const schema = await getSchema(); + const files = new FilesService({ schema, accountability: null }); + const name = (String(body.filename || 'upload').replace(/[^\w.\- ]+/g, '_').slice(0, 120) || 'upload'); + const storage = (process.env.STORAGE_LOCATIONS || 'local').split(',')[0].trim() || 'local'; + const id = await files.uploadOne( + Readable.from(buffer), + { storage, filename_download: name, title: name, type }, + undefined, + { emitEvents: false }, + ); + return res.json({ id }); + } catch (err) { + logger.error(`[skrift-orders] upload: ${err.stack || err.message}`); + return res.status(500).json({ error: 'Upload fehlgeschlagen.' }); + } + }); + /** Aktuelles Kontingent abfragen, ohne etwas zu verbrauchen. */ router.get('/preview/quota', async (req, res) => { try {