mirror of
https://github.com/hpd840321/craftlabs-authorization-sdk.git
synced 2026-06-09 10:00:30 +08:00
feat(cli): add migrate command, platform API, config management
This commit is contained in:
@@ -11,5 +11,9 @@ path = "src/main.rs"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
craft-core = { path = "../craft-core" }
|
craft-core = { path = "../craft-core" }
|
||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
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"
|
||||||
|
|||||||
@@ -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<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,12 +6,25 @@ use craftlabs_auth_core::{
|
|||||||
use craftlabs_auth_core::device;
|
use craftlabs_auth_core::device;
|
||||||
use std::ffi::CString;
|
use std::ffi::CString;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
mod config;
|
||||||
|
mod platform_api;
|
||||||
|
|
||||||
|
use config::Config;
|
||||||
|
use platform_api::PlatformClient;
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(name = "craft", version, about = "CraftLabs 授权客户端")]
|
#[command(name = "craft", version, about = "CraftLabs 授权客户端")]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: Commands,
|
command: Commands,
|
||||||
|
/// 平台 API 地址 (默认: http://localhost:8080)
|
||||||
|
#[arg(long, global = true)]
|
||||||
|
api: Option<String>,
|
||||||
|
/// JSON 格式输出
|
||||||
|
#[arg(long, global = true)]
|
||||||
|
json: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
@@ -22,46 +35,47 @@ enum Commands {
|
|||||||
Activate { sn: String },
|
Activate { sn: String },
|
||||||
/// 检查授权是否有效
|
/// 检查授权是否有效
|
||||||
Check,
|
Check,
|
||||||
/// 显示授权详情 + 功能特性
|
/// 显示授权详情
|
||||||
Info,
|
Info,
|
||||||
/// 撤销本机授权
|
/// 撤销本机授权
|
||||||
Release,
|
Release,
|
||||||
|
/// 迁移授权到本机 (释放旧授权 + 激活新 SN)
|
||||||
|
Migrate { sn: String },
|
||||||
/// 显示本机设备指纹
|
/// 显示本机设备指纹
|
||||||
DeviceId,
|
DeviceId,
|
||||||
/// 手动触发心跳
|
/// 手动触发心跳
|
||||||
Heartbeat,
|
Heartbeat,
|
||||||
|
/// 查看/修改配置
|
||||||
|
Config {
|
||||||
|
/// 查看或设置: status, set-api <url>, set-sn <sn>
|
||||||
|
action: Vec<String>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_config_path() -> PathBuf {
|
fn get_config_path() -> PathBuf {
|
||||||
let mut path = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
|
let mut path = dirs::config_dir().unwrap_or_else(|| PathBuf::from("."));
|
||||||
path.push("craftlabs-auth-config.json");
|
path.push("craftlabs");
|
||||||
path
|
fs::create_dir_all(&path).ok();
|
||||||
}
|
path.join("config.json")
|
||||||
|
|
||||||
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
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
let config = load_or_create_config();
|
let config_path = get_config_path();
|
||||||
let c_config = CString::new(config).unwrap();
|
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 {
|
match &cli.command {
|
||||||
Commands::Status => {
|
Commands::Status => {
|
||||||
print_status();
|
print_status(&config, cli.json);
|
||||||
}
|
}
|
||||||
Commands::Activate { sn } => {
|
Commands::Activate { sn } => {
|
||||||
let handle = craft_initialize(c_config.as_ptr());
|
let handle = init_engine();
|
||||||
if handle.is_null() {
|
if handle.is_null() {
|
||||||
eprintln!("错误: 初始化授权引擎失败");
|
eprintln!("错误: 初始化授权引擎失败");
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
@@ -69,107 +83,167 @@ fn main() {
|
|||||||
let c_sn = CString::new(sn.as_str()).unwrap();
|
let c_sn = CString::new(sn.as_str()).unwrap();
|
||||||
let result = craft_activate(handle, c_sn.as_ptr(), std::ptr::null());
|
let result = craft_activate(handle, c_sn.as_ptr(), std::ptr::null());
|
||||||
if result.success != 0 {
|
if result.success != 0 {
|
||||||
println!("✅ 激活成功");
|
println!("OK: 激活成功");
|
||||||
save_config_with_sn(sn);
|
config.sn = Some(sn.clone());
|
||||||
|
config.save(&config_path);
|
||||||
|
rt.block_on(sync_activation(&config, sn));
|
||||||
} else {
|
} else {
|
||||||
let msg = if result.message.is_null() { "未知错误" } else {
|
let msg = if result.message.is_null() { "未知错误" } else {
|
||||||
unsafe { std::ffi::CStr::from_ptr(result.message) }.to_str().unwrap_or("未知错误")
|
unsafe { std::ffi::CStr::from_ptr(result.message) }.to_str().unwrap_or("未知错误")
|
||||||
};
|
};
|
||||||
eprintln!("❌ 激活失败: {}", msg);
|
eprintln!("错误: 激活失败 - {}", msg);
|
||||||
}
|
}
|
||||||
craft_destroy(handle);
|
craft_destroy(handle);
|
||||||
}
|
}
|
||||||
Commands::Check => {
|
Commands::Check => {
|
||||||
let handle = craft_initialize(c_config.as_ptr());
|
let handle = init_engine();
|
||||||
if handle.is_null() { eprintln!("初始化失败"); return; }
|
if handle.is_null() { eprintln!("错误: 初始化失败"); return; }
|
||||||
let result = craft_check_license(handle);
|
let result = craft_check_license(handle);
|
||||||
if result.success != 0 {
|
if cli.json {
|
||||||
println!("✅ 授权有效");
|
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 {
|
} else {
|
||||||
println!("❌ 授权无效");
|
println!("授权状态: {}", if result.success != 0 { "有效" } else { "无效" });
|
||||||
}
|
}
|
||||||
craft_destroy(handle);
|
craft_destroy(handle);
|
||||||
}
|
}
|
||||||
Commands::Info => {
|
Commands::Info => {
|
||||||
let handle = craft_initialize(c_config.as_ptr());
|
let handle = init_engine();
|
||||||
if handle.is_null() { eprintln!("初始化失败"); return; }
|
if handle.is_null() { eprintln!("错误: 初始化失败"); return; }
|
||||||
let info = craft_get_license_info(handle);
|
let info_ptr = craft_get_license_info(handle);
|
||||||
if !info.is_null() {
|
if !info_ptr.is_null() {
|
||||||
let i = unsafe { &*info };
|
let info = unsafe { &*info_ptr };
|
||||||
let names = unsafe { std::slice::from_raw_parts(i.feature_names, i.feature_count as usize) };
|
println!("授权状态: {}", if info.is_licensed != 0 { "已授权" } else { "未授权" });
|
||||||
let values = unsafe { std::slice::from_raw_parts(i.feature_values, i.feature_count as usize) };
|
if info.expiration_date > 0 {
|
||||||
println!("授权状态: {}", if i.is_licensed != 0 { "已授权" } else { "未授权" });
|
println!("过期时间戳: {}", info.expiration_date);
|
||||||
if i.expiration_date > 0 {
|
|
||||||
println!("过期时间: {}", i.expiration_date);
|
|
||||||
}
|
}
|
||||||
if i.feature_count > 0 {
|
if info.feature_count > 0 {
|
||||||
println!("功能特性 ({}):", i.feature_count);
|
println!("功能特性 ({}):", info.feature_count);
|
||||||
for idx in 0..i.feature_count as usize {
|
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() {
|
let name = if idx < names.len() && !names[idx].is_null() {
|
||||||
unsafe { std::ffi::CStr::from_ptr(names[idx]) }
|
unsafe { std::ffi::CStr::from_ptr(names[idx]) }
|
||||||
.to_str().unwrap_or("?")
|
.to_str().unwrap_or("?")
|
||||||
} else { "?" };
|
} else { "?" };
|
||||||
let val = if idx < values.len() { values[idx] } else { 0 };
|
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 {
|
} else {
|
||||||
println!("无法获取授权信息");
|
println!("无法获取授权信息");
|
||||||
}
|
}
|
||||||
craft_destroy(handle);
|
craft_destroy(handle);
|
||||||
}
|
}
|
||||||
Commands::Release => {
|
Commands::Release => {
|
||||||
let handle = craft_initialize(c_config.as_ptr());
|
let handle = init_engine();
|
||||||
if handle.is_null() { eprintln!("初始化失败"); return; }
|
if handle.is_null() { eprintln!("错误: 初始化失败"); return; }
|
||||||
let result = craft_release(handle);
|
let result = craft_release(handle);
|
||||||
if result.success != 0 {
|
if result.success != 0 {
|
||||||
println!("✅ 授权已撤销");
|
println!("OK: 授权已撤销");
|
||||||
|
config.sn = None;
|
||||||
|
config.save(&config_path);
|
||||||
} else {
|
} 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);
|
craft_destroy(handle);
|
||||||
}
|
}
|
||||||
Commands::DeviceId => {
|
Commands::DeviceId => {
|
||||||
let fp = device::collect();
|
let fp = device::collect();
|
||||||
|
if cli.json {
|
||||||
|
println!("{{\"device_id\":\"{}\"}}", fp.composite_hash);
|
||||||
|
} else {
|
||||||
println!("设备指纹: {}", fp.composite_hash);
|
println!("设备指纹: {}", fp.composite_hash);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Commands::Heartbeat => {
|
Commands::Heartbeat => {
|
||||||
let handle = craft_initialize(c_config.as_ptr());
|
let handle = init_engine();
|
||||||
if handle.is_null() { eprintln!("初始化失败"); return; }
|
if handle.is_null() { eprintln!("错误: 初始化失败"); return; }
|
||||||
let result = craft_heartbeat(handle);
|
let result = craft_heartbeat(handle);
|
||||||
if result.success != 0 {
|
println!("心跳: {}", if result.success != 0 { "OK" } else { "FAIL" });
|
||||||
println!("✅ 心跳成功");
|
|
||||||
} else {
|
|
||||||
println!("❌ 心跳失败");
|
|
||||||
}
|
|
||||||
craft_destroy(handle);
|
craft_destroy(handle);
|
||||||
}
|
}
|
||||||
|
Commands::Config { action } => {
|
||||||
|
handle_config(action, &mut config, &config_path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_status() {
|
fn init_engine() -> *mut craftlabs_auth_core::CraftContext {
|
||||||
let config = load_or_create_config();
|
let config_json = r#"{"provider":"selfhosted","schemaVersion":1,"scenario":"floating"}"#;
|
||||||
let c_config = CString::new(config).unwrap();
|
let c = CString::new(config_json).unwrap();
|
||||||
let handle = craft_initialize(c_config.as_ptr());
|
craft_initialize(c.as_ptr())
|
||||||
if handle.is_null() { eprintln!("初始化失败"); return; }
|
}
|
||||||
|
|
||||||
let check = craft_check_license(handle);
|
|
||||||
println!("授权状态: {}", if check.success != 0 { "✅ 有效" } else { "❌ 无效" });
|
|
||||||
|
|
||||||
|
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();
|
let fp = device::collect();
|
||||||
|
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!("设备指纹: {}", fp.composite_hash);
|
||||||
|
println!("API 地址: {}", config.api_base_url);
|
||||||
|
if let Some(sn) = &config.sn {
|
||||||
|
println!("绑定 SN: {}", sn);
|
||||||
|
}
|
||||||
|
}
|
||||||
craft_destroy(handle);
|
craft_destroy(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn save_config_with_sn(sn: &str) {
|
fn handle_config(action: &[String], config: &mut Config, path: &PathBuf) {
|
||||||
let config = serde_json::json!({
|
if action.is_empty() {
|
||||||
"provider": "selfhosted",
|
println!("API 地址: {}", config.api_base_url);
|
||||||
"schemaVersion": 1,
|
println!("绑定 SN: {}", config.sn.as_deref().unwrap_or("(无)"));
|
||||||
"scenario": "floating",
|
println!("配置路径: {}", path.display());
|
||||||
"floating": { "sn": sn }
|
return;
|
||||||
});
|
}
|
||||||
let path = get_config_path();
|
match action[0].as_str() {
|
||||||
std::fs::write(&path, serde_json::to_string_pretty(&config).unwrap()).ok();
|
"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 <url>|set-sn <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),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user