initial site
This commit is contained in:
commit
da5b259d06
167 files changed
+34157
No files matched your search
+140
@@ -0,0 +1,140 @@
|
||||
# Брауз-тормоз на флуд заказами: с одного IP не чаще 10 заказов в минуту
|
||||
# (burst 10 сглаживает «двойной клик»). Работает ДО запроса в CRM.
|
||||
limit_req_zone $binary_remote_addr zone=orders_zone:1m rate=10r/m;
|
||||
|
||||
# Общий тормоз на прочее /api/ (NF-04): флуд catalog/stoplist выедал пул БД
|
||||
# CRM. Легитимный сайт опрашивает эти эндпоинты раз в минуту-десять,
|
||||
# 120r/m с burst 60 — с большим запасом. Панель персонала ходит на CRM
|
||||
# напрямую (127.0.0.1:47613) и этот лимит не замечает.
|
||||
limit_req_zone $binary_remote_addr zone=api_zone:1m rate=120r/m;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Реальный IP гостя для лимитеров CRM. Доверие к X-Forwarded-For —
|
||||
# только 127.0.0.1 и docker-подсеть compose (NF-10): раньше верили всем
|
||||
# RFC1918-пирам, и LAN-клиент при экспоузе сайта мог вращать per-IP
|
||||
# лимитеры подделкой заголовка. Подсеть зафиксирована в
|
||||
# docker-compose.yml (networks.default.ipam) — при смене её там
|
||||
# обновите и здесь.
|
||||
set_real_ip_from 127.0.0.1;
|
||||
set_real_ip_from 172.18.0.0/16;
|
||||
real_ip_header X-Forwarded-For;
|
||||
real_ip_recursive on;
|
||||
|
||||
# single-file сборка (~750 KB html) без gzip = лишний трафик гостям
|
||||
gzip on;
|
||||
gzip_comp_level 5;
|
||||
gzip_min_length 1024;
|
||||
gzip_vary on;
|
||||
gzip_types text/css application/javascript application/json image/svg+xml application/xml text/plain;
|
||||
|
||||
server_tokens off;
|
||||
|
||||
# Базовые заголовки безопасности. Внимание nginx: location со своим
|
||||
# add_header (например /media/) НЕ наследует их — там заголовки повторены.
|
||||
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;
|
||||
# CSP: бандл инлайнен в single-file index.html (поэтому 'unsafe-inline' у
|
||||
# script-src), плюс Метрика/GA и Google Fonts. object/base/frame-ancestors
|
||||
# наглухо закрыты — внедрённый в страницу тег не подгрузит и не отправит.
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://mc.yandex.ru https://yastatic.net https://www.googletagmanager.com https://www.google-analytics.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' data: https://fonts.gstatic.com; img-src 'self' data: https://mc.yandex.ru https://www.google-analytics.com https://www.googletagmanager.com; connect-src 'self' https://mc.yandex.ru https://www.google-analytics.com https://analytics.google.com; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'" always;
|
||||
|
||||
# Заказы и стоп-лист живут в CRM-контейнере (см. docker-compose.yml).
|
||||
# Точный location = /api/orders ниже переопределяет этот для POST заказов
|
||||
# со своей зоной (приоритет nginx: точный бьёт префиксный).
|
||||
location /api/ {
|
||||
limit_req zone=api_zone burst=60 nodelay;
|
||||
limit_req_status 429;
|
||||
proxy_pass http://crm:47613;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_connect_timeout 5s;
|
||||
proxy_read_timeout 15s;
|
||||
}
|
||||
|
||||
# Точка создания заказа — отдельный тормоз поверх лимитеров CRM
|
||||
# (nginx отвечает 429 раньше, чем запрос дойдёт до Python).
|
||||
location = /api/orders {
|
||||
limit_req zone=orders_zone burst=10 nodelay;
|
||||
limit_req_status 429;
|
||||
proxy_pass http://crm:47613;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_connect_timeout 5s;
|
||||
proxy_read_timeout 15s;
|
||||
}
|
||||
|
||||
# Входная точка SPA: ТОЛЬКО no-cache — в single-file сборке внутри
|
||||
# index.html инлайнен весь бандл, закэшировать его на 7 дней = гости
|
||||
# не увидят обновлений сайта до конца срока кэша. (Точное совпадение
|
||||
# бьёт регэксп-локейшен расширений ниже; прочие .html честно 404.)
|
||||
location = /index.html {
|
||||
add_header Cache-Control "no-cache";
|
||||
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;
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://mc.yandex.ru https://yastatic.net https://www.googletagmanager.com https://www.google-analytics.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' data: https://fonts.gstatic.com; img-src 'self' data: https://mc.yandex.ru https://www.google-analytics.com https://www.googletagmanager.com; connect-src 'self' https://mc.yandex.ru https://www.google-analytics.com https://analytics.google.com; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'" always;
|
||||
}
|
||||
|
||||
# SEO-файлы: точный Content-Type и кэш не больше часа
|
||||
# (types {} сбрасывает mime.types — иначе .xml ушёл бы как text/xml)
|
||||
location = /robots.txt {
|
||||
types { }
|
||||
default_type text/plain;
|
||||
expires 1h;
|
||||
}
|
||||
location = /sitemap.xml {
|
||||
types { }
|
||||
default_type application/xml;
|
||||
expires 1h;
|
||||
}
|
||||
|
||||
# Загруженные владельцем фото позиций — из CRM (/media/<id>.<ext>).
|
||||
# ^~ обязателен: без него регэксп-локейшен расширений (*.jpg, *.avif…)
|
||||
# перехватывал бы запросы, и все фото отдавали 404 вместо прокси в CRM.
|
||||
location ^~ /media/ {
|
||||
proxy_pass http://crm:47613;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
expires 7d;
|
||||
add_header Cache-Control "public, max-age=604800";
|
||||
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;
|
||||
add_header Content-Security-Policy "default-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'" always;
|
||||
}
|
||||
|
||||
# Vite SPA fallback: client-side routes should resolve to index.html.
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location = /healthz {
|
||||
access_log off;
|
||||
add_header Content-Type text/plain;
|
||||
return 200 'ok\n';
|
||||
}
|
||||
|
||||
# Файлы с расширениями отдаются только с диска: несуществующие — честный
|
||||
# 404, а не index.html (soft-404 вреден для поисковиков). SPA-fallback
|
||||
# выше работает только для путей без расширения.
|
||||
location ~* \.(?:css|js|mjs|cjs|json|map|webmanifest|txt|xml|svg|ico|png|jpg|jpeg|gif|webp|avif|jfif|woff2?|ttf|otf|eot|mp4|webm|mp3|wav|ogg|pdf|html)$ {
|
||||
expires 7d;
|
||||
add_header Cache-Control "public, max-age=604800";
|
||||
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;
|
||||
add_header Content-Security-Policy "default-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'" always;
|
||||
try_files $uri =404;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user