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.
This commit is contained in:
darko
2026-10-16 11:25:09 +03:00
parent e897a6c5eb
commit fcafe1ae15
36 changed files with 4520 additions and 22 deletions
+22 -2
View File
@@ -1,7 +1,10 @@
# Сплит-тренер: статический сайт. Один и тот же файл годится и для контейнера, и для nginx на хосте.
# Скрипт deploy/install-debian.sh заменяет ровно три строки с пометкой "# <-": listen, server_name и root.
# Сплит-тренер: статический сайт и прокси к 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 _; # <- имя сайта
@@ -29,6 +32,23 @@ server {
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;