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:
parent
8f4b985f79
commit
48fb4118fd
@ -39,7 +39,7 @@ export function createApp() {
|
|||||||
// versendet werden (keine separate Objekt-Storage-Anbindung in diesem
|
// versendet werden (keine separate Objekt-Storage-Anbindung in diesem
|
||||||
// Demo-Stand). 10mb reichte für einzelne Fotos, war aber zu knapp für drei
|
// Demo-Stand). 10mb reichte für einzelne Fotos, war aber zu knapp für drei
|
||||||
// hochauflösende Handyfotos in einem Request (HTTP 413).
|
// 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', paymentsRouter);
|
||||||
app.use('/v1', cockpitRouter);
|
app.use('/v1', cockpitRouter);
|
||||||
app.use('/v1', authRouter);
|
app.use('/v1', authRouter);
|
||||||
|
|||||||
@ -1209,18 +1209,18 @@
|
|||||||
const password = document.getElementById('acceptPassword').value;
|
const password = document.getElementById('acceptPassword').value;
|
||||||
const body = { fullName, password };
|
const body = { fullName, password };
|
||||||
if (isTenant) {
|
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]]
|
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) {
|
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.phoneNumber = document.getElementById('acceptPhone').value.trim();
|
||||||
body.firstResidenceAddress = document.getElementById('acceptFirstResidence').value.trim();
|
body.firstResidenceAddress = document.getElementById('acceptFirstResidence').value.trim();
|
||||||
body.idDocumentFrontUrl = await readFileAsDataUrl(idFrontInput.files[0]);
|
body.idDocumentFrontUrl = await readFileAsCompressedDataUrl(idFrontInput.files[0], 1800, 0.82);
|
||||||
body.idDocumentBackUrl = await readFileAsDataUrl(idBackInput.files[0]);
|
body.idDocumentBackUrl = await readFileAsCompressedDataUrl(idBackInput.files[0], 1800, 0.82);
|
||||||
if (schufaInput.files.length) {
|
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;
|
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() {
|
function renderInventoryPhotoPreview() {
|
||||||
const preview = document.getElementById('inventoryPhotoPreview');
|
const preview = document.getElementById('inventoryPhotoPreview');
|
||||||
preview.innerHTML = '';
|
preview.innerHTML = '';
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user