Compress ID/Schufa photos client-side before upload, raise server limit further

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.
This commit is contained in:
Giuseppe Lombardo 2026-08-13 11:52:12 +00:00
parent 8f4b985f79
commit 48fb4118fd
2 changed files with 35 additions and 7 deletions

View File

@ -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);

View File

@ -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 = '';