Files
ERP/frontend/nginx.conf
MMD baa32a2652 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)
2026-09-20 20:35:16 +03:30

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";
}
}