feat(auth): 添加设备在线状态上报和UUID功能

添加设备UUID生成和存储功能,实现设备在线状态上报API
新增oauth_report_online和oauth_get_device_uuid命令
优化OAuth token处理流程,支持JWT格式解析
This commit is contained in:
Yukino_fox
2026-04-13 20:59:55 +08:00
parent 90fe00d119
commit c58ae5c731
5 changed files with 228 additions and 12 deletions
+168 -2
View File
@@ -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<String, String> {
// 尝试通过外部服务获取公网 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<serde_json::Value>,
state: State<'_, Arc<RwLock<AppState>>>,
) -> Result<IpcResponse<OnlineStatusResponse>, 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<IpcResponse<String>, String> {
let device_uuid = get_or_create_device_uuid();
Ok(IpcResponse::success(device_uuid))
}
+2
View File
@@ -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,
+14 -2
View File
@@ -39,7 +39,11 @@ interface PluginStats {
export const PluginManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
const { t } = useTranslation()
const [data, setData] = useState<Plugin[]>([])
const [stats, setStats] = useState<PluginStats>({ total_plugins: 0, enabled_plugins: 0, disabled_plugins: 0 })
const [stats, setStats] = useState<PluginStats>({
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) => (
<Tooltip title={desc}>
<span style={{ maxWidth: 200, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", display: "block" }}>
<span
style={{
maxWidth: 200,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
display: "block",
}}
>
{desc || "-"}
</span>
</Tooltip>
+7 -2
View File
@@ -151,7 +151,9 @@ export class PluginRuntime {
typeof (defaultExport as Record<string, unknown>).setup === "function"
) {
return {
setup: ((defaultExport as Record<string, unknown>).setup as PluginSetup).bind(defaultExport),
setup: ((defaultExport as Record<string, unknown>).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<any>)?.detail)
+37 -6
View File
@@ -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