Files
darko fcafe1ae15 feat: аккаунты по почте с подтверждением, сбросом пароля и синхронизацией прогресса
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.
2026-10-16 11:25:09 +03:00

79 lines
4.3 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Сплит-тренер: статический сайт и прокси к 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;
}
}