fix: 修复登录持久化问题

删除了json-rules-engine
This commit is contained in:
NanGua-QWQ
2026-04-18 20:10:19 +08:00
parent b458a10deb
commit 1d70dd4af2
5 changed files with 209 additions and 159 deletions
-1
View File
@@ -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",
+18 -61
View File
@@ -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
+44 -16
View File
@@ -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
>
<div
style={{ marginBottom: "10px", color: "var(--ss-text-secondary)", fontSize: "12px" }}
+107 -73
View File
@@ -1,5 +1,5 @@
import { Button, message, Modal, Space, Spin } from "antd"
import { useEffect, useState } from "react"
import { useCallback, useEffect, useRef, useState } from "react"
import { useTranslation } from "react-i18next"
import { open } from "@tauri-apps/plugin-shell"
import { listen, UnlistenFn } from "@tauri-apps/api/event"
@@ -28,18 +28,28 @@ interface OAuthCallbackResult {
error_description?: string
}
// 全局锁,确保同一时间只有一个回调在处理
let isProcessingCallback = false
// 全局监听器引用,确保只有一个监听器
let globalUnlisten: UnlistenFn | null = null
const OAUTH_EXPECTED_STATE_KEY = "oauth_expected_state"
const OAUTH_CODE_VERIFIER_KEY = "oauth_code_verifier"
const OAUTH_CALLBACK_URL_KEY = "oauth_callback_url"
const DEFAULT_CALLBACK_URL = "http://127.0.0.1:16888/oauth/callback"
const getExpectedState = () => 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,9 +68,25 @@ export function OAuthLogin({ visible, onClose, onSuccess }: OAuthLoginProps) {
}
}
const handleOAuthCallback = async (result: OAuthCallbackResult) => {
// 全局锁检查,防止重复处理
if (isProcessingCallback) {
const stopCallbackServer = useCallback(async () => {
const api = (window as any).api
if (!api?.oauthStopCallbackServer) return
try {
await api.oauthStopCallbackServer()
} catch (error) {
console.error("[OAuth] 停止回调服务器失败:", error)
}
}, [])
const clearOAuthTempState = useCallback(() => {
clearExpectedState()
clearCodeVerifier()
clearCallbackUrl()
}, [])
const handleOAuthCallback = useCallback(
async (result: OAuthCallbackResult) => {
if (isProcessingCallbackRef.current) {
console.log("[OAuth] 回调正在处理中,跳过")
return
}
@@ -69,42 +95,45 @@ export function OAuthLogin({ visible, onClose, onSuccess }: OAuthLoginProps) {
const config = getOAuthConfig()
if (!config) {
console.error("[OAuth] 配置不存在")
return
}
try {
isProcessingCallback = true
if (result.error) {
console.error("[OAuth] 错误:", result.error, result.error_description)
message.error(result.error_description || result.error || "授权失败")
setLoading(false)
return
}
if (result.code) {
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 不匹配")
message.error("OAuth 配置未设置")
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")
if (!codeVerifier) {
message.error("安全验证失败:缺少 code_verifier")
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())
const expectedState = getExpectedState()
if (expectedState && result.state !== expectedState) {
message.error("安全验证失败:state 不匹配")
return
}
const codeVerifier = getCodeVerifier()
if (!codeVerifier) {
message.error("安全验证失败:缺少 code_verifier")
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 {
console.log("[OAuth] 回调处理完成,重置状态")
await stopCallbackServer()
clearOAuthTempState()
setLoading(false)
// 延迟释放锁,确保其他可能的重复事件被忽略
setTimeout(() => {
isProcessingCallback = false
console.log("[OAuth] 全局锁已释放")
}, 1000)
}
window.setTimeout(() => {
isProcessingCallbackRef.current = false
console.log("[OAuth] 局部锁已释放")
}, 300)
}
},
[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<OAuthCallbackResult>("oauth-callback", async (event) => {
unlisten = await listen<OAuthCallbackResult>("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) {
<Modal
title={t("auth.oauthLogin", "SECTL Auth 登录")}
open={visible}
onCancel={onClose}
onCancel={handleModalClose}
footer={null}
width={400}
centered
+38 -6
View File
@@ -206,6 +206,12 @@ export const Settings: React.FC<{
permission: number
} | null>(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 (
<Tag
@@ -312,10 +318,40 @@ export const Settings: React.FC<{
}
const authRes = await api.authGetStatus()
if (authRes.success && authRes.data) setSecurityStatus(authRes.data)
const oauthStateRes = await api.oauthLoadLoginState()
if (oauthStateRes?.success && oauthStateRes.data) {
setOAuthUserInfo({
user_id: oauthStateRes.data.user_id,
email: oauthStateRes.data.email,
name: oauthStateRes.data.name,
github_username: oauthStateRes.data.github_username,
permission: oauthStateRes.data.permission,
})
} else {
setOAuthUserInfo(null)
}
await loadMcpStatus()
await loadSystemFonts(savedFontFamily)
}
const handleOAuthLogout = async () => {
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<{
<div>
<Text type="secondary">{t("settings.account.permission")}:</Text>
<Tag color="blue" style={{ marginLeft: "8px" }}>
{oauthUserInfo.permission === 1
? t("permissions.admin")
: oauthUserInfo.permission === 2
? t("permissions.points")
: t("permissions.view")}
{getOAuthPermissionLabel(oauthUserInfo.permission)}
</Tag>
</div>
<Divider />
<Button danger onClick={() => setOAuthUserInfo(null)}>
<Button danger onClick={handleOAuthLogout}>
{t("settings.account.logout")}
</Button>
</div>