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:
+10
-12
@@ -1320,18 +1320,16 @@ export const api = {
|
||||
},
|
||||
// آدرس blob احراز هویتشده برای پیشنمایش (تصویر/PDF در مودال).
|
||||
// تگهای <img>/<iframe>/<a href> توکن نمیفرستند، پس بایتها را با توکن
|
||||
// میگیریم و یک objectURL محلی برمیگردانیم. آزادسازی با revokeBlobUrl.
|
||||
blobUrl: async (quoteId: string, kind: FileKind, id: string): Promise<string> => {
|
||||
let res: Response;
|
||||
const path = `/quotes/${quoteId}/${kind}/${id}`;
|
||||
try {
|
||||
res = await fetch(BASE_URL + path, { headers: authHeaders() });
|
||||
} catch {
|
||||
throw new ApiError("اتصال به سرور برقرار نشد — آیا بکاند در دسترس است؟", 0);
|
||||
}
|
||||
handle401(path, res);
|
||||
if (!res.ok) throw new ApiError(faHttpError(res.status, "خطای سرور"), res.status);
|
||||
return URL.createObjectURL(await res.blob());
|
||||
// آدرس پیشنمایش احراز هویتشده برای <iframe>/<img>.
|
||||
// اینها نمیتوانند هدر Authorization بفرستند، پس بکاند یک توکن
|
||||
// امضاشدهی کوتاهعمر در کوئری میدهد. عمداً blob ساخته نمیشود:
|
||||
// دانلودمنیجرها (IDM) blob را شکار میکنند و فایل ۰ بایتی میسازند،
|
||||
// در حالی که این یک URL واقعی است و درست بارگذاری میشود.
|
||||
previewUrl: async (quoteId: string, kind: FileKind, id: string): Promise<string> => {
|
||||
const res = await getData<{ url: string; token: string }>(
|
||||
`/quotes/${quoteId}/preview-url/${kind}/${id}`,
|
||||
);
|
||||
return BASE_URL + res.url;
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user