All checks were successful
Deploy / changes (push) Successful in 5s
Deploy Trade-In / changes (push) Successful in 5s
Deploy Trade-In / build-backend (push) Has been skipped
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy Trade-In / build-frontend (push) Successful in 1m42s
Deploy Trade-In / deploy (push) Successful in 39s
Deploy / build-frontend (push) Successful in 2m55s
Deploy / deploy (push) Successful in 1m1s
PR B (frontend) to backend RBAC #585. RouteGuard wraps app, NoAccessScreen on 403 from /me, TopNav filtered by role.
57 lines
1.8 KiB
TypeScript
57 lines
1.8 KiB
TypeScript
/**
|
||
* 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<string, RegExp>();
|
||
|
||
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;
|
||
}
|