feat(portal-shell): v2.1 P0 auth + middleware + login + graphql proxy

- 新增 ARCHITECTURE.md v3.0:portal-shell 架构权威文档
  涵盖 §3.4 V3-A2/A3 认证链、§4 GraphQL 联邦、§5 安全、
  §6 部署、§10 P0-P3 验收清单
- 新增 middleware.ts:认证 + 路由门禁
  httpOnly cookie edu_session(JWT)读取
  DEV_MODE 合成 dev-user/teacher 身份(NODE_ENV!=production &&
  NEXT_PUBLIC_DEV_MODE=true)
  生产模式 jose JWKS RS256 验签(iss/aud 校验)
  路由权限位图注入 x-user-id/x-user-role/x-user-permissions 头
  /shell/** 强制 checkRoutePermission,拒绝跳 /shell/forbidden
- 新增 instrumentation.ts:生产环境 DEV_MODE 强制 false
  防止生产环境误开 DEV_MODE 合成身份
- 新增 app/api/auth/login/route.ts + logout/route.ts
  登录走 api-gateway /v1/iam/login
  设置 httpOnly + Secure + SameSite=Strict cookie
- 新增 app/api/graphql/route.ts:同域 GraphQL 代理
  转发到 apollo-router,注入 router-authorization 头
- 新增 app/login/page.tsx + login-form.tsx
  zod 表单校验,next 参数支持
- 新增 app/shell/forbidden/page.tsx:403 页面
- 更新 route-permissions.ts:补全 P0 路由权限映射
- 更新 permission-bitmap.ts(shared-ts):位图编码/解码
- 更新 apollo-client.ts:DEV_MODE APQ 关闭,错误处理
- 更新 config-fetcher.ts:config-service 直连降级
- 更新 ApolloProvider.tsx:SSR/RSC 兼容
- 更新 eslint.config.js:design-tokens/no-hardcoded-fonts
  白名单调整
This commit is contained in:
SpecialX
2026-07-22 11:35:36 +08:00
parent 682f323bad
commit cfb7b005fd
23 changed files with 2542 additions and 435 deletions

View File

@@ -267,14 +267,44 @@ export const DASHBOARD_ROUTE_PERMISSIONS: Record<
*
* 用于 /api/* 路径的权限校验。
* 注意API Route 通常需要更严格的权限校验,因为它们直接操作数据。
*
* P0-2ARCHITECTURE.md §6.2 §3.4 V3-A3新增 auth/graphql 公开端点。
*/
export const API_ROUTE_PERMISSIONS: Record<string, RoutePermissionConfig> = {
// 错误上报端点:所有登录用户可访问
// 错误上报端点:所有登录用户可访问(空 config 表示登录即可)
"/api/log": {},
// 健康检查:公开
"/api/healthz": {},
"/api/health": {},
"/api/ready": {},
// 认证端点:公开(未登录也要能调登录接口)
"/api/auth/login": {},
"/api/auth/logout": {},
// GraphQL 同域代理:登录即可(细粒度由后端 resolver 把关)
"/api/graphql": {},
};
/**
* 5. 公共路由白名单P0-2ARCHITECTURE.md §6.2 §3.4 V3-A3 / §11.7 红线 #5
*
* 这些路由允许匿名访问(登录前/无身份也能访问)。
* middleware 对白名单路由跳过身份校验,直接放行。
*
* 注意:白名单外的路由,未登录访问 → middleware 重定向到 /login。
*/
export const PUBLIC_ROUTES: readonly string[] = [
"/",
"/login",
"/shell/forbidden",
"/api/health",
"/api/healthz",
"/api/ready",
"/api/log",
"/api/auth/login",
"/api/auth/logout",
"/api/graphql",
];
/**
* 校验权限配置的合法性(开发时辅助)
*
@@ -343,6 +373,11 @@ export function checkRoutePermission(
userBitmap: string,
userRole: Role,
): RoutePermissionResult {
// 0. 公共路由白名单优先(含 /shell/forbidden 自身,避免循环重定向)
if (PUBLIC_ROUTES.includes(pathname)) {
return { allowed: true, matchedPath: "PUBLIC" };
}
// 1. 匹配精确路由
const exactConfig = EXACT_ROUTE_PERMISSIONS[pathname];
if (exactConfig) {
@@ -368,14 +403,23 @@ export function checkRoutePermission(
if (apiConfig) {
return evaluateConfig(apiConfig, userBitmap, userRole, pathname);
}
// 未配置的 API 路由默认拒绝
// 未配置的 API 路由默认拒绝fail-closed§11.7 红线 #5
return {
allowed: false,
reason: "no_config",
};
}
// 5. 未匹配任何配置:默认放行(如 / /login /shell/forbidden 等公共路由
// 5. /shell/** 下未登记路由 → 默认拒绝fail-closed§3.4 V3-A3 / §11.7 红线 #5
// 防止"幽灵路由"绕过门禁;新增路由必须显式登记到 EXACT/PREFIX/DASHBOARD 表
if (pathname.startsWith("/shell/") || pathname === "/shell") {
return {
allowed: false,
reason: "no_config",
};
}
// 6. 其他未匹配路由(如 /favicon.ico / 静态资源)默认放行
return { allowed: true };
}