frontend/site-finder: CRUD профилей весов недостижим — UI просит Admin Token, которого негде взять #2782

Closed
opened 2026-08-07 08:59:23 +00:00 by bot-backend · 2 comments
Collaborator

Найдено при разборе #2775 (удаление мёртвой серверной проверки X-Admin-Token). Это обратный случай: серверная часть мертва, а клиентская — живая и именно поэтому сломанная.

Симптом

На /legacy/site-finder панель WeightProfilePanel показывает подсказку:

Укажите User ID и Admin Token для сохранения профилей.

Ввести Admin Token негде. Поля нет — оно удалено в PR #442. Поле «User ID» тоже не показывается, потому что само загорожено токеном (page.tsx:333{!!adminToken && (...)}).

Механика

frontend/src/app/legacy/site-finder/page.tsx:137
  const [adminToken] = useState<string>(() =>
    ... localStorage.getItem("admin_token") ?? "")

admin_token в localStorage только читается — по всему frontend/src нет ни одного setItem("admin_token", ...). Записывал его тот самый инпут, который убрали в #442.

Дальше:

WeightProfilePanel.tsx:93   const canUseCrud = !!userId && !!adminToken;   // всегда false
WeightProfilePanel.tsx:94   useWeightProfiles(userId ?? "", adminToken ?? "")  // enabled: false

Итог: сохранение / загрузка именованных профилей весов POI недоступны никому, кто не поставит ключ в localStorage руками через DevTools.

Почему заголовок вообще ни при чём

weightProfiles.ts:95 всё ещё шлёт {"X-Admin-Token": adminToken} во все запросы CRUD. Сервер этот заголовок не читает с PR #437, а backend/app/core/deps.py с его verify_admin_token удалён в #2775. То есть значение, за которым заперта фича, ни на что не влияет даже если его подставить.

Что чинить (нужно решение, а не только код)

Фича заперта на артефакт снятой авторизации. Варианты:

  1. Снять гейт по токену. canUseCrud = !!userId, убрать adminToken из пропсов и из profilesHeaders. Доступ к /api/v1/admin/* и так закрыт двумя живыми слоями — rbac_guard (role != admin → 403) и Caddy basic_auth. Тогда нужен видимый инпут User ID.
  2. Привязать user_id к вошедшему пользователю вместо ручного ввода — у обоих стеков есть единый вход, X-Authenticated-User доезжает до бэкенда.

Вариант 2 честнее (профили и так per-user), но это продуктовое решение — поэтому issue, а не PR.

Диагноз

Оборванная проводка. Не мёртвый код: компоненты, хук и 5 CRUD-эндпоинтов живы и покрыты тестами (backend/tests/test_admin_weight_profiles.py). Оборван ровно один провод — источник значения, которым фича разблокируется.

Найдено при разборе #2775 (удаление мёртвой серверной проверки `X-Admin-Token`). Это **обратный** случай: серверная часть мертва, а клиентская — живая и именно поэтому сломанная. ## Симптом На `/legacy/site-finder` панель `WeightProfilePanel` показывает подсказку: > Укажите User ID и Admin Token для сохранения профилей. Ввести Admin Token негде. Поля нет — оно удалено в PR #442. Поле «User ID» тоже не показывается, потому что само загорожено токеном (`page.tsx:333` → `{!!adminToken && (...)}`). ## Механика ``` frontend/src/app/legacy/site-finder/page.tsx:137 const [adminToken] = useState<string>(() => ... localStorage.getItem("admin_token") ?? "") ``` `admin_token` в `localStorage` **только читается** — по всему `frontend/src` нет ни одного `setItem("admin_token", ...)`. Записывал его тот самый инпут, который убрали в #442. Дальше: ``` WeightProfilePanel.tsx:93 const canUseCrud = !!userId && !!adminToken; // всегда false WeightProfilePanel.tsx:94 useWeightProfiles(userId ?? "", adminToken ?? "") // enabled: false ``` Итог: сохранение / загрузка именованных профилей весов POI недоступны никому, кто не поставит ключ в localStorage руками через DevTools. ## Почему заголовок вообще ни при чём `weightProfiles.ts:95` всё ещё шлёт `{"X-Admin-Token": adminToken}` во все запросы CRUD. Сервер этот заголовок не читает с PR #437, а `backend/app/core/deps.py` с его `verify_admin_token` удалён в #2775. То есть значение, за которым заперта фича, ни на что не влияет **даже если его подставить**. ## Что чинить (нужно решение, а не только код) Фича заперта на артефакт снятой авторизации. Варианты: 1. **Снять гейт по токену.** `canUseCrud = !!userId`, убрать `adminToken` из пропсов и из `profilesHeaders`. Доступ к `/api/v1/admin/*` и так закрыт двумя живыми слоями — `rbac_guard` (role != admin → 403) и Caddy basic_auth. Тогда нужен видимый инпут User ID. 2. **Привязать `user_id` к вошедшему пользователю** вместо ручного ввода — у обоих стеков есть единый вход, `X-Authenticated-User` доезжает до бэкенда. Вариант 2 честнее (профили и так per-user), но это продуктовое решение — поэтому issue, а не PR. ## Диагноз **Оборванная проводка.** Не мёртвый код: компоненты, хук и 5 CRUD-эндпоинтов живы и покрыты тестами (`backend/tests/test_admin_weight_profiles.py`). Оборван ровно один провод — источник значения, которым фича разблокируется.
Author
Collaborator

Working on this in PR #2788.

Предпосылка issue проверена живым запросом на проде и подтвердилась: X-Admin-Token не читает ни бэкенд, ни Caddy — GET weight-profiles отдаёт один и тот же 200 с корректным токеном, с мусорным и без заголовка вовсе. Настоящая защита — Caddy basic_auth (401 без кред) + rbac_guard (403 admin only для pilot и analyst).

Выбран вариант 2 (привязка к вошедшему пользователю), а не 1: единственный сохранённый на проде профиль лежит под user_id='admin' — под именем basic_auth-аккаунта, так что миграция не нужна, а видимый инпут User ID не нужен вовсе.

По ходу нашлась вторая оборванная проводка на том же пути: analyze слал profile_id без profile_user_id, и бэкенд молча считал по системным весам, рапортуя source="profile". Починено в том же PR — иначе разблокированный дропдаун показывал бы одни ползунки, а score считался бы по другим весам.

Working on this in PR #2788. Предпосылка issue проверена живым запросом на проде и подтвердилась: `X-Admin-Token` не читает ни бэкенд, ни Caddy — GET weight-profiles отдаёт один и тот же 200 с корректным токеном, с мусорным и без заголовка вовсе. Настоящая защита — Caddy basic_auth (401 без кред) + rbac_guard (403 admin only для pilot и analyst). Выбран вариант 2 (привязка к вошедшему пользователю), а не 1: единственный сохранённый на проде профиль лежит под `user_id='admin'` — под именем basic_auth-аккаунта, так что миграция не нужна, а видимый инпут User ID не нужен вовсе. По ходу нашлась вторая оборванная проводка на том же пути: analyze слал `profile_id` без `profile_user_id`, и бэкенд молча считал по системным весам, рапортуя `source="profile"`. Починено в том же PR — иначе разблокированный дропдаун показывал бы одни ползунки, а score считался бы по другим весам.
Author
Collaborator

Смержено в PR #2788, задеплоено, прод-проверено.

Деплой подтверждён по коду в контейнере, не по флагу: старая строка «Укажите User ID» в /app/.next контейнера gendesign-frontend-1 отсутствует, новая («Пользователь не определён») присутствует.

CRUD доступен админу — живым запросом через настоящий UI (Playwright, basic_auth admin, https://gendsgn.ru/legacy/site-finder):

OPTIONS: ['По умолчанию', 'admin ★']        ← профиль от 15.05, недостижимый с #442
CRUD REQUESTS: 1
   GET /api/v1/admin/site-finder/weight-profiles?user_id=admin
   X-Admin-Token sent?: False
SAVE DIALOG: «Сохранится для пользователя admin — другие его не увидят.»
OPTIONS AFTER SAVE: ['По умолчанию', 'admin ★', 'ЗЗ-проверка-2782']

Запись реально дошла до БД (id=5, user_id='admin', school=2.7 — ровно тот ползунок, который двигал тест) и удалена после проверки; в таблице снова исходные 4 строки.

Не-админ доступа не получает — тоже живым запросом:

запрос результат
pilottest GET 403 admin only
analysttest GET 403 admin only
pilottest POST (создать) 403
pilottest DELETE 403
без пользователя 401
через Caddy без кред 401
через Caddy со спуфнутым X-Authenticated-User, без кред 401
через Caddy, admin + мусорный X-Admin-Token 200 — заголовок по-прежнему ни на что не влияет

Вторая проводка тоже проверена на живом проде: analyze?profile_id=1&profile_user_id=adminsource: profile, tram_stop: −0.4 (веса профиля). До правки фронт слал profile_id в одиночку и получал −0.5, то есть системные веса под вывеской source="profile".

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


Что осталось за рамками этого issue (вынес в отдельный тикет, чтобы не потерялось):

  1. На /site-finder/analysis/[cad] §4.1 кнопка «Применить» ни к чему не приводит: weights в Section3SettingsAndCompetitors — write-only state, до analyze не доезжает. Это было и до правки, к токену отношения не имеет.
  2. Системные пресеты (user_id='__system__': Эконом / Комфорт / Бизнес, засеяны 16.05) не видны никому: хук не передаёт include_system=true. Просто передать нельзя — resolve_weights() ищет профиль как get_profile(db, user_id, profile_id), и __system__-профиль под чужим user_id не найдётся → тихий откат на системные веса. Нужна правка бэкенда.
  3. useUpdateProfile / useDeleteProfile не вызываются ниоткуда — в UI сейчас есть только list + create, редактирования и удаления профиля нет.
Смержено в PR #2788, задеплоено, прод-проверено. **Деплой подтверждён по коду в контейнере**, не по флагу: старая строка «Укажите User ID» в `/app/.next` контейнера `gendesign-frontend-1` отсутствует, новая («Пользователь не определён») присутствует. **CRUD доступен админу — живым запросом через настоящий UI** (Playwright, basic_auth `admin`, `https://gendsgn.ru/legacy/site-finder`): ``` OPTIONS: ['По умолчанию', 'admin ★'] ← профиль от 15.05, недостижимый с #442 CRUD REQUESTS: 1 GET /api/v1/admin/site-finder/weight-profiles?user_id=admin X-Admin-Token sent?: False SAVE DIALOG: «Сохранится для пользователя admin — другие его не увидят.» OPTIONS AFTER SAVE: ['По умолчанию', 'admin ★', 'ЗЗ-проверка-2782'] ``` Запись реально дошла до БД (`id=5, user_id='admin', school=2.7` — ровно тот ползунок, который двигал тест) и удалена после проверки; в таблице снова исходные 4 строки. **Не-админ доступа не получает** — тоже живым запросом: | запрос | результат | |---|---| | `pilottest` GET | `403 admin only` | | `analysttest` GET | `403 admin only` | | `pilottest` POST (создать) | `403` | | `pilottest` DELETE | `403` | | без пользователя | `401` | | через Caddy без кред | `401` | | через Caddy со спуфнутым `X-Authenticated-User`, без кред | `401` | | через Caddy, `admin` + мусорный `X-Admin-Token` | `200` — заголовок по-прежнему ни на что не влияет | **Вторая проводка тоже проверена на живом проде**: `analyze?profile_id=1&profile_user_id=admin` → `source: profile, tram_stop: −0.4` (веса профиля). До правки фронт слал `profile_id` в одиночку и получал `−0.5`, то есть системные веса под вывеской `source="profile"`. Правка в общем компоненте, поэтому чинится и `/site-finder/analysis/[cad]` §4.1 — там та же панель рендерится вообще без пропсов и показывала ту же ложную подсказку. --- **Что осталось за рамками этого issue** (вынес в отдельный тикет, чтобы не потерялось): 1. На `/site-finder/analysis/[cad]` §4.1 кнопка «Применить» ни к чему не приводит: `weights` в `Section3SettingsAndCompetitors` — write-only state, до `analyze` не доезжает. Это было и до правки, к токену отношения не имеет. 2. Системные пресеты (`user_id='__system__'`: Эконом / Комфорт / Бизнес, засеяны 16.05) не видны никому: хук не передаёт `include_system=true`. Просто передать нельзя — `resolve_weights()` ищет профиль как `get_profile(db, user_id, profile_id)`, и `__system__`-профиль под чужим `user_id` не найдётся → тихий откат на системные веса. Нужна правка бэкенда. 3. `useUpdateProfile` / `useDeleteProfile` не вызываются ниоткуда — в UI сейчас есть только list + create, редактирования и удаления профиля нет.
Sign in to join this conversation.
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#2782
No description provided.