use std::time::{Duration, Instant};
pub enum Decision {
Pending,
Approved(String),
Failed(String),
}
pub fn decide(http_status: u16, body: &str) -> Decision {
match http_status {
428 => Decision::Pending,
200 => {
match serde_json::from_str::<serde_json::Value>(body)
.ok()
.and_then(|v| v["api_key"].as_str().map(|s| s.to_string()))
{
Some(k) => Decision::Approved(k),
None => Decision::Failed("approved but no api_key in response".into()),
}
}
410 => Decision::Failed("code expired — run `zc init` again".into()),
403 => Decision::Failed("authorization was denied".into()),
404 => Decision::Failed("session not found".into()),
s => Decision::Failed(format!("unexpected status {s}")),
}
}
pub fn print_connect_guidance() {
eprintln!();
eprintln!("To connect, grab your API key from the dashboard (Profile → API Keys), then:");
eprintln!();
eprintln!(" # pick your environment");
eprintln!(
" export ZAKURO_API_URL={} # production",
crate::credentials::PROD_API_URL
);
eprintln!(
" export ZAKURO_API_URL={} # staging",
crate::credentials::STAGING_API_URL
);
eprintln!();
eprintln!(" export ZAKURO_API_KEY=<your key>");
eprintln!(" zc connect # join the mesh (add --docker to use Docker)");
}
#[derive(Debug, PartialEq)]
pub(crate) enum Plan {
ConnectMesh,
ReportOnly,
SignIn,
}
pub(crate) fn plan(has_key: bool, force: bool, no_connect: bool) -> Plan {
if !has_key || force {
return Plan::SignIn;
}
if no_connect {
return Plan::ReportOnly;
}
Plan::ConnectMesh
}
fn connect_mesh() -> bool {
match crate::vpn::ensure(crate::vpn::connector::Preference::Auto) {
Ok(crate::vpn::MeshAccess::Host) => {
println!("✓ Mesh reachable from this host. Try `zc me`.");
true
}
Ok(crate::vpn::MeshAccess::Proxy(p)) => {
println!("✓ Mesh reachable via VPN container proxy ({p}). Try `zc me`.");
true
}
Err(e) => {
eprintln!("✗ VPN setup failed: {e}");
eprintln!(" Retry with `zc connect`.");
false
}
}
}
pub fn run() -> i32 {
crate::credentials::load_into_env();
let api_url =
if std::env::args().any(|a| a == "--staging") && std::env::var("ZAKURO_API_URL").is_err() {
crate::credentials::STAGING_API_URL.to_string()
} else {
crate::credentials::default_api_url()
};
let has_key = std::env::var("ZAKURO_API_KEY")
.map(|k| !k.trim().is_empty())
.unwrap_or(false);
let force = std::env::args().any(|a| a == "--force");
let no_connect = std::env::args().any(|a| a == "--no-connect");
match plan(has_key, force, no_connect) {
Plan::ReportOnly => {
eprintln!("Already signed in (→ {api_url}).");
eprintln!(" Connect with: zc connect");
eprintln!(" Re-auth with: zc login --force");
return 0;
}
Plan::ConnectMesh => {
eprintln!("Already signed in (→ {api_url}).");
eprintln!(" Connecting to the zakuro mesh…");
connect_mesh();
return 0;
}
Plan::SignIn => {}
}
let start_url = format!("{}/api/auth/cli/start", api_url.trim_end_matches('/'));
let start_send = ureq::post(&start_url)
.config()
.http_status_as_error(false)
.build()
.send("");
let start_resp = match start_send {
Ok(r) if r.status().as_u16() == 200 => r,
Ok(r) if r.status().as_u16() == 404 => {
eprintln!(
"Browser sign-in isn't available on {api_url} (device-code endpoint not found)."
);
print_connect_guidance();
return if has_key { 0 } else { 1 };
}
Ok(r) => {
eprintln!(
"Sign-in could not start on {api_url}: HTTP {}",
r.status().as_u16()
);
print_connect_guidance();
return 1;
}
Err(e) => {
eprintln!("Could not reach {api_url}: {e}");
print_connect_guidance();
return 1;
}
};
let start_body = match start_resp.into_body().read_to_string() {
Ok(s) => s,
Err(e) => {
eprintln!("Failed to read start response: {e}");
return 1;
}
};
let start: serde_json::Value = match serde_json::from_str(&start_body) {
Ok(v) => v,
Err(e) => {
eprintln!("Failed to parse start response: {e}");
return 1;
}
};
let device_code = start["device_code"].as_str().unwrap_or("").to_string();
let user_code = start["user_code"].as_str().unwrap_or("");
let verification_uri = start["verification_uri"].as_str().unwrap_or("");
let interval = start["interval"].as_u64().unwrap_or(5);
let expires_in = start["expires_in"].as_u64().unwrap_or(300);
println!("\n To sign in, open: {verification_uri}");
println!(" and confirm the code: {user_code}\n");
let _ = open_browser(verification_uri);
let poll_url = format!("{}/api/auth/cli/poll", api_url.trim_end_matches('/'));
let poll_start = Instant::now();
loop {
std::thread::sleep(Duration::from_secs(interval));
if poll_start.elapsed().as_secs() >= expires_in {
eprintln!("\nCode expired — run `zc init` again");
return 1;
}
let payload = serde_json::to_string(&serde_json::json!({"device_code": &device_code}))
.unwrap_or_default();
let (status, resp_body) = match ureq::post(&poll_url)
.config()
.http_status_as_error(false)
.build()
.header("Content-Type", "application/json")
.send(payload.as_str())
{
Ok(r) => {
let s = r.status().as_u16();
let b = r.into_body().read_to_string().unwrap_or_default();
(s, b)
}
Err(_) => continue, };
match decide(status, &resp_body) {
Decision::Pending => {
print!(".");
use std::io::Write;
let _ = std::io::stdout().flush();
}
Decision::Approved(key) => {
if let Err(e) = crate::credentials::save(&key, Some(&api_url)) {
eprintln!("\nsigned in but could not write credentials: {e}");
return 1;
}
std::env::set_var("ZAKURO_API_KEY", &key);
std::env::set_var("ZAKURO_API_URL", &api_url);
println!("\n✓ Signed in. Key saved to ~/.zakuro/credentials.");
println!(" Connecting to the zakuro mesh…");
return if connect_mesh() { 0 } else { 1 };
}
Decision::Failed(msg) => {
eprintln!("\n{msg}");
return 1;
}
}
}
}
pub fn open_browser(url: &str) -> std::io::Result<()> {
let cmd = if cfg!(target_os = "macos") {
"open"
} else {
"xdg-open"
};
std::process::Command::new(cmd)
.arg(url)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
.map(|_| ())
}
#[cfg(test)]
mod tests {
#[test]
fn decision_maps_statuses() {
assert!(matches!(super::decide(428, ""), super::Decision::Pending));
assert!(matches!(
super::decide(200, "{\"api_key\":\"zk_1_x\"}"),
super::Decision::Approved(ref k) if k == "zk_1_x"
));
assert!(matches!(super::decide(410, ""), super::Decision::Failed(_)));
}
#[test]
fn already_signed_in_connects_the_mesh() {
assert_eq!(super::plan(true, false, false), super::Plan::ConnectMesh);
}
#[test]
fn no_key_signs_in() {
assert_eq!(super::plan(false, false, false), super::Plan::SignIn);
}
#[test]
fn force_signs_in_again() {
assert_eq!(super::plan(true, true, false), super::Plan::SignIn);
assert_eq!(super::plan(true, true, true), super::Plan::SignIn);
}
#[test]
fn no_connect_keeps_the_old_report_only_behaviour() {
assert_eq!(super::plan(true, false, true), super::Plan::ReportOnly);
}
#[test]
fn blank_key_is_not_signed_in() {
assert_eq!(super::plan(false, false, true), super::Plan::SignIn);
}
}