From 48fb4118fdfc9476cb0390a3eb5287732967ef5e Mon Sep 17 00:00:00 2001 From: bernd Date: Thu, 13 Aug 2026 11:52:12 +0000 Subject: [PATCH] Compress ID/Schufa photos client-side before upload, raise server limit further MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Server-side testing showed the 30mb express.json limit itself was fine for payloads up to 30MB, but real phone photos (15-25MB each, ×3 files) can still exceed that. Adds readFileAsCompressedDataUrl: downscales image uploads to max 1800px on the long edge at 0.82 JPEG quality via canvas before converting to a data URL — typically shrinks a multi-MB phone photo to a few hundred KB, which is more than sufficient resolution for a legible ID/Schufa scan. PDFs pass through uncompressed. Also raises the raw pre-upload size check to 25MB/file and the server body limit to 50mb as a safety margin for PDF-heavy cases the client-side compression doesn't touch. --- backend/src/app.ts | 2 +- web-dashboard/index.html | 40 ++++++++++++++++++++++++++++++++++------ 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/backend/src/app.ts b/backend/src/app.ts index 3eb972a..d3b720e 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -39,7 +39,7 @@ export function createApp() { // versendet werden (keine separate Objekt-Storage-Anbindung in diesem // Demo-Stand). 10mb reichte für einzelne Fotos, war aber zu knapp für drei // hochauflösende Handyfotos in einem Request (HTTP 413). - app.use(express.json({ limit: '30mb' })); + app.use(express.json({ limit: '50mb' })); app.use('/v1', paymentsRouter); app.use('/v1', cockpitRouter); app.use('/v1', authRouter); diff --git a/web-dashboard/index.html b/web-dashboard/index.html index 0935878..6ea4725 100644 --- a/web-dashboard/index.html +++ b/web-dashboard/index.html @@ -1209,18 +1209,18 @@ const password = document.getElementById('acceptPassword').value; const body = { fullName, password }; if (isTenant) { - const MAX_UPLOAD_BYTES = 8 * 1024 * 1024; // 8MB je Datei, reichlich Luft unter dem 30mb-Server-Limit für alle 3 Dateien zusammen + const MAX_RAW_UPLOAD_BYTES = 25 * 1024 * 1024; // Rohdatei vor Komprimierung — Fotos werden verkleinert, PDFs bleiben wie sie sind const tooLarge = [idFrontInput.files[0], idBackInput.files[0], schufaInput.files[0]] - .filter(f => f && f.size > MAX_UPLOAD_BYTES); + .filter(f => f && f.size > MAX_RAW_UPLOAD_BYTES); if (tooLarge.length) { - throw new Error(`Datei zu groß (max. 8 MB): ${tooLarge.map(f => f.name).join(', ')}. Bitte kleineres Foto/Scan wählen.`); + throw new Error(`Datei zu groß (max. 25 MB): ${tooLarge.map(f => f.name).join(', ')}. Bitte kleineres Foto/Scan wählen.`); } body.phoneNumber = document.getElementById('acceptPhone').value.trim(); body.firstResidenceAddress = document.getElementById('acceptFirstResidence').value.trim(); - body.idDocumentFrontUrl = await readFileAsDataUrl(idFrontInput.files[0]); - body.idDocumentBackUrl = await readFileAsDataUrl(idBackInput.files[0]); + body.idDocumentFrontUrl = await readFileAsCompressedDataUrl(idFrontInput.files[0], 1800, 0.82); + body.idDocumentBackUrl = await readFileAsCompressedDataUrl(idBackInput.files[0], 1800, 0.82); if (schufaInput.files.length) { - body.schufaDocumentUrl = await readFileAsDataUrl(schufaInput.files[0]); + body.schufaDocumentUrl = await readFileAsCompressedDataUrl(schufaInput.files[0], 1800, 0.82); body.schufaDocumentDate = schufaDateInput.value; } } @@ -2203,6 +2203,34 @@ }); } + // Handyfotos (Ausweis, Schufa-Scan) können 15-25MB pro Bild groß sein — + // als Data-URL im JSON-Body summiert sich das schnell über jedes sinnvolle + // Server-Limit. Bilder werden daher vor dem Versand auf eine für Dokumente + // mehr als ausreichende Auflösung herunterskaliert (PDFs bleiben unangetastet, + // die sind i. d. R. schon kompakt). + function readFileAsCompressedDataUrl(file, maxDimension, quality) { + if (file.type === 'application/pdf') return readFileAsDataUrl(file); + return new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => { + let { width, height } = img; + if (width > maxDimension || height > maxDimension) { + const scale = maxDimension / Math.max(width, height); + width = Math.round(width * scale); + height = Math.round(height * scale); + } + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + canvas.getContext('2d').drawImage(img, 0, 0, width, height); + URL.revokeObjectURL(img.src); + resolve(canvas.toDataURL('image/jpeg', quality || 0.82)); + }; + img.onerror = () => reject(new Error('Bild konnte nicht gelesen werden')); + img.src = URL.createObjectURL(file); + }); + } + function renderInventoryPhotoPreview() { const preview = document.getElementById('inventoryPhotoPreview'); preview.innerHTML = '';