diff --git a/.env.example b/.env.example
index a3428fc..873ff68 100644
--- a/.env.example
+++ b/.env.example
@@ -4,6 +4,8 @@ UPLOAD_SUBDIR=upload
NITRO_PORT=3399
SCHEDULER_MAX_CONCURRENCY=5
SCHEDULER_LOG_RETENTION_DAYS=30
+BOOTSTRAP_ADMIN_USERNAME=admin
+BOOTSTRAP_ADMIN_PASSWORD=123456qaz
GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret
APP_URL=http://localhost:3399
\ No newline at end of file
diff --git a/app/middleware/auth.global.ts b/app/middleware/auth.global.ts
index 1af5d8f..4500ed5 100644
--- a/app/middleware/auth.global.ts
+++ b/app/middleware/auth.global.ts
@@ -5,6 +5,7 @@ import {
normalizeSafeRedirect,
} from "@/utils/auth-routes";
import { useAuthSession } from "../composables/useAuthSession";
+import { FRONTEND_LOGIN_PATH } from "common/config"
export default defineNuxtRouteMiddleware(async (to) => {
if(to.path.startsWith("/__nuxt_error")) {
@@ -27,7 +28,7 @@ export default defineNuxtRouteMiddleware(async (to) => {
if (!isLoggedIn && !isPublicRoute(currentPath)) {
return navigateTo({
- path: "/auth",
+ path: FRONTEND_LOGIN_PATH,
query: { redirect: currentFullPath },
});
}
diff --git a/app/pages/admin.vue b/app/pages/admin.vue
index 10099ed..5a3936b 100644
--- a/app/pages/admin.vue
+++ b/app/pages/admin.vue
@@ -22,8 +22,6 @@ const adminNav: NavItem[] = [
icon: 'lucide:settings',
children: [
{ label: '用户管理', to: '/admin/users' },
- { label: '角色管理', to: '/admin/roles' },
- { label: '操作日志', to: '/admin/logs' },
]
},
]
diff --git a/app/pages/admin/users/index.vue b/app/pages/admin/users/index.vue
new file mode 100644
index 0000000..7f7bc23
--- /dev/null
+++ b/app/pages/admin/users/index.vue
@@ -0,0 +1,5 @@
+
+
+ dsada
+
+
\ No newline at end of file
diff --git a/app/pages/photo/index.vue b/app/pages/photo/index.vue
index 9b1d2a6..99f303e 100644
--- a/app/pages/photo/index.vue
+++ b/app/pages/photo/index.vue
@@ -10,7 +10,6 @@ interface CardItem {
aspectRatio: number
}
-const { capture, play } = useFlipAnimation()
const masonryEl = ref(null)
const allItems = ref([])
@@ -80,14 +79,8 @@ function updateColumns() {
else n = 5
if (n !== columnCount.value) {
- if (masonryEl.value) capture(masonryEl.value, '.card-reveal')
columnCount.value = n
distributeAll()
- nextTick(() => {
- if (masonryEl.value) {
- play(masonryEl.value, '.card-reveal', { duration: 420 })
- }
- })
}
}
diff --git a/app/utils/auth-routes.ts b/app/utils/auth-routes.ts
index 7d14bc7..3128324 100644
--- a/app/utils/auth-routes.ts
+++ b/app/utils/auth-routes.ts
@@ -1,17 +1,10 @@
-const PUBLIC_ROUTE_EXACT = new Set(["/", "/auth/login", "/auth/register"]);
-const GUEST_ONLY_ROUTE_EXACT = new Set(["/auth/login", "/auth/register"]);
+import { FRONTEND_PAGE_ALLOWLIST, FRONTEND_PAGE_GUEST_ONLY } from "common/config"
+import { normalizePath } from "common/utils/path"
+
const PUBLIC_ROUTE_PREFIXES: string[] = [];
export const DEFAULT_AUTHENTICATED_LANDING_PATH = "/";
-function normalizePath(path: string) {
- const trimmed = path.trim();
- if (!trimmed) {
- return "/";
- }
- return trimmed.length > 1 ? trimmed.replace(/\/+$/, "") : trimmed;
-}
-
function matchesExactOrPrefix(path: string, exact: Set, prefixes: string[]) {
const normalized = normalizePath(path);
if (exact.has(normalized)) {
@@ -21,11 +14,11 @@ function matchesExactOrPrefix(path: string, exact: Set, prefixes: string
}
export function isPublicRoute(path: string) {
- return matchesExactOrPrefix(path, PUBLIC_ROUTE_EXACT, PUBLIC_ROUTE_PREFIXES);
+ return matchesExactOrPrefix(path, FRONTEND_PAGE_ALLOWLIST, PUBLIC_ROUTE_PREFIXES);
}
export function isGuestOnlyRoute(path: string) {
- return GUEST_ONLY_ROUTE_EXACT.has(normalizePath(path));
+ return FRONTEND_PAGE_GUEST_ONLY.has(normalizePath(path));
}
export function normalizeSafeRedirect(
diff --git a/bun.lock b/bun.lock
index d3ea3e6..79cd509 100644
--- a/bun.lock
+++ b/bun.lock
@@ -9,6 +9,7 @@
"@nuxt/icon": "2.2.2",
"bcryptjs": "3.0.3",
"cache": "workspace:*",
+ "common": "workspace:*",
"croner": "10.0.1",
"dotenv": "17.4.1",
"drizzle-orm": "0.45.2",
@@ -20,7 +21,6 @@
"mime": "4.1.0",
"multer": "2.1.1",
"nuxt": "4.4.5",
- "oauth": "workspace:*",
"svg-captcha": "1.4.0",
"tailwindcss": "4.3.0",
"ufo": "1.6.3",
@@ -49,22 +49,15 @@
"@types/node": "20.0.0",
},
},
+ "packages/common": {
+ "name": "common",
+ },
"packages/drizzle-pkg": {
"name": "drizzle-pkg",
},
"packages/logger": {
"name": "logger",
},
- "packages/oauth": {
- "name": "oauth",
- "version": "0.1.0",
- "dependencies": {
- "zod": "4.3.6",
- },
- "devDependencies": {
- "@types/node": "20.0.0",
- },
- },
"packages/tsconfig": {
"name": "tsconfig",
},
@@ -750,6 +743,8 @@
"commander": ["commander@2.20.3", "https://registry.npmmirror.com/commander/-/commander-2.20.3.tgz", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
+ "common": ["common@workspace:packages/common"],
+
"commondir": ["commondir@1.0.1", "https://registry.npmmirror.com/commondir/-/commondir-1.0.1.tgz", {}, "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="],
"compatx": ["compatx@0.2.0", "https://registry.npmmirror.com/compatx/-/compatx-0.2.0.tgz", {}, "sha512-6gLRNt4ygsi5NyMVhceOCFv14CIdDFN7fQjX1U4+47qVE/+kjPoXMK65KWK+dWxmFzMTuKazoQ9sch6pM0p5oA=="],
@@ -1206,8 +1201,6 @@
"nypm": ["nypm@0.6.6", "https://registry.npmmirror.com/nypm/-/nypm-0.6.6.tgz", { "dependencies": { "citty": "^0.2.2", "pathe": "^2.0.3", "tinyexec": "^1.1.1" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-vRyr0r4cbBapw07Xw8xrj9Teq3o7MUD35rSaTcanDbW+aK2XHDgJFiU6ZTj2GBw7Q12ysdsyFss+Vdz4hQ0Y6Q=="],
- "oauth": ["oauth@workspace:packages/oauth"],
-
"obug": ["obug@2.1.1", "https://registry.npmmirror.com/obug/-/obug-2.1.1.tgz", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
"ofetch": ["ofetch@1.5.1", "https://registry.npmmirror.com/ofetch/-/ofetch-1.5.1.tgz", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="],
@@ -1854,8 +1847,6 @@
"nuxt/vue-router": ["vue-router@5.0.7", "https://registry.npmmirror.com/vue-router/-/vue-router-5.0.7.tgz", { "dependencies": { "@babel/generator": "^8.0.0-rc.4", "@vue-macros/common": "^3.1.1", "@vue/devtools-api": "^8.1.1", "ast-walker-scope": "^0.8.3", "chokidar": "^5.0.0", "json5": "^2.2.3", "local-pkg": "^1.1.2", "magic-string": "^0.30.21", "mlly": "^1.8.0", "muggle-string": "^0.4.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "scule": "^1.3.0", "tinyglobby": "^0.2.15", "unplugin": "^3.0.0", "unplugin-utils": "^0.3.1", "yaml": "^2.8.2" }, "peerDependencies": { "@pinia/colada": ">=0.21.2", "@vue/compiler-sfc": "^3.5.34", "pinia": "^3.0.4", "vue": "^3.5.34" }, "optionalPeers": ["@pinia/colada", "@vue/compiler-sfc", "pinia"] }, "sha512-dqfk8kvRbCutmCOCj/XLDqDEYxc1wBdAOGLuVy5M93ifYMsBd5fIjfaPN4tQAbxr5IprdBDIox1gr4wYyOx/SA=="],
- "oauth/@types/node": ["@types/node@20.0.0", "https://registry.npmmirror.com/@types/node/-/node-20.0.0.tgz", {}, "sha512-cD2uPTDnQQCVpmRefonO98/PPijuOnnEy5oytWJFPY1N9aJCz2wJ5kSGWO+zJoed2cY2JxQh6yBuUq4vIn61hw=="],
-
"postcss-colormin/postcss": ["postcss@8.5.14", "https://registry.npmmirror.com/postcss/-/postcss-8.5.14.tgz", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg=="],
"postcss-convert-values/postcss": ["postcss@8.5.14", "https://registry.npmmirror.com/postcss/-/postcss-8.5.14.tgz", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg=="],
diff --git a/nuxt.config.ts b/nuxt.config.ts
index e552119..bfba82a 100644
--- a/nuxt.config.ts
+++ b/nuxt.config.ts
@@ -18,11 +18,25 @@ export default defineNuxtConfig({
devtools: { enabled: true },
vite: {
+ optimizeDeps: {
+ include: [
+ 'vue3-toastify',
+ ]
+ },
plugins: [
tailwindcss(),
]
},
-
+ typescript: {
+ tsConfig: {
+ compilerOptions: {
+ ignoreDeprecations: "6.0",
+ paths: {
+ 'common': ['./packages/common']
+ },
+ }
+ }
+ },
nitro: {
typescript: {
tsConfig: {
@@ -31,8 +45,10 @@ export default defineNuxtConfig({
resolvePackageJsonImports: true,
ignoreDeprecations: "6.0",
paths: {
- 'drizzle-pkg': ['./packages/drizzle-pkg/lib'],
- 'logger': ['./packages/logger/lib']
+ 'drizzle-pkg': ['./packages/drizzle-pkg'],
+ 'cache': ['./packages/cache'],
+ 'common': ['./packages/common'],
+ 'logger': ['./packages/logger']
},
}
},
diff --git a/package.json b/package.json
index 17d3a5d..73a9244 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,7 @@
"@nuxt/icon": "2.2.2",
"bcryptjs": "3.0.3",
"cache": "workspace:*",
+ "common": "workspace:*",
"croner": "10.0.1",
"dotenv": "17.4.1",
"drizzle-orm": "0.45.2",
diff --git a/packages/common/config/index.ts b/packages/common/config/index.ts
new file mode 100644
index 0000000..c342971
--- /dev/null
+++ b/packages/common/config/index.ts
@@ -0,0 +1,23 @@
+export type RouteRule = {
+ path: string;
+ methods?: string[];
+};
+
+export 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"] },
+];
+
+export const FRONTEND_LOGIN_PATH = "/auth/login"
+export const FRONTEND_REGISTER_PATH = "/auth/register"
+export const FRONTEND_PAGE_ALLOWLIST = new Set(["/", FRONTEND_LOGIN_PATH, FRONTEND_REGISTER_PATH])
+export const FRONTEND_PAGE_GUEST_ONLY = new Set([FRONTEND_LOGIN_PATH, FRONTEND_REGISTER_PATH])
diff --git a/packages/common/package.json b/packages/common/package.json
new file mode 100644
index 0000000..ea471c6
--- /dev/null
+++ b/packages/common/package.json
@@ -0,0 +1,4 @@
+{
+ "name": "common",
+ "sideEffects": false
+}
\ No newline at end of file
diff --git a/packages/common/tsconfig.json b/packages/common/tsconfig.json
new file mode 100644
index 0000000..b473b69
--- /dev/null
+++ b/packages/common/tsconfig.json
@@ -0,0 +1,3 @@
+{
+ "extends": "tsconfig/tsconfig.json"
+}
\ No newline at end of file
diff --git a/packages/common/utils/path.ts b/packages/common/utils/path.ts
new file mode 100644
index 0000000..c34a8e1
--- /dev/null
+++ b/packages/common/utils/path.ts
@@ -0,0 +1,24 @@
+/**
+ * 将路由路径转换为正则表达式,支持 :
+ * - `:id` 匹配单个路径段
+ * - `:id+` 匹配多个路径段
+ * - `:id?` 可选
+ */
+export function pathToRegexp(pattern: string): RegExp {
+ // 只匹配 :param 部分(到下一个 / 或字符串结尾),不包括后续路径段
+ const escaped = pattern.replace(/:[^/]+(\?)?/g, (match) => {
+ if (match.endsWith("?")) {
+ return "([^/]*)";
+ }
+ return "([^/]+)";
+ });
+ return new RegExp(`^${escaped}$`);
+}
+
+export function normalizePath(path: string) {
+ const trimmed = path.trim();
+ if (!trimmed) {
+ return "/";
+ }
+ return trimmed.length > 1 ? trimmed.replace(/\/+$/, "") : trimmed;
+}
diff --git a/packages/drizzle-pkg/db.sqlite b/packages/drizzle-pkg/db.sqlite
index a2946a0..6499ddc 100644
Binary files a/packages/drizzle-pkg/db.sqlite and b/packages/drizzle-pkg/db.sqlite differ
diff --git a/server/api/auth/oauth/[provider]/callback.get.ts b/server/api/auth/oauth/[provider]/callback.get.ts
index 5207fa0..bba13d4 100644
--- a/server/api/auth/oauth/[provider]/callback.get.ts
+++ b/server/api/auth/oauth/[provider]/callback.get.ts
@@ -1,6 +1,7 @@
import { oauthManager } from '#server/service/oauth/oauth-manager';
import { setSessionCookie } from '#server/service/auth/cookie';
import { OAuthError } from '#server/service/oauth/oauth-error';
+import { FRONTEND_LOGIN_PATH } from "common/config"
export default defineWrappedResponseHandler(async (event) => {
const providerName = getRouterParam(event, 'provider');
@@ -9,7 +10,7 @@ export default defineWrappedResponseHandler(async (event) => {
const { code, state } = query as { code?: string; state?: string };
if (!code || !state) {
- return sendRedirect(event, '/auth/login?oauth_error=missing_params');
+ return sendRedirect(event, `${FRONTEND_LOGIN_PATH}?oauth_error=missing_params`);
}
try {
@@ -19,9 +20,9 @@ export default defineWrappedResponseHandler(async (event) => {
setSessionCookie(event, result.sessionId);
}
- return sendRedirect(event, '/auth/login?oauth_success=1');
+ return sendRedirect(event, `${FRONTEND_LOGIN_PATH}??oauth_success=1`);
} catch (error) {
const errorCode = error instanceof OAuthError ? error.code : 'OAUTH_UNKNOWN';
- return sendRedirect(event, `/auth/login?oauth_error=${errorCode}`);
+ return sendRedirect(event, `${FRONTEND_LOGIN_PATH}??oauth_error=${errorCode}`);
}
});
\ No newline at end of file
diff --git a/server/middleware/02.auth-guard.ts b/server/middleware/02.auth-guard.ts
index 1b840e0..c90ef93 100644
--- a/server/middleware/02.auth-guard.ts
+++ b/server/middleware/02.auth-guard.ts
@@ -1,6 +1,8 @@
import { UNAUTHORIZED_MESSAGE } from "#server/constants/auth";
import { isAllowlistedApiPath, isFrontendPageAllowed } from "#server/utils/auth-api-routes";
import { getCurrentUser } from "#server/utils/context";
+import { FRONTEND_LOGIN_PATH, FRONTEND_PAGE_GUEST_ONLY } from "common/config"
+import { normalizePath } from "common/utils/path";
export default eventHandler(async (event) => {
const path = event.path;
@@ -35,11 +37,11 @@ export default eventHandler(async (event) => {
const user = await getCurrentUser(event);
if (!user) {
// 未登录且页面不在白名单,重定向到登录页
- return sendRedirect(event, "/auth/login", 302);
+ return sendRedirect(event, FRONTEND_LOGIN_PATH, 302);
}
// 已登录用户访问登录/注册页面,重定向到首页
- if (path.startsWith("/auth/login") || path.startsWith("/auth/register")) {
+ if (FRONTEND_PAGE_GUEST_ONLY.has(normalizePath(path))) {
return sendRedirect(event, "/", 302);
}
});
\ No newline at end of file
diff --git a/server/service/oauth/oauth-manager.ts b/server/service/oauth/oauth-manager.ts
index 1a4bb14..2554e02 100644
--- a/server/service/oauth/oauth-manager.ts
+++ b/server/service/oauth/oauth-manager.ts
@@ -6,6 +6,7 @@ import { OAuthStateStore, oauthStateStore } from './oauth-state-store';
import { OAuthError, OAuthErrorCodes } from './oauth-error';
import { registerUser, createSession } from '#server/service/auth';
import type { MinimalUser } from '#server/service/auth';
+import { FRONTEND_LOGIN_PATH } from "common/config"
const logger = log4js.getLogger("OAUTH");
@@ -28,6 +29,14 @@ export type OAuthCallbackResult = {
expiresAt?: Date;
};
+export type FieldMapping = {
+ provider?: string;
+ providerUserId: string;
+ username?: string;
+ email?: string;
+ avatar?: string;
+};
+
export type OAuthProviderConfig = {
name: string;
icon: string;
@@ -38,13 +47,7 @@ export type OAuthProviderConfig = {
userInfoUrl: string;
scopes: string[];
redirectUri: string;
- mapUserInfo: (raw: Record) => {
- provider: string;
- providerUserId: string;
- username?: string;
- email?: string;
- avatar?: string;
- };
+ userInfoMapping: FieldMapping;
};
const providers: Record = {
@@ -58,14 +61,11 @@ const providers: Record = {
userInfoUrl: 'https://api.github.com/user',
scopes: ['read:user', 'user:email'],
redirectUri: `${process.env.APP_URL}/api/auth/oauth/github/callback`,
- mapUserInfo(raw: Record) {
- return {
- provider: 'github',
- providerUserId: String(raw.id ?? ''),
- username: String(raw.login ?? ''),
- email: String(raw.email ?? ''),
- avatar: String(raw.avatar_url ?? ''),
- };
+ userInfoMapping: {
+ providerUserId: 'id',
+ username: 'login',
+ email: 'email',
+ avatar: 'avatar_url',
},
},
};
@@ -89,7 +89,7 @@ export class OAuthManager {
const redirectUri = userId
? `/profile?bind_success=1`
- : `/auth/login?oauth_success=1`;
+ : `${FRONTEND_LOGIN_PATH}?oauth_success=1`;
const { state } = this.stateStore.generate(providerName, redirectUri, userId);
@@ -283,6 +283,21 @@ export class OAuthManager {
}>;
}
+ private mapUserInfo(raw: Record, mapping: FieldMapping) {
+ const getValue = (path: string) => {
+ const value = (raw as Record)[path];
+ return value != null ? String(value) : undefined;
+ };
+
+ return {
+ provider: mapping.provider || '',
+ providerUserId: getValue(mapping.providerUserId) || '',
+ username: mapping.username ? getValue(mapping.username) : undefined,
+ email: mapping.email ? getValue(mapping.email) : undefined,
+ avatar: mapping.avatar ? getValue(mapping.avatar) : undefined,
+ };
+ }
+
private async getUserInfo(
provider: OAuthProviderConfig,
accessToken: string
@@ -299,7 +314,7 @@ export class OAuthManager {
}
const raw = await response.json() as Record;
- return provider.mapUserInfo(raw);
+ return this.mapUserInfo(raw, provider.userInfoMapping);
}
private async createOAuthAccount(data: {
diff --git a/server/utils/auth-api-routes.ts b/server/utils/auth-api-routes.ts
index d3ebc30..db2b457 100644
--- a/server/utils/auth-api-routes.ts
+++ b/server/utils/auth-api-routes.ts
@@ -1,21 +1,5 @@
-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"] },
-];
+import { API_ALLOWLIST, FRONTEND_PAGE_ALLOWLIST } from "common/config"
+import { pathToRegexp } from "common/utils/path"
/** 公开 API 以只读为主,需配合服务端校验与限流 */
export function isPublicApiPath(path: string, method?: string) {
@@ -29,42 +13,18 @@ export function isPublicApiPath(path: string, method?: string) {
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 Array.from(FRONTEND_PAGE_ALLOWLIST).some((rule) => {
+ const regex = pathToRegexp(rule);
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;