From e74e9106b3b3fba6a56cf7e0f19fb009b5024b12 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Sun, 16 Aug 2026 10:39:15 +0300 Subject: [PATCH] =?UTF-8?q?fix(mera/b2c):=20/estimate=20=D1=81=D0=BD=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=20=D0=B7=D0=B0=D0=BA=D1=80=D1=8B=D0=B2=D0=B0=D0=BB?= =?UTF-8?q?=D1=81=D1=8F=20=D0=B3=D0=B2=D0=B0=D1=80=D0=B4=D0=BE=D0=BC=20?= =?UTF-8?q?=E2=80=94=20=D0=BA=D0=BE=D1=80=D0=BE=D1=82=D0=BA=D0=B8=D0=B9=20?= =?UTF-8?q?=D0=B0=D0=B4=D1=80=D0=B5=D1=81=20=D0=B7=D0=B0=D0=B2=D0=B5=D0=BB?= =?UTF-8?q?=D0=B8=20=D0=B2=20Caddy,=20=D0=BD=D0=BE=20=D0=BD=D0=B5=20=D0=B2?= =?UTF-8?q?=20RouteGuard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Замер на проде сразу после выкатки #2913: страница `/estimate` отдаёт 200 и выглядит правильно, но после гидратации монтируется `GuardedRoute` — 404 /trade-in/api/v1/me ×3 404 /trade-in/api/v1/trade-in/support/anon/unread плюс виджет чата поддержки из закрытого контура и экран «Не удалось проверить доступ» внизу страницы. Форма при этом переставала работать ВОВСЕ: разметка с сервера видна, поле принимает текст, а обработчики не подключены — человек печатает адрес и не получает ни подсказок, ни ошибки. Проверено, что дело не в API: ручной `fetch` из той же страницы отдаёт 200 и восемь подсказок. Причина ровно та же, что чинил PR #2910 для `/`: список публичных путей в гварде и матчер `@meraPages` в Caddyfile — одно множество, записанное в двух местах. Тогда разошлись на `/`, теперь на `/estimate`. Оба раза страница отдавала 200, ломалась только в браузере, и ни один тест этого не видел. Комментарий в шапке гварда инцидент не предотвратил (он там был), поэтому совпадение теперь запинено ТЕСТОМ: он читает Caddyfile и требует, чтобы каждый путь из `@meraPages` проходил `isPublicPath`. Сверяемся с источником, а не дублируем список третий раз. Мутация — вернуть список к состоянию, которое уехало на прод: 2 failed / 3 passed × пропускает короткие адреса публичного домена × совпадает с тем, что публичный домен реально раздаёт (Caddyfile) откат → 5 passed. Плюс vitest 56 passed, tsc, lint, isolation guard 41 файл. `Caddyfile` уже в paths-фильтре фронтового лэйна (#2913), так что правка одного лишь конфига теперь тоже гоняет этот гейт. --- .../src/components/auth/RouteGuard.tsx | 22 ++++++++--- .../__tests__/RouteGuard.publicPaths.test.ts | 37 +++++++++++++++++++ 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/tradein-mvp/frontend/src/components/auth/RouteGuard.tsx b/tradein-mvp/frontend/src/components/auth/RouteGuard.tsx index 36a6a379..1a5ceb77 100644 --- a/tradein-mvp/frontend/src/components/auth/RouteGuard.tsx +++ b/tradein-mvp/frontend/src/components/auth/RouteGuard.tsx @@ -65,12 +65,24 @@ const PUBLIC_PATHS = ["/mera-public"]; * проверить доступ». Замер на проде: три вызова `/me` плюс подтянутый следом * клиент чата поддержки. * - * Те же четыре коротких адреса раздаёт Caddy на публичном домене, и только на - * нём: на `gendsgn.ru` их ловит catch-all 404, а `/` там отдаёт server-side - * redirect на `/v2` из `next.config` — то есть приложение по пути `/` вообще не - * рендерится и bypass ему не достаётся. + * Те же короткие адреса раздаёт Caddy на публичном домене, и только на нём: на + * `gendsgn.ru` их ловит catch-all 404, а `/` там отдаёт server-side redirect на + * `/v2` из `next.config` — то есть приложение по пути `/` вообще не рендерится + * и bypass ему не достаётся. + * + * ⚠️ ЭТОТ СПИСОК ОБЯЗАН СОВПАДАТЬ С МАТЧЕРОМ `@meraPages` В КОРНЕВОМ Caddyfile. + * Повтор инцидента 16.08.2026: короткий `/estimate` завели в Caddy и забыли + * здесь. Страница отдавала 200 и выглядела правильно, но после гидратации + * монтировался `GuardedRoute` — три запроса к `/me` (404, API на этом хосте не + * проксируется), виджет чата поддержки из закрытого контура и экран «Не удалось + * проверить доступ» внизу. Форма при этом переставала работать вовсе: разметка + * с сервера видна, а обработчики не подключались — человек печатал адрес и не + * получал ни подсказок, ни ошибки. + * + * Совпадение теперь запинено тестом (`__tests__/RouteGuard.publicPaths.test.ts` + * читает Caddyfile), потому что комментарий этот инцидент уже не предотвратил. */ -const PUBLIC_SHORT_PATHS = ["/", "/oferta", "/refund", "/privacy"]; +const PUBLIC_SHORT_PATHS = ["/", "/estimate", "/oferta", "/refund", "/privacy", "/v3"]; export function isPublicPath(rawPath: string): boolean { // Точное совпадение — ОТДЕЛЬНОЙ веткой и первым делом. Префиксную проверку diff --git a/tradein-mvp/frontend/src/components/auth/__tests__/RouteGuard.publicPaths.test.ts b/tradein-mvp/frontend/src/components/auth/__tests__/RouteGuard.publicPaths.test.ts index de8c0af2..21eedf8c 100644 --- a/tradein-mvp/frontend/src/components/auth/__tests__/RouteGuard.publicPaths.test.ts +++ b/tradein-mvp/frontend/src/components/auth/__tests__/RouteGuard.publicPaths.test.ts @@ -1,3 +1,6 @@ +import { readFileSync } from "node:fs"; +import path from "node:path"; + import { describe, expect, it } from "vitest"; import { isPublicPath } from "../RouteGuard"; @@ -25,11 +28,45 @@ describe("isPublicPath", () => { it("пропускает короткие адреса публичного домена", () => { expect(isPublicPath("/")).toBe(true); + expect(isPublicPath("/estimate")).toBe(true); expect(isPublicPath("/oferta")).toBe(true); expect(isPublicPath("/refund")).toBe(true); expect(isPublicPath("/privacy")).toBe(true); }); + /** + * ГЛАВНЫЙ ГЕЙТ, добавленный после повтора инцидента 16.08.2026. + * + * Список публичных путей в гварде и матчер `@meraPages` в Caddyfile — это + * одно и то же множество, записанное в двух местах. Первый раз разошлись на + * `/`: домен появился, гвард о нём не знал. Второй раз — на `/estimate`: + * короткий адрес завели в Caddy, в гвард добавить забыли. Оба раза страница + * отдавала 200 и выглядела правильно, а ломалась только после гидратации — + * то есть ни один тест и ни один смоук по коду ответа этого не видел. + * + * Проверка читает конфиг прокси, а не дублирует список: единственный способ + * не разойтись — сверяться с источником. + */ + it("совпадает с тем, что публичный домен реально раздаёт (Caddyfile)", () => { + // __tests__ → auth → components → src → frontend → tradein-mvp → корень. + const repoRoot = path.resolve(__dirname, "../../../../../.."); + const caddyfile = readFileSync(path.join(repoRoot, "Caddyfile"), "utf8"); + const served = caddyfile + .match(/@meraPages path ([^\n]+)/)?.[1] + .split(/\s+/) + .filter(Boolean); + + expect(served, "не нашли матчер @meraPages — проверка стала бы пустой").toBeTruthy(); + + for (const route of served ?? []) { + expect( + isPublicPath(route), + `${route} раздаётся на meraocenka.ru, но гвард считает его закрытым — ` + + "после гидратации на нём смонтируется GuardedRoute и страница перестанет работать", + ).toBe(true); + } + }); + it("НЕ делает публичным закрытый контур — главный риск правки", () => { for (const closed of [ "/v2",