Merge pull request 'fix(mera/b2c): публичный лэндинг больше не показывает «Не удалось проверить доступ»' (#2910) from fix/mera-public-host-bypass into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / test (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-backend (push) Has been skipped
Deploy Trade-In / build-frontend (push) Successful in 2m38s
Deploy Trade-In / deploy (push) Successful in 1m12s
Deploy Trade-In / deploy-status (push) Successful in 1s
All checks were successful
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / test (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-backend (push) Has been skipped
Deploy Trade-In / build-frontend (push) Successful in 2m38s
Deploy Trade-In / deploy (push) Successful in 1m12s
Deploy Trade-In / deploy-status (push) Successful in 1s
This commit is contained in:
commit
7537b54dbd
2 changed files with 95 additions and 4 deletions
|
|
@ -32,9 +32,12 @@ const GuardedRoute = dynamic(() => import("@/components/auth/GuardedRoute"));
|
|||
|
||||
// Публичный (B2C) периметр МЕРЫ. В отличие от NEXT_PUBLIC_ENABLE_PREVIEW —
|
||||
// ЭТОТ bypass ВСЕГДА включён (в т.ч. в проде): страница публична по
|
||||
// продуктовому решению, а не временный QA-артефакт. Домен/rewrite для неё
|
||||
// настраиваются отдельным PR — на поведение гварда это не влияет, он смотрит
|
||||
// на путь.
|
||||
// продуктовому решению, а не временный QA-артефакт.
|
||||
//
|
||||
// ⚠️ Здесь раньше стояло: «Домен/rewrite настраиваются отдельным PR — на
|
||||
// поведение гварда это не влияет, он смотрит на путь». Это оказалось неверно:
|
||||
// короткий домен переписывает путь внутренне, и в браузере остаётся «/».
|
||||
// Разбор и список коротких адресов — в комментарии к PUBLIC_SHORT_PATHS ниже.
|
||||
//
|
||||
// ⚠️ Пути здесь — БЕЗ basePath. `usePathname()` в Next возвращает путь в
|
||||
// пространстве без префикса: в проде (basePath=/trade-in) на странице
|
||||
|
|
@ -45,7 +48,37 @@ const GuardedRoute = dynamic(() => import("@/components/auth/GuardedRoute"));
|
|||
// срабатывать в проде, и аноним поедет на /login.
|
||||
const PUBLIC_PATHS = ["/mera-public"];
|
||||
|
||||
function isPublicPath(rawPath: string): boolean {
|
||||
/**
|
||||
* КОРОТКИЕ АДРЕСА ПУБЛИЧНОГО ДОМЕНА (инцидент 15.08.2026).
|
||||
*
|
||||
* Прежний докстринг утверждал: «домен/rewrite для неё настраиваются отдельным
|
||||
* PR — на поведение гварда это не влияет, он смотрит на путь». Это допущение
|
||||
* оказалось неверным ровно в тот момент, когда домен появился.
|
||||
*
|
||||
* На `meraocenka.ru` Caddy переписывает путь ВНУТРЕННЕ (`rewrite`, не `redir`):
|
||||
* посетитель остаётся на `meraocenka.ru/`, а бэкенд получает
|
||||
* `/trade-in/mera-public`. Поэтому SSR видит `/mera-public` и bypass
|
||||
* срабатывает, а после гидратации `usePathname()` возвращает уже `/` — путь из
|
||||
* адресной строки, — bypass молча отваливается, монтируется `GuardedRoute`,
|
||||
* летит `GET /api/v1/me`, и посетитель публичной страницы получает 404 (на этом
|
||||
* хосте Caddy намеренно не проксирует `/trade-in/api/*`) и экран «Не удалось
|
||||
* проверить доступ». Замер на проде: три вызова `/me` плюс подтянутый следом
|
||||
* клиент чата поддержки.
|
||||
*
|
||||
* Те же четыре коротких адреса раздаёт Caddy на публичном домене, и только на
|
||||
* нём: на `gendsgn.ru` их ловит catch-all 404, а `/` там отдаёт server-side
|
||||
* redirect на `/v2` из `next.config` — то есть приложение по пути `/` вообще не
|
||||
* рендерится и bypass ему не достаётся.
|
||||
*/
|
||||
const PUBLIC_SHORT_PATHS = ["/", "/oferta", "/refund", "/privacy"];
|
||||
|
||||
export function isPublicPath(rawPath: string): boolean {
|
||||
// Точное совпадение — ОТДЕЛЬНОЙ веткой и первым делом. Префиксную проверку
|
||||
// ниже для «/» применять нельзя: `startsWith("/")` истинно для ЛЮБОГО пути,
|
||||
// то есть один такой элемент в списке сделал бы публичным всё приложение.
|
||||
if (PUBLIC_SHORT_PATHS.includes(rawPath)) {
|
||||
return true;
|
||||
}
|
||||
return PUBLIC_PATHS.some((p) => rawPath === p || rawPath.startsWith(`${p}/`));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { isPublicPath } from "../RouteGuard";
|
||||
|
||||
/**
|
||||
* Регресс на инцидент 15.08.2026: на meraocenka.ru посетитель публичного
|
||||
* лэндинга видел внизу страницы «Не удалось проверить доступ».
|
||||
*
|
||||
* Причина: Caddy переписывает `/` в `/trade-in/mera-public` ВНУТРЕННЕ, поэтому
|
||||
* после гидратации `usePathname()` отдаёт `/`, а в списке публичных путей был
|
||||
* только `/mera-public`. Bypass отваливался → монтировался GuardedRoute →
|
||||
* `GET /api/v1/me` → 404 (API на этом хосте намеренно не проксируется) →
|
||||
* NoAccessScreen.
|
||||
*
|
||||
* Второй охраняемый здесь инвариант важнее первого: «/» обязан сверяться
|
||||
* ТОЧНЫМ совпадением. Наивное добавление его в префиксный список сделало бы
|
||||
* публичным всё приложение — `"/v2/admin".startsWith("/")` истинно.
|
||||
*/
|
||||
describe("isPublicPath", () => {
|
||||
it("пропускает поддерево лэндинга (путь без basePath)", () => {
|
||||
expect(isPublicPath("/mera-public")).toBe(true);
|
||||
expect(isPublicPath("/mera-public/privacy")).toBe(true);
|
||||
expect(isPublicPath("/mera-public/v3")).toBe(true);
|
||||
});
|
||||
|
||||
it("пропускает короткие адреса публичного домена", () => {
|
||||
expect(isPublicPath("/")).toBe(true);
|
||||
expect(isPublicPath("/oferta")).toBe(true);
|
||||
expect(isPublicPath("/refund")).toBe(true);
|
||||
expect(isPublicPath("/privacy")).toBe(true);
|
||||
});
|
||||
|
||||
it("НЕ делает публичным закрытый контур — главный риск правки", () => {
|
||||
for (const closed of [
|
||||
"/v2",
|
||||
"/v2/admin",
|
||||
"/history",
|
||||
"/admin/analytics",
|
||||
"/admin/audit",
|
||||
"/scrapers",
|
||||
"/scrapers/avito",
|
||||
"/cache",
|
||||
"/team",
|
||||
"/versions",
|
||||
"/login",
|
||||
]) {
|
||||
expect(isPublicPath(closed), `${closed} не должен быть публичным`).toBe(
|
||||
false,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("не пропускает пути, лишь начинающиеся с публичного имени", () => {
|
||||
expect(isPublicPath("/mera-public-admin")).toBe(false);
|
||||
expect(isPublicPath("/ofertas")).toBe(false);
|
||||
expect(isPublicPath("/privacy-internal")).toBe(false);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue