/** * MIRROR of main frontend `frontend/src/lib/isPathAllowed.ts` — keep in sync * manually. Rationale: tradein-mvp — отдельный Next.js bundle с basePath=/trade-in, * нет общего workspace package, поэтому дублируем (как делают backend mirrors * `backend/app/core/auth.py` ↔ `tradein-mvp/backend/app/core/auth.py`). * * JS-порт `backend/app/core/auth.py::is_path_allowed`. Семантика glob: * `/**` → matches everything (admin scope) * `/foo/**` → matches `/foo`, `/foo/`, `/foo/bar`, … * `/foo/*` → один сегмент после `/foo/` * `/foo` → exact match */ const REGEX_META = /[.+^${}()|[\]\\]/g; function escapeRegex(s: string): string { return s.replace(REGEX_META, "\\$&"); } function globToRegex(pattern: string): RegExp { const dstar = "\x00DSTAR\x00"; const sstar = "\x00SSTAR\x00"; const work = pattern.replace(/\*\*/g, dstar).replace(/\*/g, sstar); let regex = escapeRegex(work); regex = regex.replace(new RegExp(escapeRegex(dstar), "g"), ".*"); regex = regex.replace(new RegExp(escapeRegex(sstar), "g"), "[^/]*"); if (regex.endsWith("/.*")) { regex = regex.slice(0, -"/.*".length) + "(?:/.*)?"; } return new RegExp(`^${regex}$`); } const REGEX_CACHE = new Map(); function compileGlob(pattern: string): RegExp { const cached = REGEX_CACHE.get(pattern); if (cached) return cached; const compiled = globToRegex(pattern); REGEX_CACHE.set(pattern, compiled); return compiled; } export function isPathAllowed( allowedPaths: readonly string[], denyPaths: readonly string[], path: string, ): boolean { for (const p of denyPaths) { if (compileGlob(p).test(path)) return false; } for (const p of allowedPaths) { if (compileGlob(p).test(path)) return true; } return false; }