feat(tradein/support): чат поддержки без входа — экран логина и «доступа нет» #2577

Merged
lekss361 merged 1 commit from feat/tradein-anon-support into main 2026-07-31 13:22:21 +00:00

1 commit

Author SHA1 Message Date
bot-backend
40fc94ee91 feat(tradein/support): чат поддержки без входа — экран логина и «доступа нет»
All checks were successful
CI / changes (pull_request) Successful in 12s
CI Trade-In / changes (pull_request) Successful in 12s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 1m42s
CI Trade-In / backend-tests (pull_request) Successful in 2m31s
После cutover'а на свою авторизацию (#2558) единственным каналом в поддержку
остался чат ЗА логином, а самая частая причина писать в поддержку — как раз
«не могу войти». 2026-07-31 это выстрелило: «Практика» весь день билась в форму
входа (5 неудачных попыток с трёх разных IP, ни одной успешной) и сообщить об
этом из продукта не могла ничем — на /login не было ни чата, ни контакта.

Backend — 4 ручки /api/v1/trade-in/support/anon/* (public в rbac_guard):
- Идентичность анонима — opaque-токен в httpOnly+Secure куке; тред живёт в тех
  же web_support_threads под ключом `anon:<token>`. Двоеточие делает коллизию с
  реальным логином структурно невозможной (CHECK миграции 193 разрешает только
  `^[A-Za-z0-9._-]{3,64}$`) — аноним не может попасть в чужой тред.
- Изоляция та же, что у авторизованной ветки: thread_id снаружи не принимается
  ни в каком виде, тред резолвится ИСКЛЮЧИТЕЛЬНО из куки.
- Форма куки валидируется — мусор из браузера не становится ключом треда.
- В Telegram-топик уходит не токен (это bearer треда), а `anon-<6 hex sha256>`;
  зеркало помечено «[С САЙТА · БЕЗ ВХОДА]» — оператору важно, что аккаунта нет.
- Анти-абуз: два бюджета — per-token (12/мин) и per-IP (10/10мин). Второй ловит
  обход ротацией куки, без него публичная ручка записи в общий топик беззащитна.
- Кука и запись в БД — только после успешного sendMessage (порядок операций H1),
  неудачная отправка не закрепляет за посетителем пустой тред.

Frontend:
- `SupportScope = "auth" | "anon"` в useSupportChat: scope выбирает базовый путь
  и входит в ключ кэша (иначе после логина в панели висела бы переписка анонима).
  Дефолт "auth" — существующие места монтирования не меняются.
- `AnonSupportWidget` монтируется на /login и в NoAccessScreen — обе точки тупики,
  из которых пользователю больше некуда идти. На /login добавлена подсказка.

Ответы оператора маршрутизируются без изменений в bridge.py: реплай резолвится
по topic_message_id → thread_id, кто автор треда — там неважно.

Тесты: 13 новых на анонимную ветку + 2 на границу public/authed в rbac_guard.
62 passed (test_support + test_rbac).
2026-07-31 16:04:51 +03:00