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

74 lines
4.6 KiB
Bash
Executable File
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.
#!/usr/bin/env bash
# Установка и обновление Сплит-тренера на Debian 12/13: nginx + статические файлы.
#
# sudo ./deploy/install-debian.sh # за обратным прокси: слушаем 127.0.0.1:8080
# sudo LISTEN=80 DOMAIN=trainer.example.com ./deploy/install-debian.sh # без прокси, напрямую на порту 80
#
# Повторный запуск обновляет файлы сайта и конфиг. Ничего, кроме этого сайта, скрипт не трогает.
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; }
[ "$(id -u)" -eq 0 ] || die "запустите от root: sudo $0"
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SRC="$HERE/../public"
CONF_TEMPLATE="$HERE/$NAME.conf"
[ -f "$SRC/index.html" ] || die "не найден $SRC/index.html: запускайте скрипт из клонированного репозитория"
[ -f "$CONF_TEMPLATE" ] || die "не найден $CONF_TEMPLATE"
# Значения подставляются в sed: не пускаем туда ничего, кроме безопасных символов
[[ "$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
[ "${FORCE:-0}" = "1" ] || die "скрипт рассчитан на Debian 12/13 (у вас: ${PRETTY_NAME:-неизвестно}). Чтобы всё равно продолжить: FORCE=1"
fi
echo "==> Устанавливаю nginx"
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq --no-install-recommends nginx >/dev/null
echo "==> Копирую сайт в $WEBROOT"
install -d -m 755 "$WEBROOT"
find "$WEBROOT" -mindepth 1 -maxdepth 1 -type f -delete # убираем только файлы прошлой версии
install -m 644 "$SRC"/* "$WEBROOT"/
echo "==> Настраиваю nginx (listen $LISTEN, server_name $DOMAIN)"
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;|" \
-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
systemctl enable --now nginx >/dev/null 2>&1
systemctl reload nginx
# Проверяем, что сайт действительно отвечает
HOSTPORT="$LISTEN"; [[ "$HOSTPORT" =~ ^[0-9]+$ ]] && HOSTPORT="127.0.0.1:$HOSTPORT"
[ "${HOSTPORT%%:*}" = "0.0.0.0" ] && HOSTPORT="127.0.0.1:${HOSTPORT##*:}"
if command -v curl >/dev/null 2>&1 && ! curl -fsS -m 5 -o /dev/null "http://$HOSTPORT/healthz" -H "Host: ${DOMAIN%% *}"; then
echo "Предупреждение: проверка http://$HOSTPORT/healthz не прошла. Если порт 80 занят сайтом по умолчанию, см. docs/DEPLOY.md" >&2
fi
echo "==> Готово: http://$HOSTPORT/"