// 本机会话持久化:令牌只存本机数据目录(0600),不进入 WebView 或日志。 use std::path::{Path, PathBuf}; use serde::{Deserialize, Serialize}; use crate::api::Line; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Session { pub base_url: String, pub token: String, pub username: String, pub display_name: String, pub device_name: String, pub lines: Vec, } fn session_path(dir: &Path) -> PathBuf { dir.join("session.json") } pub fn save(dir: &Path, session: &Session) -> Result<(), String> { std::fs::create_dir_all(dir).map_err(|err| format!("无法创建数据目录:{err}"))?; #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700)) .map_err(|err| format!("无法限制数据目录权限:{err}"))?; } let raw = serde_json::to_string_pretty(session).map_err(|err| err.to_string())?; write_private(&session_path(dir), raw.as_bytes()) } pub fn load(dir: &Path) -> Option { let raw = std::fs::read_to_string(session_path(dir)).ok()?; serde_json::from_str(&raw).ok() } pub fn clear(dir: &Path) { let _ = std::fs::remove_file(session_path(dir)); } #[cfg(unix)] fn write_private(path: &Path, bytes: &[u8]) -> Result<(), String> { use std::io::Write; use std::os::unix::fs::OpenOptionsExt; let mut file = std::fs::OpenOptions::new() .write(true) .create(true) .truncate(true) .mode(0o600) .open(path) .map_err(|err| format!("无法写入会话文件:{err}"))?; use std::os::unix::fs::PermissionsExt; file.set_permissions(std::fs::Permissions::from_mode(0o600)) .map_err(|err| format!("无法限制会话文件权限:{err}"))?; file.write_all(bytes) .map_err(|err| format!("写入会话文件失败:{err}")) } #[cfg(not(unix))] fn write_private(path: &Path, bytes: &[u8]) -> Result<(), String> { std::fs::write(path, bytes).map_err(|err| format!("无法写入会话文件:{err}")) } #[cfg(test)] mod tests { use super::*; #[test] fn session_roundtrip_and_clear() { let dir = std::env::temp_dir().join(format!("opsx-session-test-{}", std::process::id())); let session = Session { base_url: "https://vpn.example.com".to_string(), token: "token-abc".to_string(), username: "alice".to_string(), display_name: "爱丽丝".to_string(), device_name: "alice-mac".to_string(), lines: vec![], }; save(&dir, &session).unwrap(); let loaded = load(&dir).unwrap(); assert_eq!(loaded.token, "token-abc"); assert_eq!(loaded.username, "alice"); clear(&dir); assert!(load(&dir).is_none()); let _ = std::fs::remove_dir_all(&dir); } }