mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-19 19:04:23 +08:00
feat(auth): 添加OAuth登录状态持久化功能
实现OAuth登录状态的保存、加载和清除功能,包括: - 添加三个新的API接口用于状态管理 - 创建useOAuthPersist hook处理状态持久化逻辑 - 在登录流程中自动保存状态 - 在应用启动时自动恢复登录状态
This commit is contained in:
@@ -770,3 +770,131 @@ pub async fn oauth_get_device_uuid() -> Result<IpcResponse<String>, 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<String>,
|
||||
pub permission: u32,
|
||||
pub login_time: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn oauth_save_login_state(state_data: OAuthState) -> Result<IpcResponse<()>, 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<IpcResponse<Option<OAuthState>>, 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<IpcResponse<()>, 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<RwLock<AppState>>>,
|
||||
) -> Result<IpcResponse<OAuthTokenResponse>, 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))
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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<OAuthUserInfo | null>(null)
|
||||
const [loginState, setLoginState] = useState<OAuthLoginState | null>(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,
|
||||
}
|
||||
}
|
||||
@@ -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"),
|
||||
|
||||
Reference in New Issue
Block a user