API аккаунтов на чистой стандартной библиотеке Python (server/splitapi): - регистрация по почте: пароль задаётся по ссылке из письма, аккаунт создаётся только после подтверждения - вход, выход, сброс пароля по почте (токены одноразовые, в базе только SHA-256), удаление аккаунта - пароли scrypt, лимиты попыток по паре «адрес + IP», IP и аккаунту, защита от CSRF - хранение прогресса пользователя с оптимистичной блокировкой версии - SMTP через переменные окружения; без SMTP на публичном адресе API не стартует Клиент: public/auth.js (вход, регистрация, сброс, синхронизация), public/sync.js (чистое слияние прогресса). Без API сайт работает как раньше. Развёртывание: Dockerfile.api, сервис splitapi в docker-compose, проксирование /api/ в nginx с ограничением частоты, deploy/install-api-debian.sh (systemd), .env.example. Тесты: 216 для бэкенда (unittest, покрытие около 97%), 16 для клиентского слияния. Документация: раздел «Аккаунты и почта» в docs/DEPLOY.md, раздел 9 в docs/ИНСТРУКЦИЯ.md.
79 lines
4.3 KiB
Plaintext
79 lines
4.3 KiB
Plaintext
# Сплит-тренер: статический сайт и прокси к API аккаунтов. Один и тот же файл годится и для контейнера, и для nginx на хосте.
|
||
# Скрипт deploy/install-debian.sh заменяет строки с пометкой "# <-": listen, server_name, root и адрес API.
|
||
# Совместимо с nginx 1.22 (Debian 12) и 1.26 (Debian 13).
|
||
|
||
# Грубая защита API от потока запросов (точные лимиты на вход, регистрацию и сброс пароля считает сам API)
|
||
limit_req_zone $binary_remote_addr zone=splitapi_zone:10m rate=10r/s;
|
||
|
||
server {
|
||
listen 8080; # <- порт или адрес:порт
|
||
server_name _; # <- имя сайта
|
||
root /usr/share/nginx/html; # <- каталог с файлами
|
||
index index.html;
|
||
|
||
server_tokens off;
|
||
|
||
# За обратным прокси редиректы не должны подставлять внутренний хост и порт
|
||
absolute_redirect off;
|
||
port_in_redirect off;
|
||
|
||
# Настоящий адрес клиента (для логов) берём из X-Forwarded-For, но только от прокси из частных сетей и localhost
|
||
set_real_ip_from 127.0.0.1;
|
||
set_real_ip_from ::1;
|
||
set_real_ip_from 10.0.0.0/8;
|
||
set_real_ip_from 172.16.0.0/12;
|
||
set_real_ip_from 192.168.0.0/16;
|
||
set_real_ip_from fc00::/7;
|
||
real_ip_header X-Forwarded-For;
|
||
real_ip_recursive on;
|
||
|
||
gzip on;
|
||
gzip_vary on;
|
||
gzip_min_length 512;
|
||
gzip_types text/css application/javascript text/javascript text/plain application/json image/svg+xml;
|
||
|
||
# API аккаунтов (отдельный процесс). Если его нет, ответ 502: сайт это замечает и просто не показывает вход.
|
||
# Адрес в переменной и resolver (DNS Docker): так nginx стартует, даже если контейнер API ещё не поднялся.
|
||
resolver 127.0.0.11 valid=10s ipv6=off;
|
||
location /api/ {
|
||
set $api_upstream http://splitapi:8000; # <- адрес API
|
||
proxy_pass $api_upstream;
|
||
limit_req zone=splitapi_zone burst=20 nodelay;
|
||
limit_req_status 429;
|
||
client_max_body_size 300k;
|
||
proxy_connect_timeout 3s;
|
||
proxy_read_timeout 20s;
|
||
proxy_set_header Host $host;
|
||
# $remote_addr здесь уже настоящий клиент (real_ip выше); API верит этому заголовку, только если порт API недоступен снаружи
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
}
|
||
|
||
# Проверка живости для мониторинга и healthcheck контейнера
|
||
location = /healthz {
|
||
access_log off;
|
||
default_type text/plain;
|
||
return 200 "ok\n";
|
||
}
|
||
|
||
# Все заголовки безопасности заданы в одном location: add_header не наследуется между уровнями.
|
||
# no-cache не запрещает кеш, а заставляет браузер сверять версию (ETag) при каждом открытии:
|
||
# после обновления никто не застрянет на старом app.js.
|
||
location / {
|
||
try_files $uri $uri/ =404;
|
||
|
||
add_header Cache-Control "no-cache" always;
|
||
add_header X-Content-Type-Options "nosniff" always;
|
||
add_header Referrer-Policy "no-referrer" always;
|
||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always;
|
||
add_header Content-Security-Policy "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'self'" always;
|
||
# HSTS не ставим здесь: HTTPS завершает внешний прокси, он и должен отдавать Strict-Transport-Security.
|
||
}
|
||
|
||
# Скрытые файлы (.git, .env и т. п.) не отдаём никогда
|
||
location ~ /\. {
|
||
return 404;
|
||
}
|
||
}
|