use anyhow::Result;
use zeptoclaw::config::Config;
use zeptoclaw::security::PairingManager;
use super::PairAction;
pub(crate) async fn cmd_pair(action: PairAction) -> Result<()> {
let config = Config::load()?;
match action {
PairAction::New => cmd_pair_new(&config).await,
PairAction::List => cmd_pair_list(&config).await,
PairAction::Revoke { device } => cmd_pair_revoke(&config, &device).await,
}
}
async fn cmd_pair_new(config: &Config) -> Result<()> {
let mut mgr = PairingManager::new(config.pairing.max_attempts, config.pairing.lockout_secs);
let code = mgr.generate_pairing_code();
println!("Pairing code: {}", code);
println!();
println!("This code is valid for 5 minutes.");
println!("Use it to pair a device by sending:");
println!(" Authorization: Bearer <token-from-pairing>");
println!();
println!("Waiting for device to pair (press Ctrl+C to cancel)...");
println!();
println!("To complete pairing from another terminal or device:");
println!(
" curl -X POST http://localhost:{}/pair \\",
config.gateway.port
);
println!(" -H 'Content-Type: application/json' \\");
println!(
" -d '{{\"code\": \"{}\", \"device_name\": \"my-device\"}}'",
code
);
Ok(())
}
async fn cmd_pair_list(config: &Config) -> Result<()> {
let mgr = PairingManager::new(config.pairing.max_attempts, config.pairing.lockout_secs);
let devices = mgr.list_devices();
if devices.is_empty() {
println!("No paired devices.");
return Ok(());
}
println!("{:<20} {:<24} {:<24}", "DEVICE", "PAIRED AT", "LAST SEEN");
println!("{}", "-".repeat(68));
for device in &devices {
let paired = format_timestamp(device.paired_at);
let seen = format_timestamp(device.last_seen);
println!("{:<20} {:<24} {:<24}", device.name, paired, seen);
}
println!();
println!("{} device(s) paired.", devices.len());
Ok(())
}
async fn cmd_pair_revoke(config: &Config, device_name: &str) -> Result<()> {
let mut mgr = PairingManager::new(config.pairing.max_attempts, config.pairing.lockout_secs);
if mgr.revoke(device_name) {
println!("Device '{}' has been revoked.", device_name);
} else {
println!("Device '{}' not found.", device_name);
}
Ok(())
}
fn format_timestamp(ts: u64) -> String {
if ts == 0 {
return "never".to_string();
}
let duration = std::time::Duration::from_secs(ts);
let datetime = std::time::UNIX_EPOCH + duration;
match datetime.elapsed() {
Ok(ago) => {
let secs = ago.as_secs();
if secs < 60 {
format!("{}s ago", secs)
} else if secs < 3600 {
format!("{}m ago", secs / 60)
} else if secs < 86400 {
format!("{}h ago", secs / 3600)
} else {
format!("{}d ago", secs / 86400)
}
}
Err(_) => "just now".to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_timestamp_zero() {
assert_eq!(format_timestamp(0), "never");
}
#[test]
fn test_format_timestamp_recent() {
use std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let result = format_timestamp(now);
assert!(
result.contains("ago") || result == "just now",
"Unexpected: {}",
result
);
}
}