PowerPoint-grade editing: the slot picker now takes a device upload for ANY slide/slot — not just the garment's stored shots — and the fresh image is adjustable like any placed image (slot_adjust reads the same file). Storage decision (owner: deck-scoped): uploads belong to the deck and never touch the library. POST /api/presentations/uploads stores the file under <data>/deck_uploads/<draft_id>/ (the wizard's client-generated uuid) after validating content type (jpeg/png/webp), the real payload (Pillow) and a 10 MB cap; the returned `upload:<draft>/<file>` ref rides `overrides` like a shot ref (services.deck_uploads + presentation_service._match_shot_ref). /publish copies the referenced files into the board's own dir and rewrites the refs, so the deck owns its uploads; a board hard-delete removes that dir (routers/collections.delete_board). Files are served by a /deck-uploads static mount (backend main.py + nginx), resolved on the frontend through imageUrl()'s new upload-ref branch. Frontend: new DeckImageUpload component (file pick, progress, inline errors, uploaded thumbnails as re-pickable candidates kept per session) hooked into SlotEditor with a few lines; app-native UI only. Verified: backend tests/test_presentations_uploads.py (12 passed, docker python:3.11-slim + requirements-test + router deps); sibling presentations + board-delete suites 101 passed/2 pre-existing skips; frontend tsc -b, eslint (zero findings), vite build, component suite 31 passed (incl. 3 new); fastapi-route-check clean. Test file registered in ci.yml + deploy-dev.yml.
141 lines
9.8 KiB
Nginx Configuration File
141 lines
9.8 KiB
Nginx Configuration File
server {
|
|
listen 80;
|
|
server_name _;
|
|
root /usr/share/nginx/html;
|
|
index index.html;
|
|
|
|
# ── Security headers (2026-09-18 pilot audit) ──────────────────────────
|
|
# Declared at server level AND repeated inside every location that defines
|
|
# its own add_header: nginx does NOT inherit a parent's add_header once a
|
|
# child block declares one, so a single server-level block would silently
|
|
# skip /assets/, /index.html, /sw.js and the manifest.
|
|
#
|
|
# CSP (baseline 2026-09-20; script-src tightened for #1346): origin
|
|
# allow-list with NO 'unsafe-inline' in script-src — verified against the
|
|
# built bundle (dist/index.html carries zero inline scripts; only
|
|
# /assets/*.js and /registerSW.js, both 'self') and Google's documented GSI
|
|
# policy, which needs no inline script. 'wasm-unsafe-eval' + the two CDNs
|
|
# stay: the Lottie player fetches and instantiates its wasm there.
|
|
# style-src KEEPS 'unsafe-inline' (React style attributes, the login page's
|
|
# <style> block, and GSI's injected styles); img-src keeps https: for the
|
|
# scraper's external-image fallback and the moodboard samples.
|
|
add_header Strict-Transport-Security "max-age=15552000" always;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
add_header Permissions-Policy "geolocation=(self), camera=(self), microphone=(), payment=()" always;
|
|
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'wasm-unsafe-eval' https://accounts.google.com https://apis.google.com https://cdn.jsdelivr.net https://unpkg.com; style-src 'self' 'unsafe-inline' https://accounts.google.com https://fonts.googleapis.com; font-src 'self' data: https://fonts.gstatic.com; img-src 'self' data: blob: https:; media-src 'self' blob:; connect-src 'self' https://accounts.google.com https://oauth2.googleapis.com https://cdn.jsdelivr.net https://unpkg.com; frame-src 'self' https://accounts.google.com; worker-src 'self' blob:; manifest-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'; upgrade-insecure-requests" always;
|
|
server_tokens off;
|
|
|
|
# Allow large file uploads (moodboard images, PDFs, bulk capture batches).
|
|
# Bulk garment capture accepts up to 50 shots x 10MB, so the aggregate body
|
|
# can far exceed any per-file bound — 512M covers the worst case.
|
|
client_max_body_size 512M;
|
|
|
|
# Docker's embedded DNS. Combined with a *variable* in proxy_pass below, this
|
|
# makes nginx RE-RESOLVE the backend container's IP at request time (every
|
|
# ~10s) instead of caching it once at startup. Without this, each backend
|
|
# redeploy gives the container a new IP and nginx keeps dialing the old, dead
|
|
# one → "upstream timed out while connecting" → 504 on every /api call.
|
|
resolver 127.0.0.11 valid=10s ipv6=off;
|
|
|
|
# Hashed build assets (index-XXXX.js / .css) — the filename changes every
|
|
# build, so it's safe to cache them forever.
|
|
location /assets/ {
|
|
expires 1y;
|
|
add_header Cache-Control "public, max-age=31536000, immutable";
|
|
add_header Strict-Transport-Security "max-age=15552000" always;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
add_header Permissions-Policy "geolocation=(self), camera=(self), microphone=(), payment=()" always;
|
|
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'wasm-unsafe-eval' https://accounts.google.com https://apis.google.com https://cdn.jsdelivr.net https://unpkg.com; style-src 'self' 'unsafe-inline' https://accounts.google.com https://fonts.googleapis.com; font-src 'self' data: https://fonts.gstatic.com; img-src 'self' data: blob: https:; media-src 'self' blob:; connect-src 'self' https://accounts.google.com https://oauth2.googleapis.com https://cdn.jsdelivr.net https://unpkg.com; frame-src 'self' https://accounts.google.com; worker-src 'self' blob:; manifest-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'; upgrade-insecure-requests" always;
|
|
}
|
|
|
|
# index.html must NEVER be cached, or browsers keep loading an old bundle
|
|
# after a deploy (the blank-page-after-update problem). Always revalidate.
|
|
location = /index.html {
|
|
add_header Cache-Control "no-store, no-cache, must-revalidate";
|
|
add_header Strict-Transport-Security "max-age=15552000" always;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
add_header Permissions-Policy "geolocation=(self), camera=(self), microphone=(), payment=()" always;
|
|
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'wasm-unsafe-eval' https://accounts.google.com https://apis.google.com https://cdn.jsdelivr.net https://unpkg.com; style-src 'self' 'unsafe-inline' https://accounts.google.com https://fonts.googleapis.com; font-src 'self' data: https://fonts.gstatic.com; img-src 'self' data: blob: https:; media-src 'self' blob:; connect-src 'self' https://accounts.google.com https://oauth2.googleapis.com https://cdn.jsdelivr.net https://unpkg.com; frame-src 'self' https://accounts.google.com; worker-src 'self' blob:; manifest-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'; upgrade-insecure-requests" always;
|
|
expires -1;
|
|
}
|
|
|
|
# Service worker must NEVER be cached, or updates stall and users get a
|
|
# stale SW controlling a stale app shell.
|
|
location = /sw.js {
|
|
add_header Cache-Control "no-store, no-cache, must-revalidate";
|
|
add_header Strict-Transport-Security "max-age=15552000" always;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
add_header Permissions-Policy "geolocation=(self), camera=(self), microphone=(), payment=()" always;
|
|
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'wasm-unsafe-eval' https://accounts.google.com https://apis.google.com https://cdn.jsdelivr.net https://unpkg.com; style-src 'self' 'unsafe-inline' https://accounts.google.com https://fonts.googleapis.com; font-src 'self' data: https://fonts.gstatic.com; img-src 'self' data: blob: https:; media-src 'self' blob:; connect-src 'self' https://accounts.google.com https://oauth2.googleapis.com https://cdn.jsdelivr.net https://unpkg.com; frame-src 'self' https://accounts.google.com; worker-src 'self' blob:; manifest-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'; upgrade-insecure-requests" always;
|
|
expires -1;
|
|
}
|
|
|
|
# Web app manifest must ALWAYS revalidate so icon/theme/display changes
|
|
# reach browsers within 24h.
|
|
location = /manifest.webmanifest {
|
|
add_header Cache-Control "no-store, no-cache, must-revalidate";
|
|
add_header Strict-Transport-Security "max-age=15552000" always;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
add_header Permissions-Policy "geolocation=(self), camera=(self), microphone=(), payment=()" always;
|
|
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'wasm-unsafe-eval' https://accounts.google.com https://apis.google.com https://cdn.jsdelivr.net https://unpkg.com; style-src 'self' 'unsafe-inline' https://accounts.google.com https://fonts.googleapis.com; font-src 'self' data: https://fonts.gstatic.com; img-src 'self' data: blob: https:; media-src 'self' blob:; connect-src 'self' https://accounts.google.com https://oauth2.googleapis.com https://cdn.jsdelivr.net https://unpkg.com; frame-src 'self' https://accounts.google.com; worker-src 'self' blob:; manifest-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'; upgrade-insecure-requests" always;
|
|
default_type application/manifest+json;
|
|
expires -1;
|
|
}
|
|
|
|
# SPA routing — serve index.html for non-file routes
|
|
location / {
|
|
try_files $uri $uri/ /index.html;
|
|
}
|
|
|
|
# Proxy API calls to backend. The `set` + variable forces runtime DNS
|
|
# resolution (see resolver above); $request_uri preserves the exact path+query.
|
|
location /api/ {
|
|
set $backend backend;
|
|
proxy_pass http://$backend:8000$request_uri;
|
|
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;
|
|
# Fail fast if the backend is genuinely down (don't hang 60s); but allow
|
|
# long-running AI calls (KAARIGAR, scraper) to complete.
|
|
proxy_connect_timeout 10s;
|
|
proxy_read_timeout 300s;
|
|
}
|
|
|
|
# Proxy static uploads to backend
|
|
location /images/ {
|
|
set $backend backend;
|
|
proxy_pass http://$backend:8000$request_uri;
|
|
proxy_set_header Host $host;
|
|
}
|
|
|
|
location /scraped/ {
|
|
set $backend backend;
|
|
proxy_pass http://$backend:8000$request_uri;
|
|
proxy_set_header Host $host;
|
|
}
|
|
|
|
location /concepts/ {
|
|
set $backend backend;
|
|
proxy_pass http://$backend:8000$request_uri;
|
|
proxy_set_header Host $host;
|
|
}
|
|
|
|
# Deck-scoped device uploads (#1561) — the wizard's uploaded slide images.
|
|
location /deck-uploads/ {
|
|
set $backend backend;
|
|
proxy_pass http://$backend:8000$request_uri;
|
|
proxy_set_header Host $host;
|
|
}
|
|
}
|