Files
split-trainer/public/index.html
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

39 lines
2.0 KiB
HTML

<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Сплит-тренер</title>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect x='2' y='8' width='12' height='16' rx='3' fill='%233b6cff'/%3E%3Crect x='18' y='8' width='12' height='16' rx='3' fill='%233b6cff'/%3E%3C/svg%3E">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<header class="top">
<div class="brand">
<svg width="30" height="30" viewBox="0 0 32 32" aria-hidden="true"><rect x="2" y="7" width="12" height="18" rx="3" fill="currentColor"/><rect x="18" y="7" width="12" height="18" rx="3" fill="currentColor"/><g fill="var(--panel)"><rect x="5" y="11" width="2.6" height="2.6" rx=".8"/><rect x="9" y="11" width="2.6" height="2.6" rx=".8"/><rect x="5" y="16" width="2.6" height="2.6" rx=".8"/><rect x="9" y="16" width="2.6" height="2.6" rx=".8"/><rect x="21" y="11" width="2.6" height="2.6" rx=".8"/><rect x="25" y="11" width="2.6" height="2.6" rx=".8"/><rect x="21" y="16" width="2.6" height="2.6" rx=".8"/><rect x="25" y="16" width="2.6" height="2.6" rx=".8"/></g></svg>
<span>Сплит-тренер</span>
</div>
<nav id="nav" aria-label="Разделы"></nav>
</header>
<main id="app">
<section class="view" id="v-lessons"></section>
<section class="view" id="v-practice"></section>
<section class="view" id="v-keyboard"></section>
<section class="view" id="v-account"></section>
<section class="view" id="v-train"></section>
<section class="view" id="v-result"></section>
<section class="view" id="v-stats"></section>
<section class="view" id="v-settings"></section>
</main>
<div id="toast" role="status" aria-live="polite"></div>
<script src="data.js"></script>
<script src="vial.js"></script>
<script src="sync.js"></script>
<script src="auth.js"></script>
<script src="app.js"></script>
</body>
</html>