import { Button, message, Modal, Space, Spin } from "antd" import { useEffect, useState } from "react" import { useTranslation } from "react-i18next" import { open } from "@tauri-apps/plugin-shell" import { listen, UnlistenFn } from "@tauri-apps/api/event" interface OAuthLoginProps { visible: boolean onClose: () => void onSuccess: (userInfo: { user_id: string email: string name: string github_username?: string permission: number }) => void } interface OAuthConfig { platform_id: string platform_secret: string } interface OAuthCallbackResult { code?: string state?: string error?: string error_description?: string } // 全局锁,确保同一时间只有一个回调在处理 let isProcessingCallback = false // 全局监听器引用,确保只有一个监听器 let globalUnlisten: UnlistenFn | null = null 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 getOAuthConfig = (): OAuthConfig | null => { const api = (window as any).api if (!api) return null const platformId = import.meta.env.VITE_OAUTH_PLATFORM_ID const platformSecret = import.meta.env.VITE_OAUTH_PLATFORM_SECRET if (!platformId || !platformSecret) { return null } return { platform_id: platformId, platform_secret: platformSecret, } } const handleOAuthCallback = async (result: OAuthCallbackResult) => { // 全局锁检查,防止重复处理 if (isProcessingCallback) { console.log("[OAuth] 回调正在处理中,跳过") return } console.log("[OAuth] 收到回调:", result) 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 不匹配") 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") setLoading(false) return } console.log("[OAuth] 开始换取 token...") const tokenRes = await api.oauthExchangeCode( result.code, config.platform_id, config.platform_secret, callbackUrl, codeVerifier ) console.log("[OAuth] token 响应:", tokenRes) if (!tokenRes.success) { message.error(tokenRes.message || "获取访问令牌失败") setLoading(false) return } console.log("[OAuth] 开始获取用户信息...") const userRes = await api.oauthGetUserInfo(tokenRes.data.access_token) console.log("[OAuth] 用户信息响应:", userRes) 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, token_type: tokenRes.data.token_type, expires_in: tokenRes.data.expires_in, user_id: userRes.data.user_id, email: userRes.data.email, name: userRes.data.name, github_username: userRes.data.github_username, permission: userRes.data.permission, login_time: new Date().toISOString(), } try { await api.oauthSaveLoginState(loginState) console.log("[OAuth] 登录状态已保存") } catch (saveError) { 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] 回调处理完成,重置状态") setLoading(false) // 延迟释放锁,确保其他可能的重复事件被忽略 setTimeout(() => { isProcessingCallback = false console.log("[OAuth] 全局锁已释放") }, 1000) } } useEffect(() => { const setupListener = async () => { // 如果已经有全局监听器,不再创建新的 if (globalUnlisten) { console.log("[OAuth] 全局监听器已存在,跳过创建") return } try { const 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] 全局监听器已创建") } catch (error) { console.error("Failed to setup OAuth callback listener:", error) } } setupListener() // 组件卸载时不取消监听,保持全局监听 // 只在应用完全关闭时才清理 // eslint-disable-next-line react-hooks/exhaustive-deps }, []) const handleOAuthLogin = async () => { const config = getOAuthConfig() if (!config) { message.error("OAuth 配置未设置") return } setLoading(true) try { const api = (window as any).api const serverRes = await api.oauthStartCallbackServer() if (!serverRes.success) { message.error(serverRes.message || "启动回调服务器失败") setLoading(false) return } const callbackUrl = serverRes.data.url // 生成随机 state 防止 CSRF const state = generateRandomState() console.log("[OAuth] 生成 state:", state) setExpectedState(state) console.log("[OAuth] state 已设置:", getExpectedState()) const urlRes = await api.oauthGetAuthorizationUrl(config.platform_id, callbackUrl, state) if (!urlRes.success) { message.error(urlRes.message || "获取授权链接失败") setLoading(false) return } // 存储 code_verifier 用于后续换取 token sessionStorage.setItem("oauth_code_verifier", urlRes.data.code_verifier) console.log("[OAuth] code_verifier 已存储") await open(urlRes.data.url) } catch (error: any) { message.error(error.message || "登录失败") setLoading(false) } } // 生成随机 state 字符串 const generateRandomState = (): string => { const array = new Uint8Array(32) window.crypto.getRandomValues(array) return btoa(String.fromCharCode(...array)) .replace(/\+/g, "-") .replace(/\//g, "_") .replace(/=/g, "") } return (
{loading ? (
{t("auth.oauthLoggingIn", "正在登录...")}
请在浏览器中完成授权,授权后会自动返回
) : (
{t("auth.oauthHint", "使用 SECTL Auth 账号登录,享受统一认证和远程退登功能")}
)}
) }