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.
141 lines
6.1 KiB
Bash
Executable File
141 lines
6.1 KiB
Bash
Executable File
#!/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
|