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
+14
View File
@@ -301,3 +301,17 @@ td.r, th.r { text-align: right; }
.nm input { font: inherit; color: var(--fg); background: var(--panel2); border: 1px solid var(--line); border-radius: 8px; padding: 5px 9px; width: 150px; }
.card.locked { opacity: .55; }
.sec-head .btn { padding: 3px 12px; font-size: 13px; }
/* ---------- Аккаунт ---------- */
.authbox { max-width: 520px; display: grid; gap: 16px; }
.authbox .form { max-width: none; }
.text {
width: 100%; font: inherit; color: var(--fg); background: var(--panel2); border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px;
}
.text:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.msg { padding: 10px 14px; border-radius: 10px; font-weight: 550; border: 1px solid transparent; }
.msg.ok { background: color-mix(in srgb, var(--ok) 16%, transparent); border-color: var(--ok); }
.msg.err { background: color-mix(in srgb, var(--bad) 16%, transparent); border-color: var(--bad); }
.linkbtn { border: 0; background: none; padding: 0; color: var(--accent); font: inherit; font-weight: 600; cursor: pointer; text-decoration: underline; }
.linkbtn:hover { filter: brightness(1.15); }
.danger-zone { border-color: var(--bad); }