use std::time::Duration;
use anyhow::{bail, Context, Result};
use owo_colors::OwoColorize;
use crate::api::client::ApiClient;
use crate::api::models::{CollectedAuthorization, StartedAuthorization};
use crate::config::{normalise_url, Config, Profile};
use crate::render::theme;
pub async fn run(url: &str, profile_name: &str, open_browser: bool) -> Result<()> {
let base = normalise_url(url);
let client = ApiClient::new(&base, None)?;
let started: StartedAuthorization = client
.post(
"/api/auth/cli/authorizations",
&serde_json::json!({
"clientName": "fundaia",
"clientHost": machine_name(),
}),
)
.await
.with_context(|| format!("could not reach {base}"))?;
println!();
println!(
" {} {}",
"Código:".style(theme::muted()),
started.user_code.style(theme::strong())
);
println!(
" {} {}",
"Página:".style(theme::muted()),
started.verification_uri.style(theme::accent())
);
println!();
if open_browser {
match open::that_detached(&started.verification_uri_complete) {
Ok(()) => println!(" {}", "Abriendo el navegador…".style(theme::muted())),
Err(_) => println!(
" {}",
"Abre esa página para autorizar.".style(theme::muted())
),
}
} else {
println!(
" {}",
"Abre esa página para autorizar.".style(theme::muted())
);
}
println!(" {}", "Esperando…".style(theme::muted()));
let token = wait_for_token(&client, &started).await?;
let mut config = Config::load()?;
config.set(
profile_name,
Profile {
url: base.clone(),
token: token.clone(),
},
);
config.current = Some(profile_name.to_owned());
let path = config.save()?;
let identity = ApiClient::new(&base, Some(token))?
.whoami()
.await
.ok()
.flatten();
let who = identity
.map(|principal| principal.display_name)
.unwrap_or_else(|| "esta instancia".to_owned());
println!();
println!(
" {} Conectado a {} como {}",
theme::CHECK.style(theme::success()),
base.style(theme::strong()),
who.style(theme::strong())
);
println!(
" {} {}",
"Guardado en".style(theme::muted()),
path.display().style(theme::muted())
);
println!();
Ok(())
}
async fn wait_for_token(client: &ApiClient, started: &StartedAuthorization) -> Result<String> {
let interval = Duration::from_secs(started.interval_seconds.max(1));
loop {
tokio::time::sleep(interval).await;
let collected: CollectedAuthorization = client
.post(
"/api/auth/cli/collect",
&serde_json::json!({ "deviceSecret": started.device_secret }),
)
.await?;
match collected.status.as_str() {
"pending" => continue,
"approved" => {
return collected.token.ok_or_else(|| {
anyhow::anyhow!("the server approved the request but sent no token")
})
}
"denied" => bail!("the request was rejected in the browser"),
"expired" => bail!("the code expired — run `fundaia login` again"),
other => bail!("the server answered with an unexpected status: {other}"),
}
}
}
fn machine_name() -> String {
hostname::get()
.ok()
.and_then(|name| name.into_string().ok())
.unwrap_or_else(|| "una máquina sin nombre".to_owned())
}
pub fn logout(profile_name: Option<&str>) -> Result<()> {
let mut config = Config::load()?;
let name = profile_name
.map(str::to_owned)
.or_else(|| config.current.clone())
.unwrap_or_else(|| "default".to_owned());
if config.profiles.remove(&name).is_none() {
bail!("there is no profile called `{name}`");
}
if config.current.as_deref() == Some(name.as_str()) {
config.current = config.profiles.keys().next().cloned();
}
config.save()?;
println!(
" {} Perfil `{name}` olvidado en esta máquina.",
theme::CHECK.style(theme::success())
);
println!(
" {}",
"El token sigue siendo válido: revócalo en la web si quieres retirarlo del todo."
.style(theme::muted())
);
Ok(())
}