diff --git a/package.json b/package.json index cffcc04..5b366b8 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ "crypto-js": "^4.2.0", "dayjs": "^1.11.20", "i18next": "^25.8.14", - "json-rules-engine": "^7.3.1", "pinyin-pro": "^3.27.0", "react": "^19.2.1", "react-dom": "^19.2.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8c9a7e3..5bb70a8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,9 +35,6 @@ importers: i18next: specifier: ^25.8.14 version: 25.8.18(typescript@5.9.3) - json-rules-engine: - specifier: ^7.3.1 - version: 7.3.1 pinyin-pro: specifier: ^3.27.0 version: 3.28.0 @@ -462,18 +459,6 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@jsep-plugin/assignment@1.3.0': - resolution: {integrity: sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==} - engines: {node: '>= 10.16.0'} - peerDependencies: - jsep: ^0.4.0||^1.0.0 - - '@jsep-plugin/regex@1.0.4': - resolution: {integrity: sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==} - engines: {node: '>= 10.16.0'} - peerDependencies: - jsep: ^0.4.0||^1.0.0 - '@rc-component/async-validator@5.1.0': resolution: {integrity: sha512-n4HcR5siNUXRX23nDizbZBQPO0ZM/5oTtmKZ6/eqL0L2bo747cklFdZGRN2f+c9qWGICwDzrhW0H7tE9PptdcA==} engines: {node: '>=14.x'} @@ -854,66 +839,79 @@ packages: resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.59.0': resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.59.0': resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.59.0': resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.59.0': resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.59.0': resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.59.0': resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.59.0': resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.59.0': resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.59.0': resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.59.0': resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.59.0': resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.59.0': resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.59.0': resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} @@ -977,30 +975,35 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@tauri-apps/cli-linux-arm64-musl@2.10.1': resolution: {integrity: sha512-MIj78PDDGjkg3NqGptDOGgfXks7SYJwhiMh8SBoZS+vfdz7yP5jN18bNaLnDhsVIPARcAhE1TlsZe/8Yxo2zqg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@tauri-apps/cli-linux-riscv64-gnu@2.10.1': resolution: {integrity: sha512-X0lvOVUg8PCVaoEtEAnpxmnkwlE1gcMDTqfhbefICKDnOTJ5Est3qL0SrWxizDackIOKBcvtpejrSiVpuJI1kw==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] + libc: [glibc] '@tauri-apps/cli-linux-x64-gnu@2.10.1': resolution: {integrity: sha512-2/12bEzsJS9fAKybxgicCDFxYD1WEI9kO+tlDwX5znWG2GwMBaiWcmhGlZ8fi+DMe9CXlcVarMTYc0L3REIRxw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@tauri-apps/cli-linux-x64-musl@2.10.1': resolution: {integrity: sha512-Y8J0ZzswPz50UcGOFuXGEMrxbjwKSPgXftx5qnkuMs2rmwQB5ssvLb6tn54wDSYxe7S6vlLob9vt0VKuNOaCIQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@tauri-apps/cli-win32-arm64-msvc@2.10.1': resolution: {integrity: sha512-iSt5B86jHYAPJa/IlYw++SXtFPGnWtFJriHn7X0NFBVunF6zu9+/zOn8OgqIWSl8RgzhLGXQEEtGBdR4wzpVgg==} @@ -1477,9 +1480,6 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} - eventemitter2@6.4.9: - resolution: {integrity: sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==} - fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1595,9 +1595,6 @@ packages: resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} engines: {node: '>= 0.4'} - hash-it@6.0.1: - resolution: {integrity: sha512-qhl8+l4Zwi1eLlL3lja5ywmDQnBzLEJxd0QJoAVIgZpgQbdtVZrN5ypB0y3VHwBlvAalpcbM2/A6x7oUks5zNg==} - hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -1767,10 +1764,6 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true - jsep@1.4.0: - resolution: {integrity: sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==} - engines: {node: '>= 10.16.0'} - jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -1785,10 +1778,6 @@ packages: json-logic-js@2.0.5: resolution: {integrity: sha512-rTT2+lqcuUmj4DgWfmzupZqQDA64AdmYqizzMPWj3DxGdfFNsxPpcNVSaTj4l8W2tG/+hg7/mQhxjU3aPacO6g==} - json-rules-engine@7.3.1: - resolution: {integrity: sha512-NyRTQZllvAt7AQ3g9P7/t4nIwlEB+EyZV7y8/WgXfZWSlpcDryt1UH9CsoU+Z+MDvj8umN9qqEcbE6qnk9JAHw==} - engines: {node: '>=18.0.0'} - json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -1803,11 +1792,6 @@ packages: engines: {node: '>=6'} hasBin: true - jsonpath-plus@10.4.0: - resolution: {integrity: sha512-T92WWatJXmhBbKsgH/0hl+jxjdXrifi5IKeMY02DWggRxX0UElcbVzPlmgLTbvsPeW1PasQ6xE2Q75stkhGbsA==} - engines: {node: '>=18.0.0'} - hasBin: true - jsx-ast-utils@3.3.5: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} @@ -2745,14 +2729,6 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@jsep-plugin/assignment@1.3.0(jsep@1.4.0)': - dependencies: - jsep: 1.4.0 - - '@jsep-plugin/regex@1.0.4(jsep@1.4.0)': - dependencies: - jsep: 1.4.0 - '@rc-component/async-validator@5.1.0': dependencies: '@babel/runtime': 7.28.6 @@ -4017,8 +3993,6 @@ snapshots: esutils@2.0.3: {} - eventemitter2@6.4.9: {} - fast-deep-equal@3.1.3: {} fast-json-stable-stringify@2.1.0: {} @@ -4126,8 +4100,6 @@ snapshots: dependencies: has-symbols: 1.1.0 - hash-it@6.0.1: {} - hasown@2.0.2: dependencies: function-bind: 1.1.2 @@ -4304,8 +4276,6 @@ snapshots: dependencies: argparse: 2.0.1 - jsep@1.4.0: {} - jsesc@3.1.0: {} json-bigint@1.0.0: @@ -4316,13 +4286,6 @@ snapshots: json-logic-js@2.0.5: {} - json-rules-engine@7.3.1: - dependencies: - clone: 2.1.2 - eventemitter2: 6.4.9 - hash-it: 6.0.1 - jsonpath-plus: 10.4.0 - json-schema-traverse@0.4.1: {} json-stable-stringify-without-jsonify@1.0.1: {} @@ -4333,12 +4296,6 @@ snapshots: json5@2.2.3: {} - jsonpath-plus@10.4.0: - dependencies: - '@jsep-plugin/assignment': 1.3.0(jsep@1.4.0) - '@jsep-plugin/regex': 1.0.4(jsep@1.4.0) - jsep: 1.4.0 - jsx-ast-utils@3.3.5: dependencies: array-includes: 3.1.9 diff --git a/src/App.tsx b/src/App.tsx index 73d911c..9e011d9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -48,6 +48,14 @@ const applyGlobalFontFamily = (fontValue?: string) => { document.body.style.fontFamily = fontFamily } +const mapOAuthPermissionToAppPermission = ( + oauthPermission: number +): "admin" | "points" | "view" => { + if (oauthPermission >= 18) return "admin" + if (oauthPermission >= 1) return "points" + return "view" +} + function MainContent(): React.JSX.Element { const { t } = useTranslation() const navigate = useNavigate() @@ -188,15 +196,32 @@ function MainContent(): React.JSX.Element { useEffect(() => { const loadAuthAndSettings = async () => { - if (!(window as any).api) return - const authRes = await (window as any).api.authGetStatus() + const api = (window as any).api + if (!api) return + + const [authRes, oauthRes, settingsRes] = await Promise.all([ + api.authGetStatus(), + api.oauthLoadLoginState(), + api.getAllSettings(), + ]) + + const oauthPermission = + oauthRes?.success && oauthRes.data + ? mapOAuthPermissionToAppPermission(oauthRes.data.permission) + : null + if (authRes?.success && authRes.data) { - setPermission(authRes.data.permission) const anyPwd = Boolean(authRes.data.hasAdminPassword || authRes.data.hasPointsPassword) setHasAnyPassword(anyPwd) - if (anyPwd && authRes.data.permission === "view") setAuthVisible(true) + + const finalPermission = oauthPermission ?? authRes.data.permission + setPermission(finalPermission) + setAuthVisible(anyPwd && finalPermission === "view") + } else if (oauthPermission) { + setPermission(oauthPermission) + setAuthVisible(false) } - const settingsRes = await (window as any).api.getAllSettings() + if (settingsRes?.success && settingsRes.data) { setMobileBottomNavItems(normalizeStoredBottomKeys(settingsRes.data.mobile_bottom_nav_items)) applyGlobalFontFamily(settingsRes.data.font_family) @@ -425,8 +450,17 @@ function MainContent(): React.JSX.Element { } const logout = async () => { - if (!(window as any).api) return - const res = await (window as any).api.authLogout() + const api = (window as any).api + if (!api) return + + // 退出时同时清理 OAuth 持久化状态,避免刷新后又自动恢复权限 + try { + await api.oauthClearLoginState() + } catch (error) { + console.error("Failed to clear OAuth login state:", error) + } + + const res = await api.authLogout() if (res?.success && res.data) { setPermission(res.data.permission) messageApi.success(t("auth.logout")) @@ -440,14 +474,8 @@ function MainContent(): React.JSX.Element { github_username?: string permission: number }) => { - let newPermission: "admin" | "points" | "view" = "view" - if (userInfo.permission >= 18) { - newPermission = "admin" - } else if (userInfo.permission >= 1) { - newPermission = "points" - } - - setPermission(newPermission) + setPermission(mapOAuthPermissionToAppPermission(userInfo.permission)) + setAuthVisible(false) messageApi.success(t("auth.oauthSuccess", "登录成功")) } @@ -984,7 +1012,7 @@ function MainContent(): React.JSX.Element { footer={null} closable={!syncApplyLoading} maskClosable={false} - destroyOnClose + destroyOnHidden >
sessionStorage.getItem(OAUTH_EXPECTED_STATE_KEY) +const setExpectedState = (state: string) => sessionStorage.setItem(OAUTH_EXPECTED_STATE_KEY, state) +const clearExpectedState = () => sessionStorage.removeItem(OAUTH_EXPECTED_STATE_KEY) + +const getCodeVerifier = () => sessionStorage.getItem(OAUTH_CODE_VERIFIER_KEY) +const setCodeVerifier = (codeVerifier: string) => + sessionStorage.setItem(OAUTH_CODE_VERIFIER_KEY, codeVerifier) +const clearCodeVerifier = () => sessionStorage.removeItem(OAUTH_CODE_VERIFIER_KEY) + +const getCallbackUrl = () => sessionStorage.getItem(OAUTH_CALLBACK_URL_KEY) || DEFAULT_CALLBACK_URL +const setCallbackUrl = (callbackUrl: string) => sessionStorage.setItem(OAUTH_CALLBACK_URL_KEY, callbackUrl) +const clearCallbackUrl = () => sessionStorage.removeItem(OAUTH_CALLBACK_URL_KEY) export function OAuthLogin({ visible, onClose, onSuccess }: OAuthLoginProps) { const { t } = useTranslation() const [loading, setLoading] = useState(false) - // 使用 sessionStorage 存储 state,防止组件重新渲染导致丢失 - const getExpectedState = () => sessionStorage.getItem("oauth_expected_state") - const setExpectedState = (state: string) => sessionStorage.setItem("oauth_expected_state", state) - const clearExpectedState = () => sessionStorage.removeItem("oauth_expected_state") + const isProcessingCallbackRef = useRef(false) const getOAuthConfig = (): OAuthConfig | null => { const api = (window as any).api @@ -58,53 +68,72 @@ export function OAuthLogin({ visible, onClose, onSuccess }: OAuthLoginProps) { } } - const handleOAuthCallback = async (result: OAuthCallbackResult) => { - // 全局锁检查,防止重复处理 - if (isProcessingCallback) { - console.log("[OAuth] 回调正在处理中,跳过") - return - } - - console.log("[OAuth] 收到回调:", result) - const config = getOAuthConfig() - if (!config) { - console.error("[OAuth] 配置不存在") - return - } - + const stopCallbackServer = useCallback(async () => { + const api = (window as any).api + if (!api?.oauthStopCallbackServer) return try { - isProcessingCallback = true + await api.oauthStopCallbackServer() + } catch (error) { + console.error("[OAuth] 停止回调服务器失败:", error) + } + }, []) - if (result.error) { - console.error("[OAuth] 错误:", result.error, result.error_description) - message.error(result.error_description || result.error || "授权失败") + const clearOAuthTempState = useCallback(() => { + clearExpectedState() + clearCodeVerifier() + clearCallbackUrl() + }, []) + + const handleOAuthCallback = useCallback( + async (result: OAuthCallbackResult) => { + if (isProcessingCallbackRef.current) { + console.log("[OAuth] 回调正在处理中,跳过") + return + } + + console.log("[OAuth] 收到回调:", result) + const config = getOAuthConfig() + if (!config) { + console.error("[OAuth] 配置不存在") + message.error("OAuth 配置未设置") setLoading(false) return } - if (result.code) { + const api = (window as any).api + if (!api) { + message.error("API 不可用") + setLoading(false) + return + } + + try { + isProcessingCallbackRef.current = true + + if (result.error) { + console.error("[OAuth] 错误:", result.error, result.error_description) + message.error(result.error_description || result.error || "授权失败") + return + } + + if (!result.code) return + console.log("[OAuth] 授权码:", result.code) console.log("[OAuth] State:", result.state, "期望:", getExpectedState()) - // 验证 state 防止 CSRF const expectedState = getExpectedState() if (expectedState && result.state !== expectedState) { message.error("安全验证失败:state 不匹配") - setLoading(false) return } - const api = (window as any).api - const callbackUrl = "http://127.0.0.1:16888/oauth/callback" - - // 获取存储的 code_verifier - const codeVerifier = sessionStorage.getItem("oauth_code_verifier") + const codeVerifier = getCodeVerifier() if (!codeVerifier) { message.error("安全验证失败:缺少 code_verifier") - setLoading(false) return } + const callbackUrl = getCallbackUrl() console.log("[OAuth] 开始换取 token...") const tokenRes = await api.oauthExchangeCode( result.code, @@ -117,7 +146,6 @@ export function OAuthLogin({ visible, onClose, onSuccess }: OAuthLoginProps) { if (!tokenRes.success) { message.error(tokenRes.message || "获取访问令牌失败") - setLoading(false) return } @@ -127,13 +155,11 @@ export function OAuthLogin({ visible, onClose, onSuccess }: OAuthLoginProps) { if (!userRes.success) { message.error(userRes.message || "获取用户信息失败") - setLoading(false) return } console.log("[OAuth] 登录成功,保存登录状态...") - // 保存登录状态到本地 const loginState = { access_token: tokenRes.data.access_token, refresh_token: tokenRes.data.refresh_token, @@ -154,57 +180,60 @@ export function OAuthLogin({ visible, onClose, onSuccess }: OAuthLoginProps) { console.error("[OAuth] 保存登录状态失败:", saveError) } - console.log("[OAuth] 清理资源...") - await api.oauthStopCallbackServer() - clearExpectedState() - sessionStorage.removeItem("oauth_code_verifier") console.log("[OAuth] 调用 onSuccess...") onSuccess(userRes.data) console.log("[OAuth] 调用 onClose...") onClose() + } catch (error: any) { + console.error("[OAuth] 处理回调时发生错误:", error) + message.error(error.message || "登录失败") + } finally { + await stopCallbackServer() + clearOAuthTempState() + setLoading(false) + window.setTimeout(() => { + isProcessingCallbackRef.current = false + console.log("[OAuth] 局部锁已释放") + }, 300) } - } catch (error: any) { - console.error("[OAuth] 处理回调时发生错误:", error) - message.error(error.message || "登录失败") - } finally { - console.log("[OAuth] 回调处理完成,重置状态") - setLoading(false) - // 延迟释放锁,确保其他可能的重复事件被忽略 - setTimeout(() => { - isProcessingCallback = false - console.log("[OAuth] 全局锁已释放") - }, 1000) - } - } + }, + [clearOAuthTempState, onClose, onSuccess, stopCallbackServer] + ) useEffect(() => { - const setupListener = async () => { - // 如果已经有全局监听器,不再创建新的 - if (globalUnlisten) { - console.log("[OAuth] 全局监听器已存在,跳过创建") - return - } + if (!visible) return + let unlisten: UnlistenFn | null = null + let disposed = false + + const setupListener = async () => { try { - const unlisten = await listen("oauth-callback", async (event) => { + unlisten = await listen("oauth-callback", async (event) => { console.log("[OAuth] Event listener 收到 payload:", event.payload) if (event.payload) { await handleOAuthCallback(event.payload) } }) - globalUnlisten = unlisten - console.log("[OAuth] 全局监听器已创建") + + if (disposed) { + unlisten?.() + unlisten = null + } } catch (error) { console.error("Failed to setup OAuth callback listener:", error) } } - setupListener() + void setupListener() - // 组件卸载时不取消监听,保持全局监听 - // 只在应用完全关闭时才清理 - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) + return () => { + disposed = true + if (unlisten) { + unlisten() + unlisten = null + } + } + }, [handleOAuthCallback, visible]) const handleOAuthLogin = async () => { const config = getOAuthConfig() @@ -226,8 +255,8 @@ export function OAuthLogin({ visible, onClose, onSuccess }: OAuthLoginProps) { } const callbackUrl = serverRes.data.url + setCallbackUrl(callbackUrl) - // 生成随机 state 防止 CSRF const state = generateRandomState() console.log("[OAuth] 生成 state:", state) setExpectedState(state) @@ -241,8 +270,7 @@ export function OAuthLogin({ visible, onClose, onSuccess }: OAuthLoginProps) { return } - // 存储 code_verifier 用于后续换取 token - sessionStorage.setItem("oauth_code_verifier", urlRes.data.code_verifier) + setCodeVerifier(urlRes.data.code_verifier) console.log("[OAuth] code_verifier 已存储") await open(urlRes.data.url) @@ -252,7 +280,13 @@ export function OAuthLogin({ visible, onClose, onSuccess }: OAuthLoginProps) { } } - // 生成随机 state 字符串 + const handleModalClose = () => { + setLoading(false) + clearOAuthTempState() + void stopCallbackServer() + onClose() + } + const generateRandomState = (): string => { const array = new Uint8Array(32) window.crypto.getRandomValues(array) @@ -266,7 +300,7 @@ export function OAuthLogin({ visible, onClose, onSuccess }: OAuthLoginProps) { (null) + const getOAuthPermissionLabel = (oauthPermission: number) => { + if (oauthPermission >= 18) return t("permissions.admin") + if (oauthPermission >= 1) return t("permissions.points") + return t("permissions.view") + } + const permissionTag = useMemo(() => { return ( { + const api = (window as any).api + if (!api) { + setOAuthUserInfo(null) + return + } + + try { + await api.oauthClearLoginState() + setOAuthUserInfo(null) + messageApi.success(t("auth.logout")) + } catch (error: any) { + messageApi.error(error?.message || t("common.error")) + } + } + const loadAboutContent = async () => { try { const res = await fetch("/about-content.json") @@ -1098,17 +1134,13 @@ export const Settings: React.FC<{
{t("settings.account.permission")}: - {oauthUserInfo.permission === 1 - ? t("permissions.admin") - : oauthUserInfo.permission === 2 - ? t("permissions.points") - : t("permissions.view")} + {getOAuthPermissionLabel(oauthUserInfo.permission)}
-