Добавление аутентификации логин/пароль (#24) #53

Merged
NotBigGhost merged 4 commits from issue-24-login-password into dev 2026-09-13 15:36:51 +03:00
Collaborator

Что сделано

Вход по логину и паролю стал основным во всех окружениях, 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.ai/code/session_01XfTsytzT6TojfmprRDKiV6

## Что сделано Вход по логину и паролю стал основным во всех окружениях, 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
Agent added 4 commits 2026-09-13 14:13:48 +03:00
Игроки входили только через 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
NotBigGhost merged commit 0cbee9d866 into dev 2026-09-13 15:36:51 +03:00
NotBigGhost deleted branch issue-24-login-password 2026-09-13 15:36:51 +03:00
Sign in to join this conversation.