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
+10 -1
View File
@@ -10,6 +10,7 @@ set -euo pipefail
LISTEN="${LISTEN:-127.0.0.1:8080}" # адрес:порт или просто порт
DOMAIN="${DOMAIN:-_}" # server_name; "_" = любое имя
WEBROOT="${WEBROOT:-/var/www/split-trainer}"
API_UPSTREAM="${API_UPSTREAM:-http://127.0.0.1:8000}" # где слушает API аккаунтов (ставится deploy/install-api-debian.sh)
NAME="split-trainer"
die() { echo "Ошибка: $*" >&2; exit 1; }
@@ -26,6 +27,12 @@ CONF_TEMPLATE="$HERE/$NAME.conf"
[[ "$LISTEN" =~ ^[][0-9A-Za-z.:_-]+$ ]] || die "некорректный LISTEN: $LISTEN"
[[ "$DOMAIN" =~ ^[0-9A-Za-z._*-]+( [0-9A-Za-z._*-]+)*$ ]] || die "некорректный DOMAIN: $DOMAIN"
[[ "$WEBROOT" =~ ^/[0-9A-Za-z._/-]+$ ]] || die "некорректный WEBROOT: $WEBROOT"
[[ "$API_UPSTREAM" =~ ^http://(127\.0\.0\.1|\[::1\]):[0-9]{1,5}$ ]] || die "некорректный API_UPSTREAM (нужен http://127.0.0.1:порт или http://[::1]:порт, без имён: DNS здесь не работает): $API_UPSTREAM"
# Скрипт стирает старые файлы в WEBROOT: системные каталоги и каталоги верхнего уровня запрещаем
case "$WEBROOT" in
/|/etc|/etc/*|/usr|/usr/*|/bin|/bin/*|/sbin|/sbin/*|/lib*|/boot|/boot/*|/root|/root/*|/proc|/proc/*|/sys|/sys/*|/dev|/dev/*|/home|/var|/opt|/srv|/run|/run/*|/tmp)
die "WEBROOT не может быть системным каталогом: $WEBROOT (подойдёт, например, /var/www/split-trainer)";;
esac
. /etc/os-release
if [ "${ID:-}" != "debian" ] || { [ "${VERSION_ID:-}" != "12" ] && [ "${VERSION_ID:-}" != "13" ]; }; then
@@ -47,7 +54,9 @@ CONF="/etc/nginx/sites-available/$NAME.conf"
sed -e "s|^\(\s*\)listen 8080;.*|\1listen $LISTEN;|" \
-e "s|^\(\s*\)server_name _;.*|\1server_name $DOMAIN;|" \
-e "s|^\(\s*\)root /usr/share/nginx/html;.*|\1root $WEBROOT;|" \
"$CONF_TEMPLATE" > "$CONF"
-e "s|^\(\s*\)set \$api_upstream .*|\1set \$api_upstream $API_UPSTREAM;|" \
-e '/^\s*resolver 127\.0\.0\.11/d' \
"$CONF_TEMPLATE" > "$CONF" # DNS Docker на хосте не нужен: API по адресу 127.0.0.1
ln -sf "$CONF" "/etc/nginx/sites-enabled/$NAME.conf"
nginx -t