From 2ef088def5cf47d2e1a3ec95bbfba7ad6ccc93ec Mon Sep 17 00:00:00 2001 From: Yukino_fox Date: Thu, 16 Apr 2026 20:51:45 +0800 Subject: [PATCH] =?UTF-8?q?feat(auth):=20=E6=B7=BB=E5=8A=A0OAuth=E7=99=BB?= =?UTF-8?q?=E5=BD=95=E7=8A=B6=E6=80=81=E6=8C=81=E4=B9=85=E5=8C=96=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实现OAuth登录状态的保存、加载和清除功能,包括: - 添加三个新的API接口用于状态管理 - 创建useOAuthPersist hook处理状态持久化逻辑 - 在登录流程中自动保存状态 - 在应用启动时自动恢复登录状态 --- src-tauri/src/commands/auth.rs | 128 +++++++++++++++++++++++++ src-tauri/src/lib.rs | 3 + src/components/OAuth/OAuthLogin.tsx | 25 ++++- src/hooks/useOAuthPersist.ts | 142 ++++++++++++++++++++++++++++ src/preload/types.ts | 35 +++++++ 5 files changed, 332 insertions(+), 1 deletion(-) create mode 100644 src/hooks/useOAuthPersist.ts diff --git a/src-tauri/src/commands/auth.rs b/src-tauri/src/commands/auth.rs index d86d15a..4ec851c 100644 --- a/src-tauri/src/commands/auth.rs +++ b/src-tauri/src/commands/auth.rs @@ -770,3 +770,131 @@ pub async fn oauth_get_device_uuid() -> Result, String> { let device_uuid = get_or_create_device_uuid(); Ok(IpcResponse::success(device_uuid)) } + +/// OAuth 登录状态文件路径 +fn get_oauth_state_file_path() -> std::path::PathBuf { + let app_dir = dirs::config_dir() + .map(|p| p.join("secscore")) + .unwrap_or_else(|| std::path::PathBuf::from(".")); + let _ = std::fs::create_dir_all(&app_dir); + app_dir.join("oauth_state.json") +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OAuthState { + pub access_token: String, + pub refresh_token: String, + pub token_type: String, + pub expires_in: i64, + pub user_id: String, + pub email: String, + pub name: String, + pub github_username: Option, + pub permission: u32, + pub login_time: String, +} + +#[tauri::command] +pub async fn oauth_save_login_state(state_data: OAuthState) -> Result, String> { + println!("[OAuth] 保存登录状态 - user_id: {}", state_data.user_id); + + let file_path = get_oauth_state_file_path(); + let json = serde_json::to_string_pretty(&state_data) + .map_err(|e| format!("序列化失败: {}", e))?; + + std::fs::write(&file_path, json) + .map_err(|e| format!("写入文件失败: {}", e))?; + + println!("[OAuth] 登录状态已保存到: {:?}", file_path); + Ok(IpcResponse::success(())) +} + +#[tauri::command] +pub async fn oauth_load_login_state() -> Result>, String> { + let file_path = get_oauth_state_file_path(); + + if !file_path.exists() { + println!("[OAuth] 登录状态文件不存在"); + return Ok(IpcResponse::success(None)); + } + + let json = std::fs::read_to_string(&file_path) + .map_err(|e| format!("读取文件失败: {}", e))?; + + let state: OAuthState = serde_json::from_str(&json) + .map_err(|e| format!("解析 JSON 失败: {}", e))?; + + println!("[OAuth] 登录状态已加载 - user_id: {}", state.user_id); + Ok(IpcResponse::success(Some(state))) +} + +#[tauri::command] +pub async fn oauth_clear_login_state() -> Result, String> { + let file_path = get_oauth_state_file_path(); + + if file_path.exists() { + std::fs::remove_file(&file_path) + .map_err(|e| format!("删除文件失败: {}", e))?; + println!("[OAuth] 登录状态已清除"); + } + + Ok(IpcResponse::success(())) +} + +#[tauri::command] +pub async fn oauth_refresh_access_token( + refresh_token: String, + platform_id: String, + platform_secret: String, + state: State<'_, Arc>>, +) -> Result, String> { + println!("[OAuth] 刷新 access_token"); + + let device_uuid = get_or_create_device_uuid(); + let ip_address = get_local_ip().await.unwrap_or_else(|_| "127.0.0.1".to_string()); + + let state_guard = state.read(); + let client = &state_guard.http_client; + + let payload = serde_json::json!({ + "grant_type": "refresh_token", + "refresh_token": refresh_token, + "client_id": platform_id, + "client_secret": platform_secret, + "device_uuid": device_uuid, + "ip_address": ip_address, + }); + + println!("[OAuth] 刷新 token 请求参数: {:?}", payload); + + let response = client + .post("https://appwrite.sectl.top/api/oauth/token") + .json(&payload) + .send() + .await + .map_err(|e| format!("请求失败: {}", e))?; + + println!("[OAuth] 刷新 token 响应状态: {}", response.status()); + + let response_text = response + .text() + .await + .map_err(|e| format!("读取响应失败: {}", e))?; + + if !response_text.is_empty() && response_text.contains("error") { + return Ok(IpcResponse::error(&response_text)); + } + + let mut token_response: OAuthTokenResponse = serde_json::from_str(&response_text) + .map_err(|e| format!("解析响应失败: {}", e))?; + + // 处理 access_token 格式: JWT|refresh_token + if token_response.access_token.contains('|') { + let parts: Vec<&str> = token_response.access_token.split('|').collect(); + if !parts.is_empty() { + token_response.access_token = parts[0].to_string(); + } + } + + Ok(IpcResponse::success(token_response)) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index f96abff..c4f4234 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -86,6 +86,9 @@ pub fn run() { oauth_stop_callback_server, oauth_report_online, oauth_get_device_uuid, + oauth_save_login_state, + oauth_load_login_state, + oauth_clear_login_state, theme_list, theme_current, theme_set, diff --git a/src/components/OAuth/OAuthLogin.tsx b/src/components/OAuth/OAuthLogin.tsx index cedcce2..4e8ad22 100644 --- a/src/components/OAuth/OAuthLogin.tsx +++ b/src/components/OAuth/OAuthLogin.tsx @@ -131,7 +131,30 @@ export function OAuthLogin({ visible, onClose, onSuccess }: OAuthLoginProps) { return } - console.log("[OAuth] 登录成功,清理资源...") + 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") diff --git a/src/hooks/useOAuthPersist.ts b/src/hooks/useOAuthPersist.ts new file mode 100644 index 0000000..bf12623 --- /dev/null +++ b/src/hooks/useOAuthPersist.ts @@ -0,0 +1,142 @@ +/** + * OAuth 登录状态持久化 Hook + * 用于保存、恢复和清除 OAuth 登录状态 + */ + +import { useState, useEffect, useCallback } from "react" + +export interface OAuthUserInfo { + user_id: string + email: string + name: string + github_username?: string + permission: number +} + +export interface OAuthLoginState { + access_token: string + refresh_token: string + token_type: string + expires_in: number + user_id: string + email: string + name: string + github_username?: string + permission: number + login_time: string +} + +export function useOAuthPersist() { + const [isAuthenticated, setIsAuthenticated] = useState(false) + const [isLoading, setIsLoading] = useState(true) + const [userInfo, setUserInfo] = useState(null) + const [loginState, setLoginState] = useState(null) + + // 初始化时尝试恢复登录状态 + useEffect(() => { + const initAuth = async () => { + try { + const api = (window as any).api + if (!api) { + console.error("[OAuth Persist] API 不可用") + setIsLoading(false) + return + } + + console.log("[OAuth Persist] 尝试恢复登录状态...") + const result = await api.oauthLoadLoginState() + + if (result.success && result.data) { + console.log("[OAuth Persist] 找到保存的登录状态:", result.data.user_id) + setLoginState(result.data) + setUserInfo({ + user_id: result.data.user_id, + email: result.data.email, + name: result.data.name, + github_username: result.data.github_username, + permission: result.data.permission, + }) + setIsAuthenticated(true) + } else { + console.log("[OAuth Persist] 没有找到登录状态") + } + } catch (error) { + console.error("[OAuth Persist] 恢复登录状态失败:", error) + } finally { + setIsLoading(false) + } + } + + initAuth() + }, []) + + // 保存登录状态 + const saveLoginState = useCallback( + async ( + tokenData: { + access_token: string + refresh_token: string + token_type: string + expires_in: number + }, + userData: OAuthUserInfo + ) => { + try { + const api = (window as any).api + if (!api) { + console.error("[OAuth Persist] API 不可用") + return + } + + const state: OAuthLoginState = { + ...tokenData, + ...userData, + login_time: new Date().toISOString(), + } + + await api.oauthSaveLoginState(state) + setLoginState(state) + setUserInfo(userData) + setIsAuthenticated(true) + console.log("[OAuth Persist] 登录状态已保存") + } catch (error) { + console.error("[OAuth Persist] 保存登录状态失败:", error) + } + }, + [] + ) + + // 清除登录状态 + const clearLoginState = useCallback(async () => { + try { + const api = (window as any).api + if (!api) { + console.error("[OAuth Persist] API 不可用") + return + } + + await api.oauthClearLoginState() + setLoginState(null) + setUserInfo(null) + setIsAuthenticated(false) + console.log("[OAuth Persist] 登录状态已清除") + } catch (error) { + console.error("[OAuth Persist] 清除登录状态失败:", error) + } + }, []) + + // 登出 + const logout = useCallback(async () => { + await clearLoginState() + }, [clearLoginState]) + + return { + isAuthenticated, + isLoading, + userInfo, + loginState, + saveLoginState, + clearLoginState, + logout, + } +} diff --git a/src/preload/types.ts b/src/preload/types.ts index 2950244..45e798a 100644 --- a/src/preload/types.ts +++ b/src/preload/types.ts @@ -565,6 +565,41 @@ const api = { data: string message?: string }> => invoke("oauth_get_device_uuid"), + oauthSaveLoginState: (state: { + access_token: string + refresh_token: string + token_type: string + expires_in: number + user_id: string + email: string + name: string + github_username?: string + permission: number + login_time: string + }): Promise<{ + success: boolean + message?: string + }> => invoke("oauth_save_login_state", { stateData: state }), + oauthLoadLoginState: (): Promise<{ + success: boolean + data?: { + access_token: string + refresh_token: string + token_type: string + expires_in: number + user_id: string + email: string + name: string + github_username?: string + permission: number + login_time: string + } | null + message?: string + }> => invoke("oauth_load_login_state"), + oauthClearLoginState: (): Promise<{ + success: boolean + message?: string + }> => invoke("oauth_clear_login_state"), // Data import/export exportDataJson: (): Promise<{ success: boolean; data: string }> => invoke("data_export_json"),