server { listen 80 default_server; root /usr/share/nginx/html; index index.html; # Sprint 13 — security-заголовки (для SPA HTML; для API те же выставляются # уже SecurityHeadersMiddleware'ом на api-side). add_header с always # обеспечивает применение даже на 4xx/5xx (без always — только на 2xx/3xx). # CSP синхронен с SecurityHeadersOptions.DefaultCsp. add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self' wss: ws:; img-src 'self' data: blob:; font-src 'self' data:; frame-ancestors 'none'; base-uri 'self'; form-action 'self'" always; add_header X-Frame-Options "DENY" always; add_header X-Content-Type-Options "nosniff" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=()" always; add_header X-Permitted-Cross-Domain-Policies "none" always; # Sprint 28: HSTS. Brower honors HSTS only on HTTPS responses, поэтому # безопасно добавлять unconditionally — если клиент пришёл по HTTP, # header игнорируется. Без includeSubDomains и без preload — это # pre-emptive consent: можно безопасно убрать. Когда production stack # устаканится и admin.food-market.kz будет подан в hstspreload.org, # увеличить max-age до 31536000 + добавить preload и includeSubDomains. add_header Strict-Transport-Security "max-age=2592000" always; # Long-running admin imports (MoySklad etc.) read from upstream for tens of # minutes. Bump timeouts only on that path so normal API stays snappy. location /api/admin/import/ { proxy_pass http://api:8080; 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_read_timeout 60m; proxy_send_timeout 60m; proxy_request_buffering off; proxy_buffering off; } # API reverse-proxy — upstream name "api" resolves in the compose network. location /api/ { proxy_pass http://api:8080; 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 Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } location /connect/ { proxy_pass http://api:8080; proxy_http_version 1.1; proxy_set_header Host $host; } # SignalR хаб для live-уведомлений (см. NotificationsHub). # WebSocket требует upgrade-хедеры и большой read_timeout (иначе nginx # будет рвать idle-коннекшен каждые 60 сек). access_token приходит как # query (?access_token=...), Authorization-хедер middleware на API его # перекладывает в нужный вид до UseAuthentication. location /hubs/ { proxy_pass http://api:8080; 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 Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 86400; # 24h — webSocket долгоживущий proxy_send_timeout 86400; proxy_buffering off; } location /health { proxy_pass http://api:8080; } # Prometheus метрики API. Без этого блока запрос ловится SPA fallback'ом и # возвращает index.html (947 байт) вместо exposition format. На prod-домене # имеет смысл закрыть IP-фильтром (allow 192.168.0.0/16; deny all;), на # stage оставляем открытым — за gateway nginx уже есть auth/TLS-обвязка. location = /metrics { proxy_pass http://api:8080; 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; } # Swagger UI + OpenAPI-doc. На контейнере api подключается только когда # IncludeSwagger=true (env-флаг, см. Program.cs). На prod-домене флаг не # выставляем, /swagger вернёт 404 от api — это ожидаемо. location /swagger/ { proxy_pass http://api:8080; 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; } location = /swagger { return 301 /swagger/; } # Sprint 13: Hangfire Dashboard — внутренний инструмент мониторинга # фоновых джобов. Доступ только SuperAdmin'у (см. SuperAdminHangfireFilter # в API). Без этой location'и /hangfire ловился бы SPA-fallback'ом и # возвращал index.html — что выглядит как «всё ок», но дашборда нет. location /hangfire { proxy_pass http://api:8080; 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; } # Статика изображений товаров — api раздаёт /uploads/... из volume. location /uploads/ { proxy_pass http://api:8080; proxy_http_version 1.1; proxy_set_header Host $host; } # PWA: SW и manifest должны отдаваться с правильным content-type и без # кеша на самом ответе (внутри SW свой versioned cache). Иначе старый # SW залипает на клиенте и не подхватывает обновления. location = /sw.js { add_header Cache-Control "no-cache, no-store, must-revalidate"; add_header Pragma "no-cache"; expires off; try_files /sw.js =404; } location = /manifest.webmanifest { types { } default_type application/manifest+json; add_header Cache-Control "public, max-age=3600"; try_files /manifest.webmanifest =404; } location = /offline.html { try_files /offline.html =404; } # SPA fallback — all other routes return index.html location / { try_files $uri $uri/ /index.html; } }