# Honor a reverse proxy's X-Forwarded-Proto (e.g. traefik terminating TLS) so the API sees # https and builds https OAuth redirects; fall back to the connection scheme for direct access. map $http_x_forwarded_proto $fwd_proto { default $scheme; https https; http http; } server { listen 80; server_name _; root /usr/share/nginx/html; index index.html; # AUDIT M-2: security headers on the SPA. script-src 'self' works because the theme # bootstrap lives in /theme-init.js (no inline scripts); style-src needs 'unsafe-inline' # for React/Chart.js/grid-layout inline style attributes (low risk with script-src locked). add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always; add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "DENY" always; add_header Referrer-Policy "no-referrer" always; # Compress the SPA bundle (AUDIT perf note: ~680 KB JS). gzip on; gzip_types text/css application/javascript application/json image/svg+xml; gzip_min_length 1024; # SPA fallback. location / { try_files $uri $uri/ /index.html; } # Proxy API + auth calls to the backend container. location /api/ { proxy_pass http://api:8080; proxy_set_header Host $http_host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $fwd_proto; proxy_set_header X-Forwarded-Host $http_host; proxy_set_header Cookie $http_cookie; } # Google OAuth2 callback + sign-out land here (not under /api) and must # reach the backend so the cookie session is established same-origin. location ~ ^/(signin-google|signout-google) { proxy_pass http://api:8080; proxy_set_header Host $http_host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $fwd_proto; proxy_set_header X-Forwarded-Host $http_host; proxy_set_header Cookie $http_cookie; } }