Chat-Anhaenge: Korrekturen aus dem Code-Review

- Kamerafotos in anderen Formaten (z. B. HEIF von Android-Kameras) werden
  wie Dateien angenommen statt abgelehnt
- Dateinamen nach Zeichen kuerzen: ein halbiertes Emoji machte den Anhang
  unabrufbar (encodeURIComponent)
- Endet eine Aufnahme von selbst (Headset getrennt, Anruf), bleibt die
  Sprachnachricht erhalten statt "zu kurz"
- Kamera-App nur auf iOS/iPadOS/Android; Touch-Laptops bekommen die Kamera
  im Browser statt eines Dateidialogs
- Knapp neben dem Chat abgelegte Dateien oeffnet der Browser nicht mehr
  selbst (Portal bliebe sonst stehen); Datei-Felder bleiben Ablagen
- Fehler beim Ausliefern kommt als JSON statt mit dem Dateityp
- Server liest hoechstens 4 Uploads gleichzeitig ein (Speicher), weitere
  warten; abgebrochene Wartende geben ihren Platz frei

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-09-26 14:01:17 +00:00
1 parent 35d98da977
commit 06e9a0fe8d
4 files changed
+57 -8

No files matched your search

+10 -6
View File
@@ -21,6 +21,7 @@ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
const CUSTOMER_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,35}$/
// Erlaubte Endungen. Programme, Skripte, HTML und Office-Dateien mit Makros bleiben draussen.
// Dieselbe Liste steht in public/chat-composer.js (ALLOWED_EXT) fuer die Pruefung vor dem Upload.
export const ALLOWED_EXTENSIONS = [
'jpg', 'jpeg', 'png', 'gif', 'webp', 'heic', 'heif', 'avif', 'bmp', 'tif', 'tiff', 'svg',
'pdf', 'txt', 'csv', 'rtf', 'md', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'odt', 'ods', 'odp',
@@ -92,7 +93,8 @@ export function cleanFileName(name, ext) {
.trim()
const own = extOf(base)
if (own) base = base.slice(0, -(own.length + 1))
base = base.slice(0, 100).trim() || 'Anhang'
// Nach Zeichen kuerzen: ein halbiertes Emoji liesse encodeURIComponent beim Download scheitern
base = Array.from(base).slice(0, 100).join('').trim() || 'Anhang'
return `${base}.${own && (own === ext || ALLOWED.has(own)) ? own : ext}`
}
@@ -110,8 +112,8 @@ function intHeader(value, max) {
}
/**
* Art des Anhangs bestimmen. `hint` kommt vom Browser ('voice' = Sprachaufnahme, 'camera' = Kamerafoto)
* und entscheidet nur, wie etwas angezeigt wird - erlaubt wird nach Inhalt bzw. Endung.
* Art des Anhangs bestimmen. `hint` kommt vom Browser ('voice' = Sprachaufnahme) und entscheidet nur,
* wie etwas angezeigt wird - erlaubt wird nach Inhalt bzw. Endung.
*/
export function classifyUpload(buffer, name, hint) {
const label = String(name || '').slice(0, 120) || 'Datei'
@@ -123,8 +125,7 @@ export function classifyUpload(buffer, name, hint) {
if (!audio) throw httpError(400, 'Die Sprachnachricht konnte nicht gelesen werden. Bitte noch einmal aufnehmen.')
return { kind: 'voice', mime: audio.mime, ext: audio.ext }
}
if (hint === 'camera') throw httpError(400, 'Das Foto konnte nicht gelesen werden. Bitte noch einmal aufnehmen.')
// Kamerafotos in anderen Formaten (z. B. HEIF von Android-Kameras) gehen wie jede Datei nach Endung durch
const ext = extOf(name)
if (!ALLOWED.has(ext)) throw httpError(415, `„${label}“: Dieser Dateityp wird im Chat nicht unterstützt.`)
if (isExecutable(buffer)) throw httpError(415, `„${label}“ ist ein Programm und kann nicht gesendet werden.`)
@@ -333,7 +334,10 @@ export function sendAttachment(req, res, { meta, file }) {
res.setHeader('Content-Security-Policy', "default-src 'none'; img-src 'self'; media-src 'self'; style-src 'unsafe-inline'; sandbox")
}
res.sendFile(file, { cacheControl: false, etag: false, lastModified: false }, (err) => {
if (err && !res.headersSent) res.status(err.status || 404).json({ error: 'Datei nicht verfügbar' })
if (!err || res.headersSent) return
res.removeHeader('Content-Disposition')
res.removeHeader('Content-Security-Policy')
res.status(err.status || 404).type('application/json').json({ error: 'Datei nicht verfügbar' })
})
}