Вход по логину и паролю стал основным во всех окружениях, Telegram остаётся вторым способом. Логин — это ник игрока.
Stub в прод не переносился. Он пускает по нику без секрета, поэтому вход по паролю написан отдельным прод-модулем backend/app/auth/password.py, а stub остаётся только в dev.
Миграции нет.users.password_hash и провайдер local есть в схеме с 0001, на них построен вход админа.
Бэкенд
POST /api/auth/register и POST /api/auth/login.
PUT /api/users/me/password: первый раз без текущего пароля, дальше — с текущим.
POST /api/users/me/telegram: привязка Telegram, ник не меняется.
PUT /api/admin/users/{id}/password: админ задаёт пароль игроку, так восстанавливается забытый.
В MeRead и AdminUserRead новое поле has_password.
Фронт
Страница входа: «Вход» и «Регистрация» по паролю, ниже Telegram.
Обязательное окно «Задайте пароль» для всех без пароля; закрыть нельзя, можно выйти.
В профиле смена пароля и привязка Telegram.
В админке кнопка «Задать пароль» и пометка «без пароля».
Сценарии из задачи:
Регистрация через Telegram → окно просит пароль → дальше вход по нику и паролю.
Регистрация по паролю → привязка Telegram в профиле → вход через Telegram в тот же аккаунт.
Существующие аккаунты при заходе видят то же обязательное окно.
Что ломается и как переходим (Compat/Breaking)
Данные: миграции нет. Аккаунты, ники, партии и вход через Telegram не меняются.
Пользователи: каждый без пароля при первом заходе после выкладки должен задать пароль, иначе дальше окна не пройти. Вход через Telegram продолжает работать.
Логин = ник: переименование, в том числе админом, меняет логин игрока. В профиле об этом подсказка.
API: изменения только добавляются. /auth/config.methods теперь ["password", "telegram"]. index.html отдаётся с no-store, так что браузер сразу получит новый бандл.
Откат: старый код password_hash игроков не читает (вход админа фильтрует role='admin'), откат образа безопасен.
Модель угроз и что проверено
Угроза
Защита
Проверено
Перебор паролей
окно 15 мин в памяти процесса: 5 неудач на «IP + логин», 20 на IP → 429; пока блок стоит, пароль не проверяется; bcrypt; от 8 символов
тесты, в т.ч. истечение окна; curl: 5×401, затем 429 даже с верным паролем
Перебор логинов
неизвестный логин и аккаунт без пароля сверяются с фиктивным хешем, ответ одинаковый
тест сравнивает тела ответов
Учётка админа как игрок и наоборот
игрок ищется только с role='player'; разные cookie и audience
тесты в обе стороны
Подбор текущего пароля на чужой открытой сессии
смена требует текущий пароль, 5 неудач на аккаунт → 429
тесты
Привязка чужого Telegram
HMAC виджета, identity и telegram_id не должны быть заняты
тело только JSON: форма text/plain не создаёт сессию, cookie SameSite=Lax
curl: сессия не создаётся, но ответ 500, а не 422 — см. #52
Утечка пароля
bcrypt; в аудит пишется только факт смены; тело API режется на 128 символах
тест аудита
Длинные пароли и bcrypt 5
новый пароль не длиннее 72 байт; на входе длинный пароль даёт False без исключения
тесты
Окно «Задайте пароль» — ограничение интерфейса, а не сервера. Аккаунт без пароля по-прежнему может работать через API с сессией из Telegram. Это сделано намеренно: вход через Telegram остаётся полноценным.
Найденная попутно давняя ошибка — ответ 500 вместо 422 на тело не в JSON, плюс эхо тела в details ответа 422 — вынесена в #52.
Коммиты
82cf7a3 Вход и регистрация по логину и паролю
c73b451 Пароль и привязка Telegram в профиле, пароль игрока из админки
462079d Фронт: вход по паролю, окно установки пароля, профиль и админка
a09766c README: вход по логину и паролю
Проверки
.\.venv\Scripts\python.exe -m pytest — 128 passed. Из них 26 новых в tests/test_password_auth.py, ещё в test_auth.py обновлены методы входа по окружениям.
npm run gen:api с живого бэкенда — только добавления в schema.d.ts.
npm run build (tsc + vite) — зелёный. В прод-бандле нет строк «Позже (dev)», «Тестовый вход», auth/dev/login.
Живой бэкенд в dev на копии dev-базы, curl:
регистрация → has_password: true;
выход → вход 200;
5 неверных паролей → 429 retry_after≈900;
неизвестный логин → 401 INVALID_CREDENTIALS;
stub-игрок → has_password: false.
Не проверено:
вёрстка в браузере — расширение Chrome не было подключено;
ручной прогон форм — вводить пароли в браузер автоматически я не могу;
Действия аудита записаны существующими create и update с пояснением в payload, а не новыми именами: фильтр журнала в админке знает только login/create/update/delete.
Неверный текущий пароль отвечает 403 WRONG_CURRENT_PASSWORD, а не 401, чтобы не путать его с истёкшей сессией.
## Что сделано
Вход по логину и паролю стал основным во всех окружениях, Telegram остаётся вторым способом. Логин — это ник игрока.
- **Stub в прод не переносился.** Он пускает по нику без секрета, поэтому вход по паролю написан отдельным прод-модулем `backend/app/auth/password.py`, а stub остаётся только в dev.
- **Миграции нет.** `users.password_hash` и провайдер `local` есть в схеме с `0001`, на них построен вход админа.
**Бэкенд**
- `POST /api/auth/register` и `POST /api/auth/login`.
- `PUT /api/users/me/password`: первый раз без текущего пароля, дальше — с текущим.
- `POST /api/users/me/telegram`: привязка Telegram, ник не меняется.
- `PUT /api/admin/users/{id}/password`: админ задаёт пароль игроку, так восстанавливается забытый.
- В `MeRead` и `AdminUserRead` новое поле `has_password`.
**Фронт**
- Страница входа: «Вход» и «Регистрация» по паролю, ниже Telegram.
- Обязательное окно «Задайте пароль» для всех без пароля; закрыть нельзя, можно выйти.
- В профиле смена пароля и привязка Telegram.
- В админке кнопка «Задать пароль» и пометка «без пароля».
Сценарии из задачи:
1. Регистрация через Telegram → окно просит пароль → дальше вход по нику и паролю.
2. Регистрация по паролю → привязка Telegram в профиле → вход через Telegram в тот же аккаунт.
3. Существующие аккаунты при заходе видят то же обязательное окно.
## Что ломается и как переходим (Compat/Breaking)
- **Данные:** миграции нет. Аккаунты, ники, партии и вход через Telegram не меняются.
- **Пользователи:** каждый без пароля при первом заходе после выкладки должен задать пароль, иначе дальше окна не пройти. Вход через Telegram продолжает работать.
- **Логин = ник:** переименование, в том числе админом, меняет логин игрока. В профиле об этом подсказка.
- **API:** изменения только добавляются. `/auth/config.methods` теперь `["password", "telegram"]`. `index.html` отдаётся с `no-store`, так что браузер сразу получит новый бандл.
- **Откат:** старый код `password_hash` игроков не читает (вход админа фильтрует `role='admin'`), откат образа безопасен.
## Модель угроз и что проверено
| Угроза | Защита | Проверено |
|---|---|---|
| Перебор паролей | окно 15 мин в памяти процесса: 5 неудач на «IP + логин», 20 на IP → 429; пока блок стоит, пароль не проверяется; bcrypt; от 8 символов | тесты, в т.ч. истечение окна; `curl`: 5×401, затем 429 даже с верным паролем |
| Перебор логинов | неизвестный логин и аккаунт без пароля сверяются с фиктивным хешем, ответ одинаковый | тест сравнивает тела ответов |
| Учётка админа как игрок и наоборот | игрок ищется только с `role='player'`; разные cookie и audience | тесты в обе стороны |
| Подбор текущего пароля на чужой открытой сессии | смена требует текущий пароль, 5 неудач на аккаунт → 429 | тесты |
| Привязка чужого Telegram | HMAC виджета, identity и `telegram_id` не должны быть заняты | тесты: подделанная подпись, занятый Telegram, повторная привязка |
| Login CSRF через HTML-форму | тело только JSON: форма `text/plain` не создаёт сессию, cookie `SameSite=Lax` | `curl`: сессия не создаётся, но ответ 500, а не 422 — см. #52 |
| Утечка пароля | bcrypt; в аудит пишется только факт смены; тело API режется на 128 символах | тест аудита |
| Длинные пароли и bcrypt 5 | новый пароль не длиннее 72 байт; на входе длинный пароль даёт `False` без исключения | тесты |
Окно «Задайте пароль» — ограничение интерфейса, а не сервера. Аккаунт без пароля по-прежнему может работать через API с сессией из Telegram. Это сделано намеренно: вход через Telegram остаётся полноценным.
**Вне рамок, для пен-теста #25:**
- отзыв ранее выданных JWT после смены пароля;
- throttle на входе админа;
- распределённый перебор с многих IP;
- лимит на регистрацию;
- 2FA.
Найденная попутно давняя ошибка — ответ 500 вместо 422 на тело не в JSON, плюс эхо тела в `details` ответа 422 — вынесена в #52.
## Коммиты
- `82cf7a3` Вход и регистрация по логину и паролю
- `c73b451` Пароль и привязка Telegram в профиле, пароль игрока из админки
- `462079d` Фронт: вход по паролю, окно установки пароля, профиль и админка
- `a09766c` README: вход по логину и паролю
## Проверки
- `.\.venv\Scripts\python.exe -m pytest` — **128 passed**. Из них 26 новых в `tests/test_password_auth.py`, ещё в `test_auth.py` обновлены методы входа по окружениям.
- `npm run gen:api` с живого бэкенда — только добавления в `schema.d.ts`.
- `npm run build` (tsc + vite) — зелёный. В прод-бандле нет строк «Позже (dev)», «Тестовый вход», `auth/dev/login`.
- Живой бэкенд в dev на копии dev-базы, `curl`:
- регистрация → `has_password: true`;
- выход → вход 200;
- 5 неверных паролей → 429 `retry_after≈900`;
- неизвестный логин → 401 `INVALID_CREDENTIALS`;
- stub-игрок → `has_password: false`.
- **Не проверено:**
- вёрстка в браузере — расширение Chrome не было подключено;
- ручной прогон форм — вводить пароли в браузер автоматически я не могу;
- привязка Telegram живьём — виджету нужен HTTPS-домен, логика покрыта тестами.
## Отклонения от плана
- Действия аудита записаны существующими `create` и `update` с пояснением в payload, а не новыми именами: фильтр журнала в админке знает только `login/create/update/delete`.
- Неверный текущий пароль отвечает 403 `WRONG_CURRENT_PASSWORD`, а не 401, чтобы не путать его с истёкшей сессией.
- Скриншоты вёрстки не сняты (см. «Не проверено»).
Closes #24
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01XfTsytzT6TojfmprRDKiV6
Игроки входили только через Telegram, а вход по нику без пароля (stub) есть лишь
в dev. Теперь основной вход во всех окружениях: POST /api/auth/register и
POST /api/auth/login, логин — это ник. Stub в прод не переносится: он пускает без
секрета и по-прежнему живёт только в dev. Новый код лежит в прод-модуле
auth/password.py и dev-модули не импортирует. Схема БД не меняется: колонка
password_hash и провайдер local есть с первой миграции, на них построен вход
админа.
Пароль от 8 символов и не длиннее 72 байт: дальше bcrypt 5 бросает ValueError.
Схема API режет тело длиннее 128 символов ещё до bcrypt. Игроком входит только
role='player', так что учётка админа не открывает сессию игрока, и наоборот.
Неизвестный логин и аккаунт без пароля сверяются с фиктивным хешем и получают ту
же 401 INVALID_CREDENTIALS: по ответу и его времени нельзя понять, есть ли логин.
От перебора — скользящее окно 15 минут в памяти процесса (рассчитано на один
воркер, как SSE-шина): 5 неудач на пару «IP + логин» и 20 на IP, дальше 429
TOO_MANY_ATTEMPTS с retry_after. Пока блок стоит, пароль не проверяется вовсе.
Успешный вход сбрасывает счётчик пары, но не IP.
В MeRead появилось has_password: по нему фронт попросит задать пароль тех, у
кого его нет. Метод password добавлен в /auth/config.
#24
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XfTsytzT6TojfmprRDKiV6
PUT /api/users/me/password задаёт или меняет пароль. Первый раз текущий пароль
не нужен: так его задают аккаунты из Telegram и все, кто появился до паролей.
Если пароль уже есть, нужен текущий. Иначе оставленная открытой сессия позволила
бы отобрать аккаунт насовсем, поэтому подбор текущего тоже ограничен: 5 неудач
на аккаунт за 15 минут. Ошибка 403 WRONG_CURRENT_PASSWORD, а не 401, чтобы фронт
не принял её за истёкшую сессию.
POST /api/users/me/telegram привязывает Telegram к аккаунту, созданному по
паролю. Подпись виджета проверяется так же, как при входе, ник не меняется.
Связка пишется в auth_identity, как при регистрации через Telegram, поэтому
следующий вход через Telegram попадает в этот аккаунт. Telegram, привязанный к
другому аккаунту, даёт 409 TELEGRAM_TAKEN, повторная привязка — 409
TELEGRAM_ALREADY_LINKED.
PUT /api/admin/users/{id}/password — способ восстановить забытый пароль: почту
приложение не хранит. Работает только для игроков, пароль админа по-прежнему
задаётся в .env. В аудит пишется только факт смены, без пароля. Сборка
AdminUserRead вынесена в хелпер, в ответе появилось has_password.
#24
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XfTsytzT6TojfmprRDKiV6
Страница входа начинается с карточки «Вход» (ник и пароль), в ней же
переключатель на регистрацию с повтором пароля. Telegram стоит ниже вторым
способом. Под ним подсказка: аккаунт, созданный по паролю, сначала привязывают к
Telegram в профиле, иначе вход через Telegram заведёт новый аккаунт. Поле логина
без автозаглавной буквы и автоисправления, у полей правильный autocomplete для
менеджеров паролей.
SetPasswordDialog показывается в AppShell всем, у кого has_password=false:
аккаунтам из Telegram и созданным до паролей. Закрыть окно нельзя, только задать
пароль или выйти. Успешный ответ кладёт в кэш профиль с has_password=true, и окно
исчезает само. Кнопка «Позже (dev)» есть лишь под import.meta.env.DEV: в
прод-бандле её строки нет, это проверено по dist.
В профиле появилась карточка «Вход в аккаунт»: смена пароля (текущий, если он
задан, новый и повтор) и привязка Telegram тем же виджетом, что при входе. У
смены ника подсказка, что ник — это и логин. В админке у игрока появилась кнопка
«Задать пароль» с вводом в той же строке, как у переименования, а в строке
игрока — пометка «без пароля».
schema.d.ts перегенерирован с живого бэкенда.
#24
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XfTsytzT6TojfmprRDKiV6
В таблицах методов входа (раздел «Аутентификация» и сводка окружений) пароль
теперь основной способ во всех окружениях, Telegram — второй, stub остаётся
только в dev. Описаны правила пароля, защита от перебора, обязательное окно для
аккаунтов без пароля, привязка Telegram и восстановление пароля через админку.
Поправлены места, где test-клон описывался как «только Telegram».
#24
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XfTsytzT6TojfmprRDKiV6
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Что сделано
Вход по логину и паролю стал основным во всех окружениях, Telegram остаётся вторым способом. Логин — это ник игрока.
backend/app/auth/password.py, а stub остаётся только в dev.users.password_hashи провайдерlocalесть в схеме с0001, на них построен вход админа.Бэкенд
POST /api/auth/registerиPOST /api/auth/login.PUT /api/users/me/password: первый раз без текущего пароля, дальше — с текущим.POST /api/users/me/telegram: привязка Telegram, ник не меняется.PUT /api/admin/users/{id}/password: админ задаёт пароль игроку, так восстанавливается забытый.MeReadиAdminUserReadновое полеhas_password.Фронт
Сценарии из задачи:
Что ломается и как переходим (Compat/Breaking)
/auth/config.methodsтеперь["password", "telegram"].index.htmlотдаётся сno-store, так что браузер сразу получит новый бандл.password_hashигроков не читает (вход админа фильтруетrole='admin'), откат образа безопасен.Модель угроз и что проверено
curl: 5×401, затем 429 даже с верным паролемrole='player'; разные cookie и audiencetelegram_idне должны быть занятыtext/plainне создаёт сессию, cookieSameSite=Laxcurl: сессия не создаётся, но ответ 500, а не 422 — см. #52Falseбез исключенияОкно «Задайте пароль» — ограничение интерфейса, а не сервера. Аккаунт без пароля по-прежнему может работать через API с сессией из Telegram. Это сделано намеренно: вход через Telegram остаётся полноценным.
Вне рамок, для пен-теста #25:
Найденная попутно давняя ошибка — ответ 500 вместо 422 на тело не в JSON, плюс эхо тела в
detailsответа 422 — вынесена в #52.Коммиты
82cf7a3Вход и регистрация по логину и паролюc73b451Пароль и привязка Telegram в профиле, пароль игрока из админки462079dФронт: вход по паролю, окно установки пароля, профиль и админкаa09766cREADME: вход по логину и паролюПроверки
.\.venv\Scripts\python.exe -m pytest— 128 passed. Из них 26 новых вtests/test_password_auth.py, ещё вtest_auth.pyобновлены методы входа по окружениям.npm run gen:apiс живого бэкенда — только добавления вschema.d.ts.npm run build(tsc + vite) — зелёный. В прод-бандле нет строк «Позже (dev)», «Тестовый вход»,auth/dev/login.curl:has_password: true;retry_after≈900;INVALID_CREDENTIALS;has_password: false.Отклонения от плана
createиupdateс пояснением в payload, а не новыми именами: фильтр журнала в админке знает толькоlogin/create/update/delete.WRONG_CURRENT_PASSWORD, а не 401, чтобы не путать его с истёкшей сессией.Closes #24
🤖 Generated with Claude Code
https://claude.ai/code/session_01XfTsytzT6TojfmprRDKiV6
PUT /api/users/me/password задаёт или меняет пароль. Первый раз текущий пароль не нужен: так его задают аккаунты из Telegram и все, кто появился до паролей. Если пароль уже есть, нужен текущий. Иначе оставленная открытой сессия позволила бы отобрать аккаунт насовсем, поэтому подбор текущего тоже ограничен: 5 неудач на аккаунт за 15 минут. Ошибка 403 WRONG_CURRENT_PASSWORD, а не 401, чтобы фронт не принял её за истёкшую сессию. POST /api/users/me/telegram привязывает Telegram к аккаунту, созданному по паролю. Подпись виджета проверяется так же, как при входе, ник не меняется. Связка пишется в auth_identity, как при регистрации через Telegram, поэтому следующий вход через Telegram попадает в этот аккаунт. Telegram, привязанный к другому аккаунту, даёт 409 TELEGRAM_TAKEN, повторная привязка — 409 TELEGRAM_ALREADY_LINKED. PUT /api/admin/users/{id}/password — способ восстановить забытый пароль: почту приложение не хранит. Работает только для игроков, пароль админа по-прежнему задаётся в .env. В аудит пишется только факт смены, без пароля. Сборка AdminUserRead вынесена в хелпер, в ответе появилось has_password. #24 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XfTsytzT6TojfmprRDKiV6