Files
split-trainer/deploy/install-api-debian.sh
T
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

141 lines
6.1 KiB
Bash
Executable File
Raw 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
# Установка API аккаунтов на Debian 12/13 без Docker: python3 из пакетов + служба systemd.
# Сначала поставьте сайт: sudo ./deploy/install-debian.sh
#
# sudo PUBLIC_URL=https://trainer.example.com \
# SMTP_HOST=smtp.example.com SMTP_USER=noreply@example.com SMTP_PASSWORD='...' \
# ./deploy/install-api-debian.sh
#
# Первый запуск создаёт /etc/split-trainer/api.env из переданных переменных. Повторный запуск обновляет код и
# перезапускает службу, а файл настроек не трогает (меняйте его вручную и перезапускайте: systemctl restart split-trainer-api).
# Пока SMTP не настроен, а сайт публичный, API не стартует (так задумано): укажите SMTP_HOST или MAIL_BACKEND=console для проверки.
set -euo pipefail
APP_DIR="/opt/split-trainer/api"
ENV_DIR="/etc/split-trainer"
ENV_FILE="$ENV_DIR/api.env"
UNIT="/etc/systemd/system/split-trainer-api.service"
API_PORT="${API_PORT:-8000}"
die() { echo "Ошибка: $*" >&2; exit 1; }
[ "$(id -u)" -eq 0 ] || die "запустите от root: sudo $0"
[[ "$API_PORT" =~ ^[0-9]{1,5}$ ]] || die "некорректный API_PORT: $API_PORT"
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SRC="$HERE/../server/splitapi"
[ -f "$SRC/__main__.py" ] || die "не найден $SRC: запускайте скрипт из клонированного репозитория"
. /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 "==> Проверяю python3"
export DEBIAN_FRONTEND=noninteractive
command -v python3 >/dev/null 2>&1 || { apt-get update -qq; apt-get install -y -qq --no-install-recommends python3 >/dev/null; }
command -v curl >/dev/null 2>&1 || { apt-get update -qq; apt-get install -y -qq --no-install-recommends curl >/dev/null; } # для проверки в конце
python3 -c 'import sys; sys.exit(0 if sys.version_info >= (3, 11) else 1)' || die "нужен Python 3.11 или новее"
python3 -c 'import hashlib, sqlite3; hashlib.scrypt' >/dev/null 2>&1 || die "в python3 нет sqlite3 или scrypt (установите пакет python3 целиком)"
echo "==> Копирую код в $APP_DIR"
install -d -m 755 "$APP_DIR"
rm -rf "$APP_DIR/splitapi" # наш собственный каталог: заменяем целиком
cp -r "$SRC" "$APP_DIR/splitapi"
find "$APP_DIR/splitapi" -name '__pycache__' -type d -prune -exec rm -rf {} +
chown -R root:root "$APP_DIR"
chmod -R go-w "$APP_DIR"
# Значение переменной в формате EnvironmentFile systemd: в двойных кавычках, \ и " экранируются, перевод строки недопустим
env_line() {
local name="$1" value="$2"
[[ "$value" != *$'\n'* && "$value" != *$'\r'* ]] || die "в значении $name не должно быть перевода строки"
printf '%s="%s"\n' "$name" "$(printf '%s' "$value" | sed -e 's/[\\"]/\\&/g')"
}
if [ -f "$ENV_FILE" ]; then
echo "==> $ENV_FILE уже есть: не меняю"
else
[ -n "${PUBLIC_URL:-}" ] || die "задайте PUBLIC_URL (внешний адрес сайта), например PUBLIC_URL=https://trainer.example.com"
echo "==> Создаю $ENV_FILE (права 0600: в нём может быть пароль почты)"
install -d -m 755 "$ENV_DIR"
umask 077
{
env_line PUBLIC_URL "$PUBLIC_URL"
env_line BIND "127.0.0.1"
env_line PORT "$API_PORT"
env_line TRUST_PROXY "1"
env_line DB_PATH "/var/lib/split-trainer/splitapi.sqlite3"
for name in MAIL_BACKEND MAIL_FROM SMTP_HOST SMTP_PORT SMTP_SECURITY SMTP_USER SMTP_PASSWORD ALLOW_REGISTRATION SESSION_DAYS; do
if [ -n "${!name:-}" ]; then env_line "$name" "${!name}"; fi
done
} > "$ENV_FILE"
chmod 600 "$ENV_FILE"
chown root:root "$ENV_FILE"
fi
echo "==> Настраиваю службу systemd"
cat > "$UNIT" <<'UNIT_EOF'
[Unit]
Description=Сплит-тренер: API аккаунтов
After=network.target
[Service]
Type=simple
WorkingDirectory=/opt/split-trainer/api
EnvironmentFile=/etc/split-trainer/api.env
Environment=PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1
ExecStart=/usr/bin/python3 -m splitapi
Restart=on-failure
RestartSec=3
# Без постоянного пользователя и почти без прав
DynamicUser=yes
StateDirectory=split-trainer
StateDirectoryMode=0700
NoNewPrivileges=yes
CapabilityBoundingSet=
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
ProtectClock=yes
ProtectHostname=yes
ProtectProc=invisible
ProcSubset=pid
RestrictNamespaces=yes
RestrictSUIDSGID=yes
LockPersonality=yes
UMask=0077
SystemCallArchitectures=native
SystemCallFilter=@system-service
MemoryMax=384M
TasksMax=64
[Install]
WantedBy=multi-user.target
UNIT_EOF
chmod 644 "$UNIT"
systemctl daemon-reload
systemctl enable split-trainer-api >/dev/null 2>&1
systemctl restart split-trainer-api
echo "==> Проверяю, что API отвечает"
PORT_IN_USE="$(sed -n 's/^PORT="\([0-9]*\)"$/\1/p' "$ENV_FILE" | head -n1)"
PORT_IN_USE="${PORT_IN_USE:-$API_PORT}"
for _ in $(seq 1 20); do
if curl -fsS -m 3 -o /dev/null "http://127.0.0.1:$PORT_IN_USE/api/health" 2>/dev/null; then
echo "==> Готово: API слушает 127.0.0.1:$PORT_IN_USE. Сайт обращается к нему через nginx (install-debian.sh, API_UPSTREAM)."
exit 0
fi
sleep 1
done
echo "API не ответил за 20 секунд. Последние строки журнала:" >&2
journalctl -u split-trainer-api -n 20 --no-pager >&2 || true
exit 1