From 56c3f32e2d84f236f9779f67702d93404cfd9f4c Mon Sep 17 00:00:00 2001 From: SpecialX <47072643+wangxiner55@users.noreply.github.com> Date: Fri, 3 Jul 2026 10:30:28 +0800 Subject: [PATCH] chore(config): update auth, env, i18n, proxy, and dependencies - Update src/auth.ts auth configuration - Update src/env.mjs environment variable validation - Update src/i18n/request.ts locale handling - Update src/proxy.ts middleware - Update src/next-auth.d.ts type declarations - Update package.json and package-lock.json dependencies --- package-lock.json | 626 +++++++++++++++++++++++++++++++++++++++++++- package.json | 1 + src/auth.ts | 201 +++++--------- src/env.mjs | 11 + src/i18n/request.ts | 12 + src/next-auth.d.ts | 27 +- src/proxy.ts | 76 ++---- 7 files changed, 755 insertions(+), 199 deletions(-) diff --git a/package-lock.json b/package-lock.json index 15041b3..8dd4b4e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -80,6 +80,7 @@ "@playwright/test": "^1.58.2", "@tailwindcss/postcss": "^4", "@tailwindcss/typography": "^0.5.16", + "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@types/bcryptjs": "^2.4.6", @@ -3301,7 +3302,7 @@ "version": "1.58.2", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "playwright": "1.58.2" @@ -6596,6 +6597,36 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, "node_modules/@testing-library/jest-dom": { "version": "6.9.1", "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", @@ -7109,6 +7140,13 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/bcryptjs": { "version": "2.4.6", "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", @@ -7348,7 +7386,6 @@ "version": "19.2.7", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", - "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -7358,7 +7395,6 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" @@ -9168,7 +9204,6 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, "license": "MIT" }, "node_modules/d3-array": { @@ -9608,6 +9643,13 @@ "node": ">=0.10.0" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, "node_modules/dotenv": { "version": "17.2.3", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", @@ -12861,6 +12903,16 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -14179,6 +14231,17 @@ } } }, + "node_modules/next-intl/node_modules/@swc/helpers": { + "version": "0.5.23", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", + "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/next-themes": { "version": "0.4.6", "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", @@ -14760,7 +14823,7 @@ "version": "1.58.2", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "playwright-core": "1.58.2" @@ -14779,7 +14842,7 @@ "version": "1.58.2", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" @@ -14980,6 +15043,41 @@ } } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -15301,7 +15399,6 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true, "license": "MIT" }, "node_modules/react-markdown": { @@ -16566,7 +16663,6 @@ "version": "4.1.18", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", - "dev": true, "license": "MIT" }, "node_modules/tailwindcss-animate": { @@ -17015,7 +17111,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -17528,6 +17624,474 @@ } } }, + "node_modules/vitest/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, "node_modules/vitest/node_modules/@vitest/mocker": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.0.tgz", @@ -17555,6 +18119,50 @@ } } }, + "node_modules/vitest/node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "peer": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, "node_modules/vitest/node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", diff --git a/package.json b/package.json index 5ff1a6b..43f9139 100644 --- a/package.json +++ b/package.json @@ -112,6 +112,7 @@ "@playwright/test": "^1.58.2", "@tailwindcss/postcss": "^4", "@tailwindcss/typography": "^0.5.16", + "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@types/bcryptjs": "^2.4.6", diff --git a/src/auth.ts b/src/auth.ts index a6dec2e..74db234 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -1,23 +1,14 @@ -import { compare } from "bcryptjs" import NextAuth from "next-auth" import Credentials from "next-auth/providers/credentials" -import { eq } from "drizzle-orm" import { resolvePermissions } from "@/shared/lib/permissions" import { isRole } from "@/shared/types/permissions" import { logLoginEvent } from "@/shared/lib/login-logger" -import { - PASSWORD_RULES, - isAccountLocked, -} from "@/shared/lib/password-policy" -import { RATE_LIMIT_RULES, rateLimit, rateLimitKey, resetRateLimit } from "@/shared/lib/rate-limit" -import { normalizeBcryptHash } from "@/shared/lib/bcrypt-utils" -import { resolveClientIp } from "@/shared/lib/http-utils" -import { - getOrCreatePasswordSecurity, - recordFailedLogin, - resetFailedLogin, -} from "@/shared/lib/password-security-service" import { normalizeRole, resolvePrimaryRole } from "@/shared/lib/role-utils" +import { + encodePermissionsBitmap, + decodePermissionsBitmap, +} from "@/shared/lib/permission-bitmap" +import { trackAuthEvent } from "@/shared/lib/track-event" export const { handlers, auth, signIn, signOut } = NextAuth({ trustHost: true, @@ -31,126 +22,32 @@ export const { handlers, auth, signIn, signOut } = NextAuth({ password: { label: "Password", type: "password" }, totpCode: { label: "2FA Code", type: "text" }, }, + // audit-P1-3 重构:authorize 回调原混合 7 类职责(107 行), + // 已抽取至 modules/auth/services/login-service.ts 的 authenticateUser 函数。 + // 此处为薄包装,仅负责动态 import(避免 server-only 模块在模块评估期加载) + // 与委托调用。所有登录逻辑(速率限制/账户锁定/密码校验/2FA/角色加载)均在 + // login-service 中实现,可独立测试。 authorize: async (credentials) => { - const email = String(credentials?.email ?? "").trim().toLowerCase() - const password = String(credentials?.password ?? "") - const totpCode = String(credentials?.totpCode ?? "").trim() - if (!email || !password) return null - - // Rate limit by IP + email to slow brute-force attempts - const clientIp = await resolveClientIp() - const loginLimitKey = rateLimitKey("login", `${clientIp}:${email}`) - const limit = rateLimit({ - key: loginLimitKey, - ...RATE_LIMIT_RULES.LOGIN, - }) - if (!limit.success) { - await logLoginEvent({ - userEmail: email, - action: "signin", - status: "failure", - errorMessage: "Rate limit exceeded", - }) - return null - } - - const [{ db }, { users, roles, usersToRoles, passwordSecurity }] = await Promise.all([ - import("@/shared/db"), - import("@/shared/db/schema"), - ]) - - const user = await db.query.users.findFirst({ - where: eq(users.email, email), - }) - if (!user) return null - - // Account lockout check - const security = await getOrCreatePasswordSecurity(db, passwordSecurity, user.id) - const lastFailedAt = security.lockedUntil - ? new Date(security.lockedUntil.getTime() - PASSWORD_RULES.lockoutDurationMinutes * 60 * 1000) - : null - if (isAccountLocked(security.failedLoginAttempts, lastFailedAt)) { - await logLoginEvent({ - userId: user.id, - userEmail: email, - action: "signin", - status: "failure", - errorMessage: "Account locked", - }) - return null - } - - const storedPassword = user.password ?? null - if (!storedPassword) return null - const normalizedPassword = normalizeBcryptHash(storedPassword) - if (!normalizedPassword.startsWith("$2")) return null - const ok = await compare(password, normalizedPassword) - - if (!ok) { - await recordFailedLogin(db, passwordSecurity, user.id) - await logLoginEvent({ - userId: user.id, - userEmail: email, - action: "signin", - status: "failure", - errorMessage: "Invalid credentials", - }) - return null - } - - // Successful login: reset counters and rate limit - await resetFailedLogin(db, passwordSecurity, user.id) - resetRateLimit(loginLimitKey) - - // 2FA verification (if user has enabled it) - const { verifyTwoFactorForLogin } = await import("@/modules/settings/actions-security") - const twoFactorResult = await verifyTwoFactorForLogin({ - userId: user.id, - token: totpCode || undefined, - }) - if (twoFactorResult.required && !twoFactorResult.valid) { - await logLoginEvent({ - userId: user.id, - userEmail: email, - action: "signin", - status: "failure", - errorMessage: totpCode - ? "Invalid 2FA code" - : "2FA required but not provided", - }) - return null - } - - const roleRows = await db - .select({ name: roles.name }) - .from(usersToRoles) - .innerJoin(roles, eq(usersToRoles.roleId, roles.id)) - .where(eq(usersToRoles.userId, user.id)) - - const roleNames = roleRows.map((r) => r.name) - const resolvedRole = resolvePrimaryRole(roleNames) - - return { - id: user.id, - name: user.name ?? undefined, - email: user.email, - role: resolvedRole, - roles: roleNames, - } + const { authenticateUser } = await import("@/modules/auth/services/login-service") + return authenticateUser(credentials) }, }), ], callbacks: { jwt: async ({ token, user }) => { - if (user) { - const u = user as { id: string; role?: string; roles?: string[]; name?: string } - token.id = u.id - token.role = normalizeRole(u.role) - token.name = u.name ?? undefined + // P0-5 修复:User 接口已在 next-auth.d.ts 中扩展了 role/roles 字段, + // 无需 `as` 断言即可安全访问。 + if (user && user.id) { + token.id = user.id + token.role = normalizeRole(user.role) + token.name = user.name ?? undefined // Store all roles (not just primary) and resolved permissions - const allRoles = (u.roles ?? [u.role ?? "student"]).filter(isRole) + const allRoles = (user.roles ?? [user.role ?? "student"]).filter(isRole) token.roles = allRoles - token.permissions = resolvePermissions(allRoles) + // audit-P1-7:JWT 中仅存位图(~14 字符),不存数组(~1.1KB)。 + // Session callback 解码还原为 Permission[] 供业务使用。 + const permissions = await resolvePermissions(allRoles) + token.permissionsBitmap = encodePermissionsBitmap(permissions) // Onboarding status is resolved from DB below; default to false on first login token.onboarded = false } @@ -181,7 +78,9 @@ export const { handlers, auth, signIn, signOut } = NextAuth({ token.role = resolvePrimaryRole(allRoles) token.name = fresh.name ?? token.name token.roles = allRoles - token.permissions = resolvePermissions(allRoles) + // audit-P1-7:刷新位图,不再写 token.permissions 数组 + const permissions = await resolvePermissions(allRoles) + token.permissionsBitmap = encodePermissionsBitmap(permissions) token.onboarded = Boolean(fresh.onboardedAt) } } @@ -189,11 +88,15 @@ export const { handlers, auth, signIn, signOut } = NextAuth({ return token }, session: async ({ session, token }) => { + // audit-P1-7:session.user.permissions 由位图解码得到, + // 业务代码(getAuthContext / usePermission)无需感知位图存在。 if (session.user) { session.user.id = String(token.id ?? "") session.user.role = normalizeRole(token.role) session.user.roles = (token.roles ?? []).filter(isRole) - session.user.permissions = (token.permissions ?? []) as typeof token.permissions + session.user.permissions = decodePermissionsBitmap( + token.permissionsBitmap ?? "", + ) session.user.onboarded = Boolean(token.onboarded) if (typeof token.name === "string") { session.user.name = token.name @@ -210,17 +113,34 @@ export const { handlers, auth, signIn, signOut } = NextAuth({ action: "signin", status: "success", }) + // audit-P1-9:登录成功埋点(用于登录成功率、异常登录地理/设备告警) + // 非阻塞:trackEvent 内部已吞掉异常 + await trackAuthEvent("auth.signin_success", { + userId: user.id, + properties: { + // user.role 是单一主角色,user.roles 是全部角色(可能未填充) + roles: (user.roles ?? (user.role ? [user.role] : [])), + }, + }) }, async signOut(message) { - // NextAuth v5 signOut event receives the session/token info - const userId = - (message as { userId?: string })?.userId ?? - (message as { token?: { id?: string } })?.token?.id ?? - "" - const userEmail = - (message as { token?: { email?: string } })?.token?.email ?? - (message as { session?: { user?: { email?: string } } })?.session?.user?.email ?? - "" + // P0-5 修复:使用 `in` 操作符对联合类型进行类型收窄,替代 `as` 断言。 + // NextAuth v5 signOut 事件 message 为联合类型: + // { session: ... } | { token: JWT } + // JWT 策略下实际收到 { token: JWT },其中 JWT 已在 next-auth.d.ts 扩展。 + let userId = "" + let userEmail = "" + let roles: string[] = [] + + if ("token" in message) { + const token = message.token + if (token) { + userId = String(token.id ?? "") + userEmail = String(token.email ?? "") + roles = (token.roles ?? []).filter(isRole) + } + } + if (userEmail) { await logLoginEvent({ userId: userId || undefined, @@ -229,6 +149,13 @@ export const { handlers, auth, signIn, signOut } = NextAuth({ status: "success", }) } + // audit-P1-9:登出埋点 + if (userId) { + await trackAuthEvent("auth.signout", { + userId, + properties: { roles }, + }) + } }, }, }) diff --git a/src/env.mjs b/src/env.mjs index 6533268..f8346d4 100644 --- a/src/env.mjs +++ b/src/env.mjs @@ -10,6 +10,13 @@ export const env = createEnv({ AI_API_KEY: z.string().min(1).optional(), AI_BASE_URL: z.string().url().optional(), AI_MODEL: z.string().min(1).optional(), + // Rate limit driver: "memory" (default, single-instance) or "redis" (distributed). + RATE_LIMIT_DRIVER: z.enum(["memory", "redis"]).default("memory"), + // Upstash Redis REST credentials (only required when RATE_LIMIT_DRIVER=redis). + UPSTASH_REDIS_REST_URL: z.string().url().optional(), + UPSTASH_REDIS_REST_TOKEN: z.string().min(1).optional(), + // audit-P2-5: Cron job bearer token for /api/cron/* endpoints. + CRON_SECRET: z.string().min(1).optional(), }, client: { NEXT_PUBLIC_APP_URL: z.string().url().optional(), @@ -23,6 +30,10 @@ export const env = createEnv({ AI_API_KEY: process.env.AI_API_KEY, AI_BASE_URL: process.env.AI_BASE_URL, AI_MODEL: process.env.AI_MODEL, + RATE_LIMIT_DRIVER: process.env.RATE_LIMIT_DRIVER, + UPSTASH_REDIS_REST_URL: process.env.UPSTASH_REDIS_REST_URL, + UPSTASH_REDIS_REST_TOKEN: process.env.UPSTASH_REDIS_REST_TOKEN, + CRON_SECRET: process.env.CRON_SECRET, }, skipValidation: !!process.env.SKIP_ENV_VALIDATION, emptyStringAsUndefined: true, diff --git a/src/i18n/request.ts b/src/i18n/request.ts index 06b7b0d..8f1273e 100644 --- a/src/i18n/request.ts +++ b/src/i18n/request.ts @@ -50,6 +50,10 @@ export default getRequestConfig(async () => { users, leave, nav, + rbac, + questions, + parent, + invitationCodes, ] = await Promise.all([ import(`@/shared/i18n/messages/${locale}/common.json`), import(`@/shared/i18n/messages/${locale}/auth.json`), @@ -81,6 +85,10 @@ export default getRequestConfig(async () => { import(`@/shared/i18n/messages/${locale}/users.json`), import(`@/shared/i18n/messages/${locale}/leave.json`), import(`@/shared/i18n/messages/${locale}/nav.json`), + import(`@/shared/i18n/messages/${locale}/rbac.json`), + import(`@/shared/i18n/messages/${locale}/questions.json`), + import(`@/shared/i18n/messages/${locale}/parent.json`), + import(`@/shared/i18n/messages/${locale}/invitation-codes.json`), ]); return { @@ -116,6 +124,10 @@ export default getRequestConfig(async () => { users: users.default, leave: leave.default, nav: nav.default, + rbac: rbac.default, + questions: questions.default, + parent: parent.default, + invitationCodes: invitationCodes.default, }, }; }); diff --git a/src/next-auth.d.ts b/src/next-auth.d.ts index 19e42e7..f936aa3 100644 --- a/src/next-auth.d.ts +++ b/src/next-auth.d.ts @@ -2,6 +2,12 @@ import type { DefaultSession } from "next-auth" import type { Permission, Role } from "@/shared/types/permissions" declare module "next-auth" { + interface User { + /** 主要角色(向后兼容) */ + role?: string + /** 用户拥有的全部角色名 */ + roles?: string[] + } interface Session { user: DefaultSession["user"] & { id: string @@ -18,7 +24,26 @@ declare module "next-auth/jwt" { id: string role: string // kept for backward compatibility roles: Role[] - permissions: Permission[] + /** + * 权限位图(base36 字符串,audit-P1-7)。 + * + * 由 `encodePermissionsBitmap()` 编码生成,约 14 字符, + * 相比 `permissions: Permission[]` JSON 数组(~1.1KB)显著减小 JWT 体积。 + * + * proxy.ts 边缘运行时直接通过 `hasPermissionInBitmap()` 检查权限, + * 无需解码为完整数组。 + * + * Session callback 中通过 `decodePermissionsBitmap()` 还原为 Permission[] + * 供服务端 `getAuthContext()` 和客户端 `usePermission()` 使用。 + */ + permissionsBitmap: string + /** + * @deprecated 仅用于向后兼容,新代码应使用 `permissionsBitmap`。 + * Session callback 已自动从 bitmap 解码并填充到 `session.user.permissions`, + * 业务代码无需直接读取 `token.permissions`。 + * 在 JWT 中保留此字段会导致 token 体积膨胀,未来版本将移除。 + */ + permissions?: Permission[] onboarded: boolean } } diff --git a/src/proxy.ts b/src/proxy.ts index f8431d7..59c3354 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -2,51 +2,15 @@ import { NextResponse } from "next/server" import type { NextRequest } from "next/server" import { getToken } from "next-auth/jwt" -import { Permissions } from "@/shared/types/permissions" - -// Route prefix → minimum required permission -// Note: /admin/announcements is covered by /admin prefix (requires school:manage) -// Note: /announcements is accessible to all authenticated users (no permission entry needed) -// P0 修复:原先 /teacher 和 /parent 都使用 EXAM_READ,但 student/parent 也有 EXAM_READ, -// 导致跨角色访问漏洞(学生可访问 /teacher/*,教师可访问 /parent/*)。 -// 改为使用各角色独有的权限点,确保跨角色访问被拒绝。 -const ROUTE_PERMISSIONS: Record = { - "/admin": Permissions.SCHOOL_MANAGE, - "/teacher": Permissions.EXAM_CREATE, - "/student": Permissions.HOMEWORK_SUBMIT, - "/parent": Permissions.DASHBOARD_PARENT_READ, - "/management": Permissions.GRADE_MANAGE, -} - -// 仪表盘路由的细粒度权限(覆盖 ROUTE_PERMISSIONS 的前缀匹配) -// 防止拥有 EXAM_READ 的学生/家长访问 /teacher/dashboard 等 -const DASHBOARD_ROUTE_PERMISSIONS: Record = { - "/admin/dashboard": Permissions.DASHBOARD_ADMIN_READ, - "/teacher/dashboard": Permissions.DASHBOARD_TEACHER_READ, - "/student/dashboard": Permissions.DASHBOARD_STUDENT_READ, - "/parent/dashboard": Permissions.DASHBOARD_PARENT_READ, -} - -// 精确路由权限(优先级最高,覆盖 ROUTE_PERMISSIONS 的前缀匹配) -// 用于将 /admin/* 下的特定页面开放给非管理员角色 -// V3.1:/admin/ai-settings 对所有 AI_CHAT 用户开放(管理自己的 private provider) -const SPECIFIC_ROUTE_PERMISSIONS: Record = { - "/admin/ai-settings": Permissions.AI_CHAT, -} - -// API route prefix → required permission -const API_PERMISSIONS: Record = { - "/api/ai/chat": Permissions.AI_CHAT, -} - -function resolveDefaultPath(roles: string[]): string { - if (roles.includes("admin")) return "/admin/dashboard" - if (roles.includes("grade_head") || roles.includes("teaching_head")) return "/teacher/dashboard" - if (roles.includes("teacher")) return "/teacher/dashboard" - if (roles.includes("student")) return "/student/dashboard" - if (roles.includes("parent")) return "/parent/dashboard" - return "/dashboard" -} +import { type Permission } from "@/shared/types/permissions" +import { resolveDefaultPath } from "@/shared/lib/route-resolver" +import { hasPermissionInBitmap } from "@/shared/lib/permission-bitmap" +import { + SPECIFIC_ROUTE_PERMISSIONS, + ROUTE_PREFIX_PERMISSIONS, + DASHBOARD_ROUTE_PERMISSIONS, + API_ROUTE_PERMISSIONS, +} from "@/shared/lib/route-permissions" // Next.js 16 renamed `middleware` to `proxy`. // See: https://nextjs.org/docs/messages/middleware-to-proxy @@ -92,13 +56,21 @@ export async function proxy(request: NextRequest) { return NextResponse.redirect(new URL(defaultPath, request.url)) } - const permissions: string[] = (token.permissions as string[]) ?? [] + const permissionsBitmap: string = (token.permissionsBitmap as string) ?? "" const roles: string[] = (token.roles as string[]) ?? [] + /** + * audit-P1-7:使用位图检查权限,避免每次路由检查都解码完整权限数组。 + * proxy.ts 在 edge runtime 运行,每个请求都经过这里,性能至关重要。 + */ + function hasPermission(requiredPerm: Permission): boolean { + return hasPermissionInBitmap(permissionsBitmap, requiredPerm) + } + // Check API route permissions - for (const [prefix, requiredPerm] of Object.entries(API_PERMISSIONS)) { + for (const [prefix, requiredPerm] of Object.entries(API_ROUTE_PERMISSIONS)) { if (pathname.startsWith(prefix)) { - if (!permissions.includes(requiredPerm)) { + if (!hasPermission(requiredPerm)) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }) } break @@ -109,7 +81,7 @@ export async function proxy(request: NextRequest) { // 优先级 1:精确路由权限(覆盖前缀匹配,用于将 /admin/* 下特定页面开放给非管理员) if (Object.prototype.hasOwnProperty.call(SPECIFIC_ROUTE_PERMISSIONS, pathname)) { const requiredPerm = SPECIFIC_ROUTE_PERMISSIONS[pathname] - if (!permissions.includes(requiredPerm)) { + if (!hasPermission(requiredPerm)) { const defaultPath = resolveDefaultPath(roles) const redirectUrl = new URL(defaultPath, request.url) redirectUrl.searchParams.set("from", pathname) @@ -122,7 +94,7 @@ export async function proxy(request: NextRequest) { // 优先级 2:仪表盘路由的细粒度权限(防止跨角色访问仪表盘) if (Object.prototype.hasOwnProperty.call(DASHBOARD_ROUTE_PERMISSIONS, pathname)) { const requiredPerm = DASHBOARD_ROUTE_PERMISSIONS[pathname] - if (!permissions.includes(requiredPerm)) { + if (!hasPermission(requiredPerm)) { const defaultPath = resolveDefaultPath(roles) const redirectUrl = new URL(defaultPath, request.url) redirectUrl.searchParams.set("from", pathname) @@ -132,9 +104,9 @@ export async function proxy(request: NextRequest) { return NextResponse.next() } - for (const [prefix, requiredPerm] of Object.entries(ROUTE_PERMISSIONS)) { + for (const [prefix, requiredPerm] of Object.entries(ROUTE_PREFIX_PERMISSIONS)) { if (pathname.startsWith(prefix)) { - if (!permissions.includes(requiredPerm)) { + if (!hasPermission(requiredPerm)) { const defaultPath = resolveDefaultPath(roles) // Carry original path + reason in URL so the target page can explain // why the user was redirected (Web Interface Guidelines: URL reflects state).