You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

86 lines
2.6 KiB

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);
});
}