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