feat(files): preview attachments via short-lived signed URLs

Downloads require an Authorization header, which <iframe>/<img> cannot
send. The previous blob: workaround broke under download managers (IDM
hooks blob URLs, fails to resolve them, and leaves a 0-byte file) and in
Chrome's mobile emulation, which has no inline PDF plugin.

Backend:
- preview_token.py: HMAC-signed token bound to one (quote, kind, file)
  with a 5-minute TTL, keyed off an existing secret
- GET /quotes/{id}/preview-url/{kind}/{attachment} mints the URL
- the two file GET routes accept a Bearer session OR the signed ?t= token
  (they leave the global auth dep, so the other six file routes now carry
  it explicitly — nothing is open by omission)

Frontend:
- api.files.previewUrl() replaces the blob helper
- shared FilePreviewModal + useFilePreview; FileList and the dashboard
  quote card both use it
- PDFs render with the browser's native viewer on desktop and pdf.js on
  canvas where no inline viewer exists (mobile); pdf.js is fed the bytes
  in memory so no PDF URL is exposed for a download manager to intercept
- removed the per-attachment download buttons on the dashboard card and
  the modal header

Ops:
- nginx: explicit MIME types (.mjs as text/javascript) — a module worker
  served as octet-stream is refused and crashed the page
- frontend service bypasses the nginx entrypoint, whose apk manifest call
  hangs when the package mirror is unreachable (502s)
This commit is contained in:
MMD
2026-09-20 20:35:16 +03:30
parent e637d80b76
commit baa32a2652
14 changed files with 796 additions and 172 deletions
+2 -1
View File
@@ -10,7 +10,6 @@
"preview": "vite preview"
},
"dependencies": {
"@sentry/react": "^10.0.0",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/utilities": "^3.2.2",
"@hookform/resolvers": "3.10",
@@ -20,11 +19,13 @@
"@radix-ui/react-label": "^2.1.15",
"@radix-ui/react-select": "^2.3.7",
"@radix-ui/react-slot": "^1.3.3",
"@sentry/react": "^10.0.0",
"@tailwindcss/vite": "^4.3.3",
"@tanstack/react-query": "^5.101.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^1.30.0",
"pdfjs-dist": "^4.10.38",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"react-hook-form": "^7.85.0",