fix(tradein): account-бренд приоритетен над ?brand= — cross-brand leak (#765) (#780)
All checks were successful
Deploy Trade-In / changes (push) Successful in 6s
Deploy Trade-In / test (push) Has been skipped
Deploy Trade-In / build-backend (push) Has been skipped
Deploy Trade-In / build-frontend (push) Successful in 1m31s
Deploy Trade-In / deploy (push) Successful in 34s

Co-authored-by: bot-frontend <bot-frontend@gendsgn.local>
Co-committed-by: bot-frontend <bot-frontend@gendsgn.local>
This commit is contained in:
bot-frontend 2026-05-30 16:29:11 +00:00 committed by bot-reviewer
parent 489666c735
commit 238f14a0d7

View file

@ -3,10 +3,10 @@
/**
* useBrand white-label resolver для #657 («Практика» и пр.).
*
* Активный бренд резолвится из ДВУХ источников (param wins):
* 1. query-параметр `?brand=<slug>` в 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=<slug>` демо/превью, только если у аккаунта нет бренда.
* (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<Brand> {