HR-ATS-Portal/frontend/nginx.conf

109 lines
4.7 KiB
Nginx Configuration File

server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# CV / multipart uploads (MAX_PDF_SIZE_MB is 10; leave headroom for form fields).
client_max_body_size 25m;
# Resolve backend-api through Docker's embedded DNS on each request instead of
# once at startup. `docker compose up` recreates backend-api with a new IP;
# a static upstream keeps the old one and every API call 502s until nginx restarts.
# proxy_pass with a variable and no URI forwards the original request URI unchanged.
resolver 127.0.0.11 valid=10s ipv6=off;
set $backend_api http://backend-api:8000;
# Security headers on every response.
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options DENY always;
add_header Referrer-Policy strict-origin-when-cross-origin always;
# Same-origin API proxy. The SPA is built with an empty VITE_API_BASE so
# fetch('/jobs/fetch') stays on this host. OpenAPI (/docs, /redoc,
# /openapi.json) is intentionally NOT proxied.
#
# Paths that are BOTH React routes (/jobs, /inbox, …) and API prefixes must
# require a sub-path: otherwise a cold open / refresh of /jobs is stolen by
# the proxy and returns a FastAPI 404 instead of index.html.
# SPA page roots that also prefix API calls — sub-path required.
location ~ ^/(jobs|inbox|pipeline|tasks|assessments|offers|managers|analytics|notifications)/ {
proxy_pass $backend_api;
proxy_http_version 1.1;
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;
proxy_set_header Authorization $http_authorization;
proxy_connect_timeout 10s;
proxy_send_timeout 120s;
proxy_read_timeout 120s;
proxy_request_buffering off;
}
# API-only prefixes (no SPA page at the bare path).
location ~ ^/(health|users|roles|permissions|permission-tags|email|job|candidate|notes|interview|feedback|activity|org-settings|saved-searches|search|documents|sheet|s3|forms|department)(/|$) {
proxy_pass $backend_api;
proxy_http_version 1.1;
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;
proxy_set_header Authorization $http_authorization;
proxy_connect_timeout 10s;
proxy_send_timeout 120s;
proxy_read_timeout 120s;
proxy_request_buffering off;
}
# One SPA at `/`. Without this, /auth/confirm-email (a router path, not a
# file) 404s when a confirmation email link is opened cold.
location / {
try_files $uri $uri/ /index.html;
}
# Hashed filenames, so they can be cached hard. A miss after a rebuild is a
# stale tab (old import() hash) — 404 + immutable would pin that miss for a
# year, so JS falls through to a one-shot reload of index.html.
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options DENY always;
add_header Referrer-Policy strict-origin-when-cross-origin always;
location ~ \.js$ {
expires 1y;
add_header Cache-Control "public, immutable";
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options DENY always;
add_header Referrer-Policy strict-origin-when-cross-origin always;
try_files $uri @stale_js;
}
}
location @stale_js {
default_type application/javascript;
add_header Cache-Control "no-store" always;
add_header X-Content-Type-Options nosniff always;
return 200 "try{if(!sessionStorage.getItem('tf-chunk-reload')){sessionStorage.setItem('tf-chunk-reload','1');location.reload();}else{sessionStorage.removeItem('tf-chunk-reload');}}catch(e){location.reload();}";
}
# index.html must never be cached, or a redeploy keeps serving the old asset hashes.
location = /index.html {
etag off;
if_modified_since off;
add_header Cache-Control "no-store, no-cache, must-revalidate, max-age=0" always;
add_header Pragma "no-cache" always;
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options DENY always;
add_header Referrer-Policy strict-origin-when-cross-origin always;
}
gzip on;
gzip_min_length 1024;
gzip_types text/css text/javascript application/javascript application/json image/svg+xml;
}