From 238f14a0d7b2315966f70e52cff156c53224910d Mon Sep 17 00:00:00 2001 From: bot-frontend Date: Sat, 30 May 2026 16:29:11 +0000 Subject: [PATCH] =?UTF-8?q?fix(tradein):=20account-=D0=B1=D1=80=D0=B5?= =?UTF-8?q?=D0=BD=D0=B4=20=D0=BF=D1=80=D0=B8=D0=BE=D1=80=D0=B8=D1=82=D0=B5?= =?UTF-8?q?=D1=82=D0=B5=D0=BD=20=D0=BD=D0=B0=D0=B4=20=3Fbrand=3D=20?= =?UTF-8?q?=E2=80=94=20cross-brand=20leak=20(#765)=20(#780)=20Co-authored-?= =?UTF-8?q?by:=20bot-frontend=20=20Co-committe?= =?UTF-8?q?d-by:=20bot-frontend=20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tradein-mvp/frontend/src/lib/useBrand.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/tradein-mvp/frontend/src/lib/useBrand.ts b/tradein-mvp/frontend/src/lib/useBrand.ts index 4df5b972..c37c4bd7 100644 --- a/tradein-mvp/frontend/src/lib/useBrand.ts +++ b/tradein-mvp/frontend/src/lib/useBrand.ts @@ -3,10 +3,10 @@ /** * useBrand — white-label resolver для #657 («Практика» и пр.). * - * Активный бренд резолвится из ДВУХ источников (param wins): - * 1. query-параметр `?brand=` в URL — явный override (демо/превью); - * 2. `me.brand` из /me — привязка бренда к аккаунту (#657 brand-by-account): - * praktika/kopylov логинятся → брендированный UI автоматически, без ?brand=. + * Активный бренд резолвится из ДВУХ источников (account-бренд приоритетен, #765): + * 1. `me.brand` из /me — привязка бренда к аккаунту (#657 brand-by-account) — WINS; + * 2. query-параметр `?brand=` — демо/превью, только если у аккаунта нет бренда. + * (praktika/kopylov логинятся → свой бренд автоматически из me.brand, без ?brand=.) * Если ни один не задан — нет бренда (generic UI), хук возвращает {data: null}. * * Mirror паттерна useMe.ts: TanStack Query, staleTime/gcTime Infinity @@ -38,15 +38,17 @@ export function getBrandSlugFromUrl(): string | null { } /** - * Активный slug бренда: `?brand=` param ПЕРЕВЕШИВАЕТ me.brand (account binding). + * Активный slug бренда: account-бренд (me.brand) ПРИОРИТЕТЕН над `?brand=`. + * `?brand=` разрешён только когда у аккаунта нет своего бренда (демо/превью) — + * иначе любой залогиненный мог бы отрендерить чужой бренд (cross-brand leak, #765). * Hook (читает useMe) — вызывать только в React-компонентах. */ export function useActiveBrandSlug(): string | null { const { data: me } = useMe(); - const urlSlug = getBrandSlugFromUrl(); - if (urlSlug) return urlSlug; - const accountSlug = me?.brand; - return accountSlug ? accountSlug.trim().toLowerCase() : null; + const accountSlug = me?.brand ? me.brand.trim().toLowerCase() : null; + // Account-бренд приоритетен: ?brand= НЕ может его перекрыть (cross-brand leak). + if (accountSlug) return accountSlug; + return getBrandSlugFromUrl(); } async function fetchBrand(slug: string): Promise {