diff --git a/server.js b/server.js index 2f1bccc..fa8998b 100644 --- a/server.js +++ b/server.js @@ -417,9 +417,61 @@ if (!fs.existsSync(basisAnhaengeDir)) { const ATTACHMENT_FILTER = (req, file, cb) => { const ext = path.extname(file.originalname).toLowerCase(); const ok = /^\.(pdf|png|jpe?g|gif|webp|docx?|rtf|odt|ods|txt|csv|xlsx?)$/.test(ext); - cb(ok ? null : new Error('Dateityp nicht erlaubt'), ok); + cb(ok ? null : dateitypFehler(), ok); }; +// Upload size limits. The numbers live here (not inline in the multer configs) +// because the message shown to the user quotes them — a limit that says one +// thing and enforces another is worse than no message at all. +const ANHANG_MAX_MB = 30; +const BILD_MAX_MB = 5; + +function dateitypFehler() { + const err = new Error('Dateityp nicht erlaubt'); + err.code = 'DATEITYP'; + return err; +} + +// Multer reports a rejected upload through its callback's `err`: a blown size +// limit, a blocked file type, a failed write. Those used to be logged to the +// server console and otherwise swallowed — the browser was redirected back to a +// page that simply did not list the file, which to the user looks exactly like +// "it uploaded but it isn't shown". Turn the error into a sentence they can act +// on. Returns null when the upload was fine. +function uploadFehlerText(err, maxMb) { + if (!err) return null; + if (err.code === 'LIMIT_FILE_SIZE') { + return `Die Datei ist zu groß. Erlaubt sind maximal ${maxMb} MB — bitte komprimiere sie (z. B. den Scan als PDF verkleinern) und lade sie erneut hoch.`; + } + if (err.code === 'DATEITYP') { + return 'Dieser Dateityp ist nicht erlaubt. Möglich sind PDF, Bilder (PNG, JPG, GIF, WebP), Word, ODT, RTF, Text, CSV und Excel.'; + } + if (err.code === 'BILDTYP') { + return 'Für dieses Bild sind nur PNG- oder JPG-Dateien möglich.'; + } + return `Die Datei konnte nicht hochgeladen werden: ${err.message || 'unbekannter Fehler'}`; +} + +// Bilder (Unterschrift, Bewerbungsfoto): nur PNG/JPG, und der Dateiname muss zur +// MIME-Art passen. Wie oben mit einem Fehler ablehnen statt still zu verwerfen. +const BILD_FILTER = (req, file, cb) => { + const ok = /^image\/(png|jpe?g)$/.test(file.mimetype) + && /^\.(png|jpe?g)$/.test(path.extname(file.originalname).toLowerCase()); + cb(ok ? null : bildtypFehler(), ok); +}; + +function bildtypFehler() { + const err = new Error('Nur PNG oder JPG'); + err.code = 'BILDTYP'; + return err; +} + +// Ein Ziel plus Fehlermeldung als Query-Parameter — dasselbe Muster wie auf der +// Jobsuche-Seite (?fehler=…), das die Views bereits als Banner rendern. +function mitFehler(pfad, text) { + return `${pfad}${pfad.includes('?') ? '&' : '?'}fehler=${encodeURIComponent(text)}`; +} + const uploadBasisAnhang = multer({ storage: multer.diskStorage({ destination: (req, file, cb) => cb(null, userStorageDirForReq(basisAnhaengeDir, req)), @@ -428,7 +480,7 @@ const uploadBasisAnhang = multer({ cb(null, `${Date.now()}_${safe}`); }, }), - limits: { fileSize: 15 * 1024 * 1024 }, // 15 MB + limits: { fileSize: ANHANG_MAX_MB * 1024 * 1024 }, fileFilter: ATTACHMENT_FILTER, }).single('datei'); @@ -447,7 +499,7 @@ const uploadInterneAnhang = multer({ cb(null, `${Date.now()}_${safe}`); }, }), - limits: { fileSize: 15 * 1024 * 1024 }, // 15 MB + limits: { fileSize: ANHANG_MAX_MB * 1024 * 1024 }, fileFilter: ATTACHMENT_FILTER, }).single('datei'); @@ -466,10 +518,8 @@ const uploadSignatur = multer({ cb(null, `signatur_${Date.now()}${ext}`); }, }), - limits: { fileSize: 5 * 1024 * 1024 }, // 5 MB - fileFilter: (req, file, cb) => cb(null, - /^image\/(png|jpe?g)$/.test(file.mimetype) - && /^\.(png|jpe?g)$/.test(path.extname(file.originalname).toLowerCase())), + limits: { fileSize: BILD_MAX_MB * 1024 * 1024 }, + fileFilter: BILD_FILTER, }).single('signatur'); // The single stored signature file, if any (in the current user's subdir). @@ -511,10 +561,8 @@ const uploadFoto = multer({ cb(null, `foto_${Date.now()}${ext}`); }, }), - limits: { fileSize: 5 * 1024 * 1024 }, // 5 MB - fileFilter: (req, file, cb) => cb(null, - /^image\/(png|jpe?g)$/.test(file.mimetype) - && /^\.(png|jpe?g)$/.test(path.extname(file.originalname).toLowerCase())), + limits: { fileSize: BILD_MAX_MB * 1024 * 1024 }, + fileFilter: BILD_FILTER, }).single('foto'); // The single stored photo file, if any (in the current user's subdir). @@ -3008,6 +3056,10 @@ initializeDatabase().then(async () => { basisTypOptions: BASIS_TYP_OPTIONS, hasApiKey: Boolean(config.get('OLLAMA_API_KEY')), hideSettings: true, + // Set by the upload routes when multer rejected a file (too large, wrong + // type) — without this the page just came back without the attachment. + fehler: req.query.fehler ? String(req.query.fehler) : null, + anhangMaxMb: ANHANG_MAX_MB, }); } catch (error) { console.error('Error loading vorlagen:', error); @@ -3206,18 +3258,19 @@ initializeDatabase().then(async () => { try { if (err) { console.error('Upload error:', err.message); - return res.redirect('/vorlagen'); + return res.redirect(mitFehler('/vorlagen', uploadFehlerText(err, ANHANG_MAX_MB))); } - if (req.file) { - const original = req.file.originalname || req.file.filename; - const name = (req.body.name && req.body.name.trim()) - ? sanitizeInput(req.body.name.trim()) - : sanitizeInput(original.replace(/\.[^.]+$/, '')); - await dbRun( - 'INSERT INTO basis_anhaenge (user_id, name, dateiname, mime, pfad) VALUES (?, ?, ?, ?, ?)', - [uidFromReq(req), name, sanitizeInput(original), req.file.mimetype || 'application/octet-stream', req.file.filename] - ); + if (!req.file) { + return res.redirect(mitFehler('/vorlagen', 'Es wurde keine Datei ausgewählt.')); } + const original = req.file.originalname || req.file.filename; + const name = (req.body.name && req.body.name.trim()) + ? sanitizeInput(req.body.name.trim()) + : sanitizeInput(original.replace(/\.[^.]+$/, '')); + await dbRun( + 'INSERT INTO basis_anhaenge (user_id, name, dateiname, mime, pfad) VALUES (?, ?, ?, ?, ?)', + [uidFromReq(req), name, sanitizeInput(original), req.file.mimetype || 'application/octet-stream', req.file.filename] + ); res.redirect('/vorlagen'); } catch (error) { console.error('Error saving attachment:', error); @@ -3269,7 +3322,10 @@ initializeDatabase().then(async () => { app.post('/unterschrift', (req, res) => { uploadSignatur(req, res, (err) => { try { - if (err) console.error('Signature upload error:', err.message); + if (err) { + console.error('Signature upload error:', err.message); + return res.redirect(mitFehler('/vorlagen', uploadFehlerText(err, BILD_MAX_MB))); + } if (req.file) { // keep only the newly uploaded file (in the user's subdir). Resolve the // dir from req.user (not currentUserId()) — this runs in the multer @@ -3309,7 +3365,10 @@ initializeDatabase().then(async () => { app.post('/bewerbungsfoto', (req, res) => { uploadFoto(req, res, (err) => { try { - if (err) console.error('Photo upload error:', err.message); + if (err) { + console.error('Photo upload error:', err.message); + return res.redirect(mitFehler('/vorlagen', uploadFehlerText(err, BILD_MAX_MB))); + } if (req.file) { // keep only the newly uploaded file (in the user's subdir). Resolve the // dir from req.user — see /unterschrift handler for why (multer callback). diff --git a/views/vorlagen.ejs b/views/vorlagen.ejs index c552f49..8235cd0 100644 --- a/views/vorlagen.ejs +++ b/views/vorlagen.ejs @@ -15,6 +15,14 @@ Zurück zur Übersicht + + <% if (typeof fehler !== 'undefined' && fehler) { %> + + <% } %> +

Basis-Unterlagen (Vorlagen)

Diese Unterlagen dienen der KI als Faktengrundlage. Beim Import einer Stelle über die Browser-Erweiterung @@ -240,7 +248,9 @@

- +