feat(mera/b2c): анонимный расчёт и повторное чтение результата по токену (за флагом) #3230

Merged
bot-backend merged 3 commits from feat/b2c-anon-estimate into main 2026-08-29 14:37:21 +00:00
Collaborator

Воронка нового лэндинга упирается в две вещи: аноним не может получить расчёт, а получив — не может прочитать его повторно. Второе критично: платный отчёт обязан переживать оплату и перезагрузку страницы.

Всё за новым флагом public_estimate_enabled (дефолт False), поэтому мерж безопасен при выключенном контуре.

Что внутри

  • data/sql/278_trade_in_estimates_public_token.sqlpublic_token_hash + public_token_expires_at, partial UNIQUE по хэшу. Хранится хэш, не сам токен.
  • app/core/config.pypublic_estimate_enabled по образцу payments_enabled.
  • app/core/rbac.py — точные строки новых публичных путей.
  • app/api/public/mera.pyPOST /estimate (делегирует в существующую логику trade_in.estimate, своей копии расчёта нет — значит наследует анонимную квоту anon:cookie:IP, семафор и consent-гейт) и POST /estimate/read.

Два решения, которые стоит увидеть

Токен читается POST-ом с телом, а не GET-ом в пути. На meraocenka.ru включён access-лог Caddy, он пишет URI целиком — капабилити-токен в пути лёг бы в файл рядом с IP посетителя, и любой с доступом к логам или бэкапу открывает чужой расчёт. Это ровно тот довод, по которому /suggest в этом же модуле уже сделан POST-ом. Отступление от ТЗ осознанное, отмечено автором.

Публичный ответ урезан. Аноним получает только бесплатную часть: число аналогов, возраст объявлений, вердикт покрытия. Цена и прогноз не отдаются — они платные. Тест двусторонний: если сломать сериализатор и пустить лишнее поле, тест обязан покраснеть.

Согласие фиксируется ДО первого INSERT. Сегодня адрес физлица попадает в trade_in_estimates раньше любого согласия — этот порядок исправлен. Тест проверяет именно ОТСУТСТВИЕ строки при отказе, а не только код 422.

Приёмка на проде

Флаг выключен, поэтому в две стадии.

Сразу после деплоя:

ssh poincare "docker exec tradein-postgres psql -U tradein -d tradein -tAF'|' -c \"SELECT column_name FROM information_schema.columns WHERE table_name='trade_in_estimates' AND column_name LIKE 'public_token%'\""

Ожидается две строки. И ручка обязана отвечать 404 (флаг выключен), а не 401 — 401 означал бы, что путь не попал в _PUBLIC_PATHS.

После включения флага: без consent → 422 и в trade_in_estimates нет новой строки; успешный расчёт → строка с created_by IS NULL (сегодня таких 0 из 1123).

За владельцем

Открывать ли анонимный расчёт вообще — решение не инженерное: с этого момента адрес физлица начинает писаться в БД. Плюс п. 5.5 политики обработки ПДн сегодня не описывает анонимный расчёт и должен быть переписан ДО включения флага.

Воронка нового лэндинга упирается в две вещи: аноним не может получить расчёт, а получив — не может прочитать его повторно. Второе критично: платный отчёт обязан переживать оплату и перезагрузку страницы. Всё за новым флагом `public_estimate_enabled` (дефолт `False`), поэтому мерж безопасен при выключенном контуре. ## Что внутри * `data/sql/278_trade_in_estimates_public_token.sql` — `public_token_hash` + `public_token_expires_at`, partial UNIQUE по хэшу. Хранится **хэш**, не сам токен. * `app/core/config.py` — `public_estimate_enabled` по образцу `payments_enabled`. * `app/core/rbac.py` — точные строки новых публичных путей. * `app/api/public/mera.py` — `POST /estimate` (делегирует в существующую логику `trade_in.estimate`, своей копии расчёта нет — значит наследует анонимную квоту `anon:cookie:IP`, семафор и consent-гейт) и `POST /estimate/read`. ## Два решения, которые стоит увидеть **Токен читается POST-ом с телом, а не GET-ом в пути.** На `meraocenka.ru` включён access-лог Caddy, он пишет URI целиком — капабилити-токен в пути лёг бы в файл рядом с IP посетителя, и любой с доступом к логам или бэкапу открывает чужой расчёт. Это ровно тот довод, по которому `/suggest` в этом же модуле уже сделан POST-ом. Отступление от ТЗ осознанное, отмечено автором. **Публичный ответ урезан.** Аноним получает только бесплатную часть: число аналогов, возраст объявлений, вердикт покрытия. Цена и прогноз не отдаются — они платные. Тест двусторонний: если сломать сериализатор и пустить лишнее поле, тест обязан покраснеть. **Согласие фиксируется ДО первого INSERT.** Сегодня адрес физлица попадает в `trade_in_estimates` раньше любого согласия — этот порядок исправлен. Тест проверяет именно ОТСУТСТВИЕ строки при отказе, а не только код 422. ## Приёмка на проде Флаг выключен, поэтому в две стадии. Сразу после деплоя: ```bash ssh poincare "docker exec tradein-postgres psql -U tradein -d tradein -tAF'|' -c \"SELECT column_name FROM information_schema.columns WHERE table_name='trade_in_estimates' AND column_name LIKE 'public_token%'\"" ``` Ожидается две строки. И ручка обязана отвечать 404 (флаг выключен), а не 401 — 401 означал бы, что путь не попал в `_PUBLIC_PATHS`. После включения флага: без `consent` → 422 **и в `trade_in_estimates` нет новой строки**; успешный расчёт → строка с `created_by IS NULL` (сегодня таких 0 из 1123). ## За владельцем Открывать ли анонимный расчёт вообще — решение не инженерное: с этого момента адрес физлица начинает писаться в БД. Плюс п. 5.5 политики обработки ПДн сегодня не описывает анонимный расчёт и должен быть переписан ДО включения флага.
bot-backend added 1 commit 2026-08-29 13:59:05 +00:00
feat(mera): анонимный расчёт и капабилити-ссылка на его бесплатную часть
Some checks failed
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Failing after 11s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
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 / backend-tests (pull_request) Successful in 5m8s
5e39bfcb23
Публичный контур умел только подсказки и пробу покрытия: полный расчёт закрыт
RBAC, а результат анонима нельзя было прочитать повторно — _assert_estimate_access
отдаёт 404 на строку с created_by IS NULL всем, кроме админа, то есть расчёт жил
ровно в теле POST-ответа и не переживал перезагрузку страницы.

POST /api/public/mera/estimate делегирует в app.api.v1.trade_in.estimate (копии
логики нет — иначе публичная когорта разъедется с платной) и отдаёт наружу только
бесплатную часть: число аналогов и вердикт покрытия из той же coverage_probe.
Цены, прогнозы и списки аналогов остаются в БД для платного контура.

Согласие 152-ФЗ обязательно и строго True на уровне схемы, поэтому отказ
происходит до входа в хендлер — раньше, чем адрес физлица дошёл бы до БД.

POST /api/public/mera/estimate/read читает бесплатную часть по токену
(secrets.token_urlsafe(32), в БД только sha256, срок жизни 7 дней, миграция 278).
Токен едет телом: access-лог Caddy пишет URI целиком, и капабилити-ссылка в пути
легла бы в файл рядом с IP посетителя — тот же довод, по которому POST'ом сделан
/suggest. Постоянный путь заодно не требует префиксной ветки в rbac._PUBLIC_PATHS.

Всё закрыто флагом public_estimate_enabled (дефолт false → 404): включение
открывает запись ПДн и требует решения владельца вместе с правкой политики.
Light1YT added 1 commit 2026-08-29 14:06:33 +00:00
test(mera/b2c): пинит kwargs делегации анонимного расчёта, а не факт вызова
Some checks failed
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Failing after 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
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 / backend-tests (pull_request) Successful in 5m11s
ad653c066f
Весь анти-абузный контур публичной ручки (анонимная квота cookie+IP, семафор,
503 вместо 502, consent-гейт) держится на одном аргументе: в
app.api.v1.trade_in.estimate уходит x_authenticated_user=None. Проверял это
ноль тестов: estimate везде замокан AsyncMock, который принимает любую
сигнатуру, — подмена None на чтение заголовка запроса оставляла все 14 тестов
зелёными, а публичная форма начинала считать от чужого имени мимо квоты.

Новый тест шлёт запрос С заголовком X-Authenticated-User: admin и сверяет
фактические await_args.kwargs; заодно требует, чтобы аргументы ехали по имени
(позиционный вызов обесценивает сверку) и чтобы аргумент вообще присутствовал
(дефолт эстиматора — чужая гарантия, не наша). Проверено падением: подмена на
request.headers.get даёт «пришло: 'admin'».

Там же UPDATE токена: параметры сверялись только по хэшу, id строки — нет.
Теперь пинится result.estimate_id: токен обязан вешаться на только что
посчитанную оценку. Проверено подменой параметра — красный по значению.

test_read_filters_by_expiry_and_hash оставлен текстовым: живого Postgres с
миграцией 278 здесь нет, а поведенческий тест, ни разу не прогнанный, — это
ещё один зелёный по построению. Вместо этого в самом тесте написано, что он
проверяет (предикат есть в тексте SQL, в параметрах хэш) и чего НЕ проверяет
(сессия — MagicMock, запрос не исполняется, протухший токен не отсекается), и
чем его заменить, когда БД появится.
Light1YT added 1 commit 2026-08-29 14:14:40 +00:00
fix(tradein): миграция публичного токена без lock_timeout — гейт #2752
All checks were successful
CI Trade-In / changes (pull_request) Successful in 11s
CI / changes (pull_request) Successful in 14s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
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 / backend-tests (pull_request) Successful in 5m35s
df35c31084
278 делает ALTER TABLE trade_in_estimates ADD COLUMN + CREATE UNIQUE INDEX на
ЖИВОЙ таблице (1123 строки на проде). ALTER берёт ACCESS EXCLUSIVE: без
lock_timeout он встал бы в очередь за запросами приложения и утащил их за собой.

Обёрнуто в BEGIN + SET LOCAL lock_timeout = '5s' + COMMIT по образцу
272_houses_region_code.sql.
Light1YT force-pushed feat/b2c-anon-estimate from df35c31084 to 6255eccc7c 2026-08-29 14:28:47 +00:00 Compare
bot-backend merged commit 67d9efbac4 into main 2026-08-29 14:37:21 +00:00
bot-backend deleted branch feat/b2c-anon-estimate 2026-08-29 14:37:21 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#3230
No description provided.