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)
70 lines
2.6 KiB
Nginx Configuration File
70 lines
2.6 KiB
Nginx Configuration File
# Frontend nginx config
|
|
# - Serves the built SPA from /usr/share/nginx/html
|
|
# - Proxies /api/* to the backend service (Docker compose network name: api)
|
|
|
|
server {
|
|
listen 80;
|
|
server_name _;
|
|
|
|
root /usr/share/nginx/html;
|
|
index index.html;
|
|
|
|
# حداکثر حجم فایلهای آپلودی (فاکتور/پیشفاکتور) — پیشفرض nginx فقط ۱ مگابایت
|
|
# است و آپلودهای بزرگتر با خطای 413 رد میشوند.
|
|
client_max_body_size 50m;
|
|
|
|
# API reverse proxy -> FastAPI backend (port 8000)
|
|
# NOTE: upstream is resolved at REQUEST time through Docker's embedded DNS
|
|
# (127.0.0.11). A plain `proxy_pass http://api:8000` caches the container
|
|
# IP at nginx startup, so after the api container restarts and gets a new
|
|
# IP, nginx keeps proxying to the stale address (which may belong to
|
|
# another service) until this container is restarted too.
|
|
location /api/ {
|
|
resolver 127.0.0.11 valid=10s ipv6=off;
|
|
set $api_upstream http://api:8000;
|
|
proxy_pass $api_upstream;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
}
|
|
|
|
# MIME types: the pdf.js worker ships as .mjs, and a browser REFUSES to
|
|
# start a module worker served as application/octet-stream (nginx's
|
|
# default for unknown extensions) — which crashes the PDF preview page.
|
|
# Declared explicitly rather than relying on the system mime.types.
|
|
types {
|
|
text/javascript mjs js;
|
|
text/css css;
|
|
text/html html;
|
|
image/svg+xml svg;
|
|
image/png png;
|
|
image/jpeg jpg jpeg;
|
|
image/webp webp;
|
|
image/x-icon ico;
|
|
font/woff2 woff2;
|
|
font/woff woff;
|
|
application/json json;
|
|
application/pdf pdf;
|
|
application/wasm wasm;
|
|
}
|
|
|
|
# SPA fallback — all client-side routes go to index.html
|
|
location / {
|
|
try_files $uri $uri/ /index.html;
|
|
}
|
|
|
|
# index.html itself must never be cached: after a redeploy the hashed
|
|
# /assets/* names change, and a stale cached HTML would 404 on the old
|
|
# chunks (blank page). Content-hashed assets below stay immutable.
|
|
location = /index.html {
|
|
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
|
}
|
|
|
|
# Static assets cache — hashed filenames are content-addressed
|
|
location /assets/ {
|
|
expires 30d;
|
|
add_header Cache-Control "public, immutable";
|
|
}
|
|
}
|