fix(site-finder): вернуть доступ к профилям весов — владелец из сессии (#2782) #2788

Merged
bot-backend merged 1 commit from fix/2782-weight-profiles-unreachable into main 2026-08-07 10:22:56 +00:00
Collaborator

Что не так

CRUD именованных профилей POI-весов на /legacy/site-finder был недостижим никому без DevTools.

Панель отпирала его значением admin_token из localStorage. Записывать его было нечем — инпут удалили в #442, setItem("admin_token", …) во фронте нет ни одного. Поле «User ID» тоже не показывалось: оно само стояло за {!!adminToken && …} (page.tsx:333).

Признак был мёртвый — проверено живым запросом, а не рассуждением

Сервер перестал читать X-Admin-Token в #437, последний verify_admin_token удалён в #2775. Ни Caddy, ни какой-либо другой промежуточный слой этот заголовок не смотрят (Caddyfile — только header_up X-Authenticated-User).

GET /api/v1/admin/site-finder/weight-profiles?user_id=admin на проде:

запрос результат
X-Authenticated-User: admin, без X-Admin-Token 200 + профиль
X-Authenticated-User: admin + мусорный X-Admin-Token 200, ответ байт-в-байт тот же
без X-Authenticated-User 401 no authenticated user
X-Authenticated-User: pilottest 403 admin only
X-Authenticated-User: analysttest 403 admin only

Через периметр (https://gendsgn.ru, basic_auth admin) — то же самое: 200 и с мусорным токеном, и без него; без кред — 401 от Caddy; подставленный клиентом X-Authenticated-User без кред — тоже 401 (Caddy не пропускает).

Снятие гейта не открывает ничего сверх того, что уже открыто сессией и ролью.

Что сделано

  1. Мёртвый механизм убран целиком: гейт !!adminToken, оба чтения localStorage (admin_token + admin_user_id), отправка заголовка во всех четырёх хуках, пропсы userId/adminToken панели и инпут User ID.
  2. Владелец профилей — вошедший пользователь: панель берёт username из /api/v1/me — там же, где его берут RouteGuard и Topbar (тот же queryKey, лишнего запроса нет). Профили и так per-user. Единственный сохранённый на проде профиль лежит под user_id='admin' — ровно под именем basic_auth-аккаунта, миграция не нужна.
  3. Вторая оборванная проводка на том же пути. analyze отправлял profile_id без profile_user_id, а resolve_weights() ищет профиль как get_profile(db, user_id, profile_id) — при user_id=None веса молча падали на системные, и ответ при этом рапортовал weights_profile.source = "profile". На проде: profile_id=1 без profile_user_idtram_stop −0.5 вместо −0.4 из профиля. Без этой правки разблокированный дропдаун показывал бы ползунки одного профиля, а score считался бы по другим весам. Ветка вынесена в buildAnalyzeOptions() — она дублировалась в двух местах page.tsx.
  4. Микрокопия. «Укажите User ID и Admin Token для сохранения профилей» обещала поле, которого нет. Новый текст называет причину, а не действие, и в проде вообще недостижим (без входа страницу не отдаёт Caddy). В диалоге сохранения добавлена строка «Сохранится для пользователя <username>».

Правка в общем компоненте, поэтому чинится и вторая страница: /site-finder/analysis/[cad] §4.1 рендерит ту же панель вообще без пропсов и показывала ту же ложную подсказку.

Test plan

  • frontend/src/components/site-finder/__tests__/WeightProfilePanel.identity.test.tsx — 6 тестов.
  • Красный прогон на старом коде, двумя проходами:
    • файлы панели и API-клиента из origin/main3 failed | 3 passed (нет дропдауна профилей, есть текст про Admin Token, в запросе есть X-Admin-Token);
    • ветка buildAnalyzeOptions возвращена к логике origin/main (profileId != null ? { profileId }) → 2 failed | 4 passed (expected { profileId: 7 } to deeply equal { profileId, profileUserId }).
  • npm run type-check, npm run lint — чисто; полный vitest run — 270 passed (33 файла).
  • Скриншот прод-страницы до правки (залогинен как admin): подсказка «Укажите User ID и Admin Token», дропдауна и кнопки «Сохранить как профиль» нет.
  • После деплоя: скриншот той же страницы + прод-проверка, что CRUD доступен админу и недоступен не-админу.

Границы

Бэкенд не тронут. Новое поле ввода токена не вводилось.

Refs #2782

## Что не так CRUD именованных профилей POI-весов на `/legacy/site-finder` был недостижим никому без DevTools. Панель отпирала его значением `admin_token` из `localStorage`. Записывать его было нечем — инпут удалили в #442, `setItem("admin_token", …)` во фронте нет ни одного. Поле «User ID» тоже не показывалось: оно само стояло за `{!!adminToken && …}` (`page.tsx:333`). ## Признак был мёртвый — проверено живым запросом, а не рассуждением Сервер перестал читать `X-Admin-Token` в #437, последний `verify_admin_token` удалён в #2775. Ни Caddy, ни какой-либо другой промежуточный слой этот заголовок не смотрят (`Caddyfile` — только `header_up X-Authenticated-User`). `GET /api/v1/admin/site-finder/weight-profiles?user_id=admin` на проде: | запрос | результат | |---|---| | `X-Authenticated-User: admin`, **без** `X-Admin-Token` | `200` + профиль | | `X-Authenticated-User: admin` + **мусорный** `X-Admin-Token` | `200`, ответ байт-в-байт тот же | | без `X-Authenticated-User` | `401 no authenticated user` | | `X-Authenticated-User: pilottest` | `403 admin only` | | `X-Authenticated-User: analysttest` | `403 admin only` | Через периметр (`https://gendsgn.ru`, basic_auth `admin`) — то же самое: `200` и с мусорным токеном, и без него; без кред — `401` от Caddy; подставленный клиентом `X-Authenticated-User` без кред — тоже `401` (Caddy не пропускает). Снятие гейта не открывает ничего сверх того, что уже открыто сессией и ролью. ## Что сделано 1. **Мёртвый механизм убран целиком**: гейт `!!adminToken`, оба чтения `localStorage` (`admin_token` + `admin_user_id`), отправка заголовка во всех четырёх хуках, пропсы `userId`/`adminToken` панели и инпут User ID. 2. **Владелец профилей — вошедший пользователь**: панель берёт `username` из `/api/v1/me` — там же, где его берут `RouteGuard` и `Topbar` (тот же queryKey, лишнего запроса нет). Профили и так per-user. Единственный сохранённый на проде профиль лежит под `user_id='admin'` — ровно под именем basic_auth-аккаунта, миграция не нужна. 3. **Вторая оборванная проводка на том же пути.** `analyze` отправлял `profile_id` без `profile_user_id`, а `resolve_weights()` ищет профиль как `get_profile(db, user_id, profile_id)` — при `user_id=None` веса молча падали на системные, и ответ при этом рапортовал `weights_profile.source = "profile"`. На проде: `profile_id=1` без `profile_user_id` → `tram_stop −0.5` вместо `−0.4` из профиля. Без этой правки разблокированный дропдаун показывал бы ползунки одного профиля, а score считался бы по другим весам. Ветка вынесена в `buildAnalyzeOptions()` — она дублировалась в двух местах `page.tsx`. 4. **Микрокопия.** «Укажите User ID и Admin Token для сохранения профилей» обещала поле, которого нет. Новый текст называет причину, а не действие, и в проде вообще недостижим (без входа страницу не отдаёт Caddy). В диалоге сохранения добавлена строка «Сохранится для пользователя `<username>`». Правка в общем компоненте, поэтому чинится и вторая страница: `/site-finder/analysis/[cad]` §4.1 рендерит ту же панель вообще без пропсов и показывала ту же ложную подсказку. ## Test plan - [x] `frontend/src/components/site-finder/__tests__/WeightProfilePanel.identity.test.tsx` — 6 тестов. - [x] **Красный прогон на старом коде**, двумя проходами: - файлы панели и API-клиента из `origin/main` → `3 failed | 3 passed` (нет дропдауна профилей, есть текст про Admin Token, в запросе есть `X-Admin-Token`); - ветка `buildAnalyzeOptions` возвращена к логике `origin/main` (`profileId != null ? { profileId }`) → `2 failed | 4 passed` (`expected { profileId: 7 } to deeply equal { profileId, profileUserId }`). - [x] `npm run type-check`, `npm run lint` — чисто; полный `vitest run` — 270 passed (33 файла). - [x] Скриншот прод-страницы до правки (залогинен как `admin`): подсказка «Укажите User ID и Admin Token», дропдауна и кнопки «Сохранить как профиль» нет. - [ ] После деплоя: скриншот той же страницы + прод-проверка, что CRUD доступен админу и недоступен не-админу. ## Границы Бэкенд не тронут. Новое поле ввода токена не вводилось. Refs #2782
bot-backend added 1 commit 2026-08-07 10:13:38 +00:00
fix(site-finder): вернуть доступ к профилям весов — владелец из сессии (#2782)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 23s
CI / changes (pull_request) Successful in 23s
CI Trade-In / backend-tests (pull_request) Has been skipped
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) Successful in 2m18s
CI / openapi-codegen-check (pull_request) Successful in 3m48s
efddb1c7c7
CRUD именованных профилей POI-весов был недостижим никому без DevTools.
Панель отпирала его значением `admin_token` из localStorage, а записать это
значение было нечем: инпут удалили в #442. Поле «User ID» тоже не показывалось —
оно само стояло за `{!!adminToken && ...}`.

Признак, за которым была заперта фича, ничего не решал. Сервер перестал читать
`X-Admin-Token` в #437, последний `verify_admin_token` удалён в #2775. Проверено
живым запросом на проде: GET weight-profiles с корректным токеном, с мусорным и
без заголовка вовсе дают один и тот же 200. Настоящая защита — два других слоя,
тоже прод-проверенных: Caddy basic_auth (без кред 401 на периметре, клиентский
X-Authenticated-User не проходит) и rbac_guard (`role != admin` → 403 admin only
для pilot и analyst).

Поэтому мёртвый механизм убран целиком, а не наполовину: гейт `!!adminToken`,
оба чтения localStorage, отправка заголовка во всех четырёх хуках и пропсы
`userId`/`adminToken` панели. Владельца профилей панель берёт из `/api/v1/me` —
там же, где его берут RouteGuard и Topbar (тот же queryKey, лишнего запроса нет).
Профили и так per-user, а единственный сохранённый на проде профиль лежит под
`user_id='admin'` — ровно под именем basic_auth-аккаунта, так что миграции нет.

Заодно чинится вторая оборванная проводка на том же пути. `analyze` отправлял
`profile_id` без `profile_user_id`, а `resolve_weights()` ищет профиль как
`get_profile(db, user_id, profile_id)` — при `user_id=None` веса молча падали на
системные, при этом ответ рапортовал `weights_profile.source = "profile"`.
Проверено на проде: `profile_id=1` без `profile_user_id` → `tram_stop −0.5`
вместо `−0.4` из профиля. Без этой правки выбор профиля в разблокированном
дропдауне показывал бы одни ползунки, а score считался бы по другим весам.
Ветка вынесена в `buildAnalyzeOptions()` — она дублировалась в двух местах.

Микрокопия: «Укажите User ID и Admin Token для сохранения профилей» обещала поле,
которого нет. Текст называет причину, а не действие, и в проде вообще
недостижим — без входа страницу не отдаёт Caddy.

Refs #2782
bot-backend merged commit 209e4e145f into main 2026-08-07 10:22:56 +00:00
bot-backend deleted branch fix/2782-weight-profiles-unreachable 2026-08-07 10:22:56 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
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#2788
No description provided.