type RouteRule = { path: string; methods?: string[]; }; const API_ALLOWLIST: RouteRule[] = [ { path: "/api/auth/captcha", methods: ["GET"] }, { path: "/api/auth/login", methods: ["POST"] }, { path: "/api/auth/register", methods: ["POST"] }, /** 访客可读:无 Cookie 时不查库,用于客户端与 SSR 会话对齐 */ { path: "/api/auth/session", methods: ["GET"] }, { path: "/api/config/global", methods: ["GET"] }, { path: "/api/auth/oauth/:provider/callback", methods: ["GET"] }, { path: "/api/auth/oauth/:provider/authorize", methods: ["GET"] }, /** 卡片相关:ID 在路径参数中 */ { path: "/api/cards/:id", methods: ["GET"] }, { path: "/api/pic/random", methods: ["GET"] }, ]; /** 公开 API 以只读为主,需配合服务端校验与限流 */ export function isPublicApiPath(path: string, method?: string) { if (!path.startsWith("/api/public/")) { return false; } const requestMethod = method?.toUpperCase() ?? "GET"; if (requestMethod === "GET") { return true; } return false; } /** 前端页面白名单:无需登录即可直接访问 */ const FRONTEND_PAGE_ALLOWLIST: RouteRule[] = [ { path: "/auth/login" }, { path: "/auth/register" }, { path: "/" }, ]; /** * 检查前端页面是否在白名单中(允许未登录用户直接访问) * 已登录用户访问这些页面会被重定向 */ export function isFrontendPageAllowed(path: string): boolean { const cleanPath = path.split("?")[0]; return FRONTEND_PAGE_ALLOWLIST.some((rule) => { const regex = pathToRegexp(rule.path); return regex.test(cleanPath!); }); } /** * 将路由路径转换为正则表达式,支持 : * - `:id` 匹配单个路径段 * - `:id+` 匹配多个路径段 * - `:id?` 可选 */ function pathToRegexp(pattern: string): RegExp { // 只匹配 :param 部分(到下一个 / 或字符串结尾),不包括后续路径段 const escaped = pattern.replace(/:[^/]+(\?)?/g, (match) => { if (match.endsWith("?")) { return "([^/]*)"; } return "([^/]+)"; }); return new RegExp(`^${escaped}$`); } export function isAllowlistedApiPath(path: string, method?: string) { if (isPublicApiPath(path, method)) { return true; } const requestMethod = method?.toUpperCase() ?? "GET"; // 移除 query string const cleanPath = path.split("?")[0]; return API_ALLOWLIST.some((rule) => { const regex = pathToRegexp(rule.path); if (!regex.test(cleanPath!)) { return false; } if (!rule.methods || rule.methods.length === 0) { return true; } return rule.methods.includes(requestMethod); }); }