Files
leaudit-platform-backend/docs/superpowers/plans/2026-05-22-route-permission-guard.md
T
2026-05-25 09:50:01 +08:00

5.8 KiB

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:

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:

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:

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:

/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

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
cd legal-platform-frontend
node --experimental-strip-types --test tests/govdoc-audit/*.test.mts
  • Step 3: Run lint
cd legal-platform-frontend
npm run lint -- --quiet
  • Step 4: Run production build
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.