# Route Permission Guard Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Prevent users from opening hidden/unauthorized frontend pages by manually entering URLs. **Architecture:** Keep backend API permission checks unchanged, and add a server-side Next.js page-route guard inside the authenticated `(audit)` layout. The guard uses backend user route authorization, normalizes feature subpages to their controlled route, and redirects unauthorized page requests before rendering content. **Tech Stack:** Next.js 15 App Router, TypeScript, Node test runner, existing RBAC `/api/rbac/user/routes` data. --- ### Task 1: Add Route Access Unit Tests **Files:** - Create: `legal-platform-frontend/lib/auth/route-access.ts` - Test: `legal-platform-frontend/tests/govdoc-audit/route-access.test.mts` - Modify: `legal-platform-frontend/lib/utils/route-alias.shared.js` - [ ] **Step 1: Write failing tests for direct URL authorization** Create tests that assert route guard behavior: ```ts import assert from "node:assert/strict"; import test from "node:test"; import { isRoutePathAllowed, flattenMenuPaths } from "../../lib/auth/route-access.ts"; import { normalizeRoutePathForPermission } from "../../lib/utils/route-alias.shared.js"; const allowedPaths = flattenMenuPaths([ { id: "home", title: "系统概览", path: "/home", icon: "", order: 1 }, { id: "rules", title: "规则管理", path: "/rules", icon: "", order: 2 }, { id: "contract", title: "合同管理", path: "/contract-template", icon: "", order: 3, children: [ { id: "contract-list", title: "模板列表", path: "/contract-template/list", icon: "", order: 1 }, ] }, ]); test("route guard allows exact authorized route", () => { assert.equal(isRoutePathAllowed("/rules", allowedPaths), true); }); test("route guard allows feature detail page through alias", () => { assert.equal(isRoutePathAllowed("/rules-test/detail?packId=3&ruleId=MM-ENT-001", allowedPaths), true); }); test("route guard rejects direct URL when route is hidden from role", () => { assert.equal(isRoutePathAllowed("/tenants", allowedPaths), false); }); test("route guard maps current govdoc pages to govdoc root permission", () => { assert.equal(normalizeRoutePathForPermission("/govdoc/audits"), "/govdoc"); assert.equal(normalizeRoutePathForPermission("/govdoc/detail/A-108bce03"), "/govdoc"); }); ``` - [ ] **Step 2: Run tests and verify failure** Run: ```bash cd legal-platform-frontend node --experimental-strip-types --test tests/govdoc-audit/route-access.test.mts ``` Expected: fail because `route-access.ts` does not exist yet and `/govdoc/*` is not normalized. ### Task 2: Implement Pure Route Access Helper **Files:** - Create: `legal-platform-frontend/lib/auth/route-access.ts` - Modify: `legal-platform-frontend/lib/utils/route-alias.shared.js` - [ ] **Step 1: Implement helper** Add functions: - `flattenMenuPaths(menuItems)` to extract authorized paths from route tree. - `normalizeRequestPath(pathname)` to strip query/hash/trailing slash and apply aliases. - `isRoutePathAllowed(pathname, allowedPaths)` to allow exact authorized routes, authorized route subtrees, and `/home`. - [ ] **Step 2: Add `/govdoc/*` route alias** Map current internal document pages to `/govdoc`, matching the existing legacy `/govdoc-audit/*` behavior. - [ ] **Step 3: Run focused tests** Run: ```bash cd legal-platform-frontend node --experimental-strip-types --test tests/govdoc-audit/route-access.test.mts tests/govdoc-audit/home-routing.test.mts ``` Expected: all tests pass. ### Task 3: Wire Server-Side Guard Into Authenticated Layout **Files:** - Modify: `legal-platform-frontend/app/(audit)/layout.tsx` - [ ] **Step 1: Read current pathname** Use `headers().get("x-pathname")`, which is already set by `middleware.ts`. - [ ] **Step 2: Fetch user authorized routes** Call `getUserRoutesByRole(userRole, frontendJWT, true)` from server layout. - [ ] **Step 3: Redirect unauthorized pages** If routes load successfully and `isRoutePathAllowed(pathname, flattenMenuPaths(routes))` is false, redirect to: ```ts /home?error=insufficient_permissions ``` Do not block `/home`. - [ ] **Step 4: Fail closed when routes cannot load** If route loading fails because the session is expired, redirect to login. For non-auth failures, redirect to `/home?error=permission_check_failed` except when already on `/home`. ### Task 4: Verify Build and Regression **Files:** - No new production files unless tests expose type issues. - [ ] **Step 1: Run focused tests** ```bash cd legal-platform-frontend node --experimental-strip-types --test tests/govdoc-audit/route-access.test.mts tests/govdoc-audit/home-routing.test.mts ``` - [ ] **Step 2: Run full existing frontend node tests** ```bash cd legal-platform-frontend node --experimental-strip-types --test tests/govdoc-audit/*.test.mts ``` - [ ] **Step 3: Run lint** ```bash cd legal-platform-frontend npm run lint -- --quiet ``` - [ ] **Step 4: Run production build** ```bash cd legal-platform-frontend npm run build ``` ### Task 5: Manual Acceptance Checklist - [ ] A role without `/tenants` cannot open `/tenants` by URL. - [ ] A role without `/rules` cannot open `/rules-test/list` or `/rules-test/detail?...` by URL. - [ ] A role with `/rules` can still open `/rules-test/list` and `/rules-test/detail?...`. - [ ] `/home` remains reachable for logged-in users. - [ ] Sidebar menu hiding remains unchanged. - [ ] Backend API 403 behavior remains independent. --- ### Self-Review - Spec coverage: Direct URL access is blocked at the server layout before page render. - Placeholder scan: No TBD/TODO remains in implementation steps. - Type consistency: Helper consumes existing `MenuItem` shape from `lib/auth/user-routes.ts`.