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 —
|
// Публичный (B2C) периметр МЕРЫ. В отличие от NEXT_PUBLIC_ENABLE_PREVIEW —
|
||||||
// ЭТОТ bypass ВСЕГДА включён (в т.ч. в проде): страница публична по
|
// ЭТОТ bypass ВСЕГДА включён (в т.ч. в проде): страница публична по
|
||||||
// продуктовому решению, а не временный QA-артефакт. Домен/rewrite для неё
|
// продуктовому решению, а не временный QA-артефакт.
|
||||||
// настраиваются отдельным PR — на поведение гварда это не влияет, он смотрит
|
//
|
||||||
// на путь.
|
// ⚠️ Здесь раньше стояло: «Домен/rewrite настраиваются отдельным PR — на
|
||||||
|
// поведение гварда это не влияет, он смотрит на путь». Это оказалось неверно:
|
||||||
|
// короткий домен переписывает путь внутренне, и в браузере остаётся «/».
|
||||||
|
// Разбор и список коротких адресов — в комментарии к PUBLIC_SHORT_PATHS ниже.
|
||||||
//
|
//
|
||||||
// ⚠️ Пути здесь — БЕЗ basePath. `usePathname()` в Next возвращает путь в
|
// ⚠️ Пути здесь — БЕЗ basePath. `usePathname()` в Next возвращает путь в
|
||||||
// пространстве без префикса: в проде (basePath=/trade-in) на странице
|
// пространстве без префикса: в проде (basePath=/trade-in) на странице
|
||||||
|
|
@ -45,7 +48,37 @@ const GuardedRoute = dynamic(() => import("@/components/auth/GuardedRoute"));
|
||||||
// срабатывать в проде, и аноним поедет на /login.
|
// срабатывать в проде, и аноним поедет на /login.
|
||||||
const PUBLIC_PATHS = ["/mera-public"];
|
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}/`));
|
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