skrift-orders: oeffentlicher Upload-Proxy /upload (FilesService, Base64) fuer Motiv-/Quelldatei-Upload durch Gaeste

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
s4luorth
2026-09-03 10:14:28 +02:00
parent 46b5155c27
commit ca0b3a845a

View File

@@ -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 {