diff --git a/native/craftlabs-auth-cli/Cargo.toml b/native/craftlabs-auth-cli/Cargo.toml index 9e36a0e..18e4da6 100644 --- a/native/craftlabs-auth-cli/Cargo.toml +++ b/native/craftlabs-auth-cli/Cargo.toml @@ -11,5 +11,9 @@ path = "src/main.rs" [dependencies] craft-core = { path = "../craft-core" } clap = { version = "4", features = ["derive"] } +serde = { version = "1", features = ["derive"] } serde_json = "1" -chrono = "0.4" +chrono = { version = "0.4", features = ["serde"] } +reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false } +tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] } +dirs = "5" diff --git a/native/craftlabs-auth-cli/src/config.rs b/native/craftlabs-auth-cli/src/config.rs new file mode 100644 index 0000000..eefa386 --- /dev/null +++ b/native/craftlabs-auth-cli/src/config.rs @@ -0,0 +1,36 @@ +use std::fs; +use std::path::Path; + +#[derive(serde::Serialize, serde::Deserialize)] +pub struct Config { + pub api_base_url: String, + pub sn: Option, +} + +impl Config { + pub fn load(path: &Path) -> Self { + if path.exists() { + fs::read_to_string(path) + .ok() + .and_then(|s| serde_json::from_str(&s).ok()) + .unwrap_or_default() + } else { + Config::default() + } + } + + pub fn save(&self, path: &Path) { + if let Ok(json) = serde_json::to_string_pretty(self) { + fs::write(path, json).ok(); + } + } +} + +impl Default for Config { + fn default() -> Self { + Config { + api_base_url: "http://localhost:8080".to_string(), + sn: None, + } + } +} diff --git a/native/craftlabs-auth-cli/src/main.rs b/native/craftlabs-auth-cli/src/main.rs index 4aa5ea5..998a317 100644 --- a/native/craftlabs-auth-cli/src/main.rs +++ b/native/craftlabs-auth-cli/src/main.rs @@ -6,12 +6,25 @@ use craftlabs_auth_core::{ use craftlabs_auth_core::device; use std::ffi::CString; use std::path::PathBuf; +use std::fs; + +mod config; +mod platform_api; + +use config::Config; +use platform_api::PlatformClient; #[derive(Parser)] #[command(name = "craft", version, about = "CraftLabs 授权客户端")] struct Cli { #[command(subcommand)] command: Commands, + /// 平台 API 地址 (默认: http://localhost:8080) + #[arg(long, global = true)] + api: Option, + /// JSON 格式输出 + #[arg(long, global = true)] + json: bool, } #[derive(Subcommand)] @@ -22,46 +35,47 @@ enum Commands { Activate { sn: String }, /// 检查授权是否有效 Check, - /// 显示授权详情 + 功能特性 + /// 显示授权详情 Info, /// 撤销本机授权 Release, + /// 迁移授权到本机 (释放旧授权 + 激活新 SN) + Migrate { sn: String }, /// 显示本机设备指纹 DeviceId, /// 手动触发心跳 Heartbeat, + /// 查看/修改配置 + Config { + /// 查看或设置: status, set-api , set-sn + action: Vec, + }, } fn get_config_path() -> PathBuf { - let mut path = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); - path.push("craftlabs-auth-config.json"); - path -} - -fn default_config() -> String { - r#"{"provider":"selfhosted","schemaVersion":1,"scenario":"floating"}"#.to_string() -} - -fn load_or_create_config() -> String { - let path = get_config_path(); - std::fs::read_to_string(&path).unwrap_or_else(|_| { - let cfg = default_config(); - std::fs::write(&path, &cfg).ok(); - cfg - }) + let mut path = dirs::config_dir().unwrap_or_else(|| PathBuf::from(".")); + path.push("craftlabs"); + fs::create_dir_all(&path).ok(); + path.join("config.json") } fn main() { let cli = Cli::parse(); - let config = load_or_create_config(); - let c_config = CString::new(config).unwrap(); + let config_path = get_config_path(); + let mut config = Config::load(&config_path); + + if let Some(api_url) = &cli.api { + config.api_base_url = api_url.clone(); + } + + let rt = tokio::runtime::Runtime::new().unwrap(); match &cli.command { Commands::Status => { - print_status(); + print_status(&config, cli.json); } Commands::Activate { sn } => { - let handle = craft_initialize(c_config.as_ptr()); + let handle = init_engine(); if handle.is_null() { eprintln!("错误: 初始化授权引擎失败"); std::process::exit(1); @@ -69,107 +83,167 @@ fn main() { let c_sn = CString::new(sn.as_str()).unwrap(); let result = craft_activate(handle, c_sn.as_ptr(), std::ptr::null()); if result.success != 0 { - println!("✅ 激活成功"); - save_config_with_sn(sn); + println!("OK: 激活成功"); + config.sn = Some(sn.clone()); + config.save(&config_path); + rt.block_on(sync_activation(&config, sn)); } else { let msg = if result.message.is_null() { "未知错误" } else { unsafe { std::ffi::CStr::from_ptr(result.message) }.to_str().unwrap_or("未知错误") }; - eprintln!("❌ 激活失败: {}", msg); + eprintln!("错误: 激活失败 - {}", msg); } craft_destroy(handle); } Commands::Check => { - let handle = craft_initialize(c_config.as_ptr()); - if handle.is_null() { eprintln!("初始化失败"); return; } + let handle = init_engine(); + if handle.is_null() { eprintln!("错误: 初始化失败"); return; } let result = craft_check_license(handle); - if result.success != 0 { - println!("✅ 授权有效"); + if cli.json { + let msg = if !result.message.is_null() { + unsafe { std::ffi::CStr::from_ptr(result.message) }.to_str().unwrap_or("") + } else { "" }; + println!("{{\"status\":{},\"message\":\"{}\"}}", result.success, msg); } else { - println!("❌ 授权无效"); + println!("授权状态: {}", if result.success != 0 { "有效" } else { "无效" }); } craft_destroy(handle); } Commands::Info => { - let handle = craft_initialize(c_config.as_ptr()); - if handle.is_null() { eprintln!("初始化失败"); return; } - let info = craft_get_license_info(handle); - if !info.is_null() { - let i = unsafe { &*info }; - let names = unsafe { std::slice::from_raw_parts(i.feature_names, i.feature_count as usize) }; - let values = unsafe { std::slice::from_raw_parts(i.feature_values, i.feature_count as usize) }; - println!("授权状态: {}", if i.is_licensed != 0 { "已授权" } else { "未授权" }); - if i.expiration_date > 0 { - println!("过期时间: {}", i.expiration_date); + let handle = init_engine(); + if handle.is_null() { eprintln!("错误: 初始化失败"); return; } + let info_ptr = craft_get_license_info(handle); + if !info_ptr.is_null() { + let info = unsafe { &*info_ptr }; + println!("授权状态: {}", if info.is_licensed != 0 { "已授权" } else { "未授权" }); + if info.expiration_date > 0 { + println!("过期时间戳: {}", info.expiration_date); } - if i.feature_count > 0 { - println!("功能特性 ({}):", i.feature_count); - for idx in 0..i.feature_count as usize { + if info.feature_count > 0 { + println!("功能特性 ({}):", info.feature_count); + let names = unsafe { std::slice::from_raw_parts(info.feature_names, info.feature_count as usize) }; + let values = unsafe { std::slice::from_raw_parts(info.feature_values, info.feature_count as usize) }; + for idx in 0..info.feature_count as usize { let name = if idx < names.len() && !names[idx].is_null() { unsafe { std::ffi::CStr::from_ptr(names[idx]) } .to_str().unwrap_or("?") } else { "?" }; let val = if idx < values.len() { values[idx] } else { 0 }; - println!(" {}: {}", name, if val != 0 { "✅" } else { "❌" }); + println!(" {}: {}", name, if val != 0 { "ON" } else { "OFF" }); } } - craft_free_license_info(info); + craft_free_license_info(info_ptr); } else { println!("无法获取授权信息"); } craft_destroy(handle); } Commands::Release => { - let handle = craft_initialize(c_config.as_ptr()); - if handle.is_null() { eprintln!("初始化失败"); return; } + let handle = init_engine(); + if handle.is_null() { eprintln!("错误: 初始化失败"); return; } let result = craft_release(handle); if result.success != 0 { - println!("✅ 授权已撤销"); + println!("OK: 授权已撤销"); + config.sn = None; + config.save(&config_path); } else { - println!("❌ 撤销失败"); + eprintln!("错误: 撤销失败"); + } + craft_destroy(handle); + } + Commands::Migrate { sn } => { + println!("正在迁移授权到新 SN: {} ...", sn); + let handle = init_engine(); + if handle.is_null() { eprintln!("错误: 初始化失败"); return; } + craft_release(handle); + craft_destroy(handle); + + let handle = init_engine(); + if handle.is_null() { eprintln!("错误: 初始化失败"); return; } + let c_sn = CString::new(sn.as_str()).unwrap(); + let result = craft_activate(handle, c_sn.as_ptr(), std::ptr::null()); + if result.success != 0 { + println!("OK: 迁移成功"); + config.sn = Some(sn.clone()); + config.save(&config_path); + } else { + eprintln!("错误: 迁移失败"); } craft_destroy(handle); } Commands::DeviceId => { let fp = device::collect(); - println!("设备指纹: {}", fp.composite_hash); + if cli.json { + println!("{{\"device_id\":\"{}\"}}", fp.composite_hash); + } else { + println!("设备指纹: {}", fp.composite_hash); + } } Commands::Heartbeat => { - let handle = craft_initialize(c_config.as_ptr()); - if handle.is_null() { eprintln!("初始化失败"); return; } + let handle = init_engine(); + if handle.is_null() { eprintln!("错误: 初始化失败"); return; } let result = craft_heartbeat(handle); - if result.success != 0 { - println!("✅ 心跳成功"); - } else { - println!("❌ 心跳失败"); - } + println!("心跳: {}", if result.success != 0 { "OK" } else { "FAIL" }); craft_destroy(handle); } + Commands::Config { action } => { + handle_config(action, &mut config, &config_path); + } } } -fn print_status() { - let config = load_or_create_config(); - let c_config = CString::new(config).unwrap(); - let handle = craft_initialize(c_config.as_ptr()); - if handle.is_null() { eprintln!("初始化失败"); return; } - - let check = craft_check_license(handle); - println!("授权状态: {}", if check.success != 0 { "✅ 有效" } else { "❌ 无效" }); +fn init_engine() -> *mut craftlabs_auth_core::CraftContext { + let config_json = r#"{"provider":"selfhosted","schemaVersion":1,"scenario":"floating"}"#; + let c = CString::new(config_json).unwrap(); + craft_initialize(c.as_ptr()) +} +fn print_status(config: &Config, json: bool) { + let handle = init_engine(); + if handle.is_null() { eprintln!("错误: 初始化失败"); return; } + let result = craft_check_license(handle); let fp = device::collect(); - println!("设备指纹: {}", fp.composite_hash); - + if json { + println!("{{\"licensed\":{},\"device_id\":\"{}\",\"api\":\"{}\",\"sn\":{}}}", + result.success, fp.composite_hash, config.api_base_url, + config.sn.as_deref().map(|s| format!("\"{}\"", s)).unwrap_or("null".into())); + } else { + println!("授权状态: {}", if result.success != 0 { "有效" } else { "无效" }); + println!("设备指纹: {}", fp.composite_hash); + println!("API 地址: {}", config.api_base_url); + if let Some(sn) = &config.sn { + println!("绑定 SN: {}", sn); + } + } craft_destroy(handle); } -fn save_config_with_sn(sn: &str) { - let config = serde_json::json!({ - "provider": "selfhosted", - "schemaVersion": 1, - "scenario": "floating", - "floating": { "sn": sn } - }); - let path = get_config_path(); - std::fs::write(&path, serde_json::to_string_pretty(&config).unwrap()).ok(); +fn handle_config(action: &[String], config: &mut Config, path: &PathBuf) { + if action.is_empty() { + println!("API 地址: {}", config.api_base_url); + println!("绑定 SN: {}", config.sn.as_deref().unwrap_or("(无)")); + println!("配置路径: {}", path.display()); + return; + } + match action[0].as_str() { + "set-api" if action.len() > 1 => { + config.api_base_url = action[1].clone(); + config.save(path); + println!("OK: API 地址已更新"); + } + "set-sn" if action.len() > 1 => { + config.sn = Some(action[1].clone()); + config.save(path); + println!("OK: SN 已设置"); + } + _ => eprintln!("用法: craft config [set-api |set-sn ]"), + } +} + +async fn sync_activation(config: &Config, sn: &str) { + let client = PlatformClient::new(&config.api_base_url); + match client.report_activation(sn, &device::collect().composite_hash).await { + Ok(_) => println!("平台同步成功"), + Err(e) => eprintln!("平台同步失败: {} (不影响本地授权)", e), + } } diff --git a/native/craftlabs-auth-cli/src/platform_api.rs b/native/craftlabs-auth-cli/src/platform_api.rs new file mode 100644 index 0000000..317a6bd --- /dev/null +++ b/native/craftlabs-auth-cli/src/platform_api.rs @@ -0,0 +1,52 @@ +use serde::{Serialize, Deserialize}; + +#[derive(Serialize)] +struct ActivationReport { + sn: String, + device_id: String, + timestamp: String, +} + +#[derive(Deserialize)] +struct ApiResponse { + status: String, + message: Option, +} + +pub struct PlatformClient { + base_url: String, + client: reqwest::Client, +} + +impl PlatformClient { + pub fn new(base_url: &str) -> Self { + PlatformClient { + base_url: base_url.trim_end_matches('/').to_string(), + client: reqwest::Client::new(), + } + } + + pub async fn report_activation(&self, sn: &str, device_id: &str) -> Result<(), String> { + let body = ActivationReport { + sn: sn.to_string(), + device_id: device_id.to_string(), + timestamp: chrono::Utc::now().to_rfc3339(), + }; + + let url = format!("{}/api/v1/licenses/activate", self.base_url); + let resp = self.client + .post(&url) + .json(&body) + .send() + .await + .map_err(|e| format!("请求失败: {}", e))?; + + if resp.status().is_success() { + Ok(()) + } else { + let status = resp.status(); + let text = resp.text().await.unwrap_or_default(); + Err(format!("HTTP {}: {}", status, text)) + } + } +}