fix: sanitize document filenames and add upload validation (#171)

* fix: sanitize document filenames and add server-side upload validation

Filenames with spaces or non-ASCII characters (e.g. Swedish ö, ä, å) caused
Supabase Storage to reject uploads with "Invalid key". This adds filename
sanitization, server-side size/type validation on both upload routes, and
fixes a duplicate-filename race condition in the upload UI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: require MIME type and handle empty sanitized filenames

Address review feedback:
- MIME type check now rejects files with missing/empty Content-Type
  instead of silently allowing them through
- Fallback to 'file' when sanitized base is empty (e.g. ööö.pdf)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-04-03 14:44:38 +02:00
committed by GitHub
co-authored by Claude Opus 4.6
parent f95e6437f2
commit 1dcec370b6
4 changed files with 68 additions and 6 deletions
@@ -12,6 +12,8 @@ export interface UploadedFile {
error?: string
fileName: string
fileSize: number
/** Unique key to track this upload (handles duplicate filenames) */
uploadKey: string
}
interface DocumentUploadZoneProps {
@@ -23,6 +25,7 @@ interface DocumentUploadZoneProps {
compact?: boolean
}
let uploadCounter = 0
const MAX_FILE_SIZE = 10 * 1024 * 1024 // 10 MB
const ACCEPTED_TYPES = ['application/pdf', 'image/jpeg', 'image/png', 'image/webp']
const ACCEPTED_EXTENSIONS = '.pdf,.jpg,.jpeg,.png,.webp'
@@ -87,6 +90,7 @@ export default function DocumentUploadZone({
error: 'Filtypen stöds inte',
fileName: file.name,
fileSize: file.size,
uploadKey: `upload-${++uploadCounter}`,
})
continue
}
@@ -97,6 +101,7 @@ export default function DocumentUploadZone({
error: 'Filen är för stor (max 10 MB)',
fileName: file.name,
fileSize: file.size,
uploadKey: `upload-${++uploadCounter}`,
})
continue
}
@@ -105,6 +110,7 @@ export default function DocumentUploadZone({
status: 'uploading',
fileName: file.name,
fileSize: file.size,
uploadKey: `upload-${++uploadCounter}`,
})
}
@@ -115,7 +121,7 @@ export default function DocumentUploadZone({
for (const f of validFiles.filter((f) => f.status === 'uploading')) {
const result = await uploadFile(f)
currentFiles = currentFiles.map((cf) =>
cf.fileName === result.fileName && cf.status === 'uploading' ? result : cf
cf.uploadKey === f.uploadKey ? result : cf
)
onFilesChange([...currentFiles])
}
@@ -200,7 +206,7 @@ export default function DocumentUploadZone({
<div className="space-y-1">
{files.map((file, index) => (
<div
key={`${file.fileName}-${index}`}
key={file.uploadKey}
className="flex items-center gap-2 text-sm py-1.5 px-2 rounded bg-muted/50"
>
{isImageType(file.file.type) ? (