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

172 lines
5.8 KiB
Markdown

# 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`.