From c58ae5c7313469afefee33fa785735b5bf3c6e51 Mon Sep 17 00:00:00 2001 From: Yukino_fox Date: Mon, 13 Apr 2026 20:59:55 +0800 Subject: [PATCH] =?UTF-8?q?feat(auth):=20=E6=B7=BB=E5=8A=A0=E8=AE=BE?= =?UTF-8?q?=E5=A4=87=E5=9C=A8=E7=BA=BF=E7=8A=B6=E6=80=81=E4=B8=8A=E6=8A=A5?= =?UTF-8?q?=E5=92=8CUUID=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加设备UUID生成和存储功能,实现设备在线状态上报API 新增oauth_report_online和oauth_get_device_uuid命令 优化OAuth token处理流程,支持JWT格式解析 --- src-tauri/src/commands/auth.rs | 170 ++++++++++++++++++++++++++++++- src-tauri/src/lib.rs | 2 + src/components/PluginManager.tsx | 16 ++- src/plugins/runtime.ts | 9 +- src/preload/types.ts | 43 ++++++-- 5 files changed, 228 insertions(+), 12 deletions(-) diff --git a/src-tauri/src/commands/auth.rs b/src-tauri/src/commands/auth.rs index 3ee96b2..d86d15a 100644 --- a/src-tauri/src/commands/auth.rs +++ b/src-tauri/src/commands/auth.rs @@ -11,6 +11,81 @@ use crate::state::AppState; use super::response::IpcResponse; +/// 生成标准 UUID v4 +fn generate_uuid() -> String { + use rand::Rng; + let mut rng = rand::thread_rng(); + let mut bytes = [0u8; 16]; + rng.fill(&mut bytes); + + // UUID v4 版本和变体位设置 + bytes[6] = (bytes[6] & 0x0f) | 0x40; // 版本 4 + bytes[8] = (bytes[8] & 0x3f) | 0x80; // 变体 10 + + format!( + "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}", + bytes[0], bytes[1], bytes[2], bytes[3], + bytes[4], bytes[5], + bytes[6], bytes[7], + bytes[8], bytes[9], + bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15] + ) +} + +/// 获取设备 UUID(从存储或生成新的) +fn get_or_create_device_uuid() -> String { + // 尝试从存储中读取 + if let Ok(uuid) = std::fs::read_to_string(get_device_uuid_file_path()) { + let uuid = uuid.trim(); + if is_valid_uuid(uuid) { + return uuid.to_string(); + } + } + + // 生成新的 UUID + let new_uuid = generate_uuid(); + + // 保存到文件 + let _ = std::fs::write(get_device_uuid_file_path(), &new_uuid); + + new_uuid +} + +/// 获取设备 UUID 文件路径 +fn get_device_uuid_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("device_uuid") +} + +/// 验证 UUID 格式 +fn is_valid_uuid(uuid: &str) -> bool { + uuid.len() == 36 + && uuid.chars().nth(8) == Some('-') + && uuid.chars().nth(13) == Some('-') + && uuid.chars().nth(18) == Some('-') + && uuid.chars().nth(23) == Some('-') +} + +/// 获取本机 IP 地址 +async fn get_local_ip() -> Result { + // 尝试通过外部服务获取公网 IP + match reqwest::get("https://api.ipify.org").await { + Ok(response) => { + if let Ok(ip) = response.text().await { + return Ok(ip.trim().to_string()); + } + } + Err(_) => {} + } + + // 备用:返回本地主机名 + Ok("127.0.0.1".to_string()) +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AuthStatusResponse { pub permission: String, @@ -42,7 +117,7 @@ pub struct OAuthTokenResponse { pub access_token: String, pub refresh_token: String, pub token_type: String, - pub expires_in: u64, + pub expires_in: i64, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -398,6 +473,12 @@ pub async fn oauth_exchange_code( code, platform_id, callback_url ); + // 获取设备 UUID 和 IP 地址 + let device_uuid = get_or_create_device_uuid(); + let ip_address = get_local_ip().await.unwrap_or_else(|_| "127.0.0.1".to_string()); + + println!("[OAuth] 设备 UUID: {}, IP: {}", device_uuid, ip_address); + let state_guard = state.read(); let client = &state_guard.http_client; let payload = serde_json::json!({ @@ -407,6 +488,8 @@ pub async fn oauth_exchange_code( "client_secret": &platform_secret, "redirect_uri": &callback_url, "code_verifier": &code_verifier, + "device_uuid": &device_uuid, + "ip_address": &ip_address, }); println!("[OAuth] 请求参数:{:?}", payload); @@ -430,9 +513,22 @@ pub async fn oauth_exchange_code( return Ok(IpcResponse::error(&response_text)); } - let token_response: OAuthTokenResponse = serde_json::from_str(&response_text) + let mut token_response: OAuthTokenResponse = serde_json::from_str(&response_text) .map_err(|e| format!("Failed to parse response: {}", e))?; + // 处理 access_token 格式: JWT|refresh_token + // 服务器返回的 access_token 包含 JWT 和 refresh_token,用 | 分隔 + // 我们只保留 JWT 部分用于后续请求 + if token_response.access_token.contains('|') { + let parts: Vec<&str> = token_response.access_token.split('|').collect(); + if !parts.is_empty() { + println!("[OAuth] 拆分 access_token,使用 JWT 部分"); + token_response.access_token = parts[0].to_string(); + } + } + + println!("[OAuth] Token 处理完成,access_token 长度: {}", token_response.access_token.len()); + Ok(IpcResponse::success(token_response)) } @@ -604,3 +700,73 @@ pub async fn oauth_refresh_token( Ok(IpcResponse::success(token_response)) } + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OnlineStatusResponse { + pub success: bool, + pub message: String, +} + +#[tauri::command] +pub async fn oauth_report_online( + platform_id: String, + device_type: String, + custom_data: Option, + state: State<'_, Arc>>, +) -> Result, String> { + let device_uuid = get_or_create_device_uuid(); + let ip_address = get_local_ip().await.unwrap_or_else(|_| "127.0.0.1".to_string()); + + println!( + "[OAuth] 上报在线状态 - platform_id: {}, device_uuid: {}, device_type: {}", + platform_id, device_uuid, device_type + ); + + let state_guard = state.read(); + let client = &state_guard.http_client; + + let mut payload = serde_json::json!({ + "platform_id": platform_id, + "device_uuid": device_uuid, + "device_type": device_type, + "ip_address": ip_address, + "country": "CN", + "province": "Unknown", + "city": "Unknown", + "district": "Unknown", + }); + + if let Some(data) = custom_data { + payload["custom_data"] = data; + } + + let response = client + .post("https://appwrite.sectl.top/api/stats/online") + .json(&payload) + .send() + .await + .map_err(|e| format!("Request failed: {}", e))?; + + println!("[OAuth] 在线状态上报响应: {}", response.status()); + + if !response.status().is_success() { + let error_text = response + .text() + .await + .unwrap_or_else(|_| "Unknown error".to_string()); + return Ok(IpcResponse::error(&error_text)); + } + + let result: OnlineStatusResponse = response + .json() + .await + .map_err(|e| format!("Failed to parse response: {}", e))?; + + Ok(IpcResponse::success(result)) +} + +#[tauri::command] +pub async fn oauth_get_device_uuid() -> Result, String> { + let device_uuid = get_or_create_device_uuid(); + Ok(IpcResponse::success(device_uuid)) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 3c5706c..f96abff 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -84,6 +84,8 @@ pub fn run() { oauth_introspect_token, oauth_start_callback_server, oauth_stop_callback_server, + oauth_report_online, + oauth_get_device_uuid, theme_list, theme_current, theme_set, diff --git a/src/components/PluginManager.tsx b/src/components/PluginManager.tsx index 9675dc7..10f860f 100644 --- a/src/components/PluginManager.tsx +++ b/src/components/PluginManager.tsx @@ -39,7 +39,11 @@ interface PluginStats { export const PluginManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => { const { t } = useTranslation() const [data, setData] = useState([]) - const [stats, setStats] = useState({ total_plugins: 0, enabled_plugins: 0, disabled_plugins: 0 }) + const [stats, setStats] = useState({ + total_plugins: 0, + enabled_plugins: 0, + disabled_plugins: 0, + }) const [loading, setLoading] = useState(false) const [installModalVisible, setInstallModalVisible] = useState(false) const [installLoading, setInstallLoading] = useState(false) @@ -180,7 +184,15 @@ export const PluginManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => { key: "description", render: (desc?: string) => ( - + {desc || "-"} diff --git a/src/plugins/runtime.ts b/src/plugins/runtime.ts index 09963a1..df042d0 100644 --- a/src/plugins/runtime.ts +++ b/src/plugins/runtime.ts @@ -151,7 +151,9 @@ export class PluginRuntime { typeof (defaultExport as Record).setup === "function" ) { return { - setup: ((defaultExport as Record).setup as PluginSetup).bind(defaultExport), + setup: ((defaultExport as Record).setup as PluginSetup).bind( + defaultExport + ), } } @@ -173,7 +175,10 @@ export class PluginRuntime { runtimeModule: pluginRuntimeModule, disposers: Array<() => void> ): PluginContext { - const registerEvent = (event: PluginHostEvent, handler: (detail: any) => void): (() => void) => { + const registerEvent = ( + event: PluginHostEvent, + handler: (detail: any) => void + ): (() => void) => { const eventName = PLUGIN_EVENT_MAP[event] const listener = (nativeEvent: Event) => { handler((nativeEvent as CustomEvent)?.detail) diff --git a/src/preload/types.ts b/src/preload/types.ts index 63fd329..2950244 100644 --- a/src/preload/types.ts +++ b/src/preload/types.ts @@ -548,6 +548,23 @@ const api = { success: boolean message?: string }> => invoke("oauth_stop_callback_server"), + oauthReportOnline: ( + platformId: string, + deviceType: string, + customData?: any + ): Promise<{ + success: boolean + data: { + success: boolean + message: string + } + message?: string + }> => invoke("oauth_report_online", { platformId, deviceType, customData }), + oauthGetDeviceUuid: (): Promise<{ + success: boolean + data: string + message?: string + }> => invoke("oauth_get_device_uuid"), // Data import/export exportDataJson: (): Promise<{ success: boolean; data: string }> => invoke("data_export_json"), @@ -721,7 +738,9 @@ const api = { data?: any[] message?: string }> => invoke("plugin_get_all"), - pluginGet: (pluginId: string): Promise<{ + pluginGet: ( + pluginId: string + ): Promise<{ success: boolean data?: any message?: string @@ -735,25 +754,37 @@ const api = { } message?: string }> => invoke("plugin_get_stats"), - pluginToggle: (pluginId: string, enabled: boolean): Promise<{ + pluginToggle: ( + pluginId: string, + enabled: boolean + ): Promise<{ success: boolean message?: string }> => invoke("plugin_toggle", { pluginId, enabled }), - pluginInstall: (manifest: any, pluginDir: string): Promise<{ + pluginInstall: ( + manifest: any, + pluginDir: string + ): Promise<{ success: boolean data?: any message?: string }> => invoke("plugin_install", { manifest, pluginDir }), - pluginUninstall: (pluginId: string): Promise<{ + pluginUninstall: ( + pluginId: string + ): Promise<{ success: boolean message?: string }> => invoke("plugin_uninstall", { pluginId }), - pluginLoadManifest: (path: string): Promise<{ + pluginLoadManifest: ( + path: string + ): Promise<{ success: boolean data?: any message?: string }> => invoke("plugin_load_manifest", { path }), - pluginGetDir: (pluginId: string): Promise<{ + pluginGetDir: ( + pluginId: string + ): Promise<{ success: boolean data?: string message?: string