mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-21 11:49:02 +08:00
fix: 修复登录持久化问题
删除了json-rules-engine
This commit is contained in:
+44
-16
@@ -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" }}
|
||||
|
||||
@@ -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,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<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
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user