use std::io::{self, Write};
use super::flows::{browser_flow, device_flow};
use super::{auth_error, OAuthConfig};
use crate::canonical::{CanonicalError, ErrorKind};
use crate::cli::{Args, Flags};
use crate::config::errors::ConfigError;
use crate::config::{config_path, defaults, partial_from_env, read_config_file, ResolvedConfig};
pub trait BrowserLauncher {
fn open(&self, url: &str) -> io::Result<()>;
}
pub trait CodeReceiver {
fn bind(&self, port: Option<u16>) -> io::Result<u16>;
fn await_query(&self) -> io::Result<String>;
}
pub trait Pacer {
fn wait(&self, secs: u64);
}
pub struct LoginIo<'a> {
pub stdout: &'a mut dyn Write,
pub stderr: &'a mut dyn Write,
pub transport: &'a dyn crate::transport::Transport,
pub store: &'a dyn crate::store::CredStore,
pub clock: &'a dyn crate::store::Clock,
pub browser: &'a dyn BrowserLauncher,
pub receiver: &'a dyn CodeReceiver,
pub pacer: &'a dyn Pacer,
pub verifier: &'a str,
pub state: &'a str,
}
pub fn login(args: &Args, io: &mut LoginIo) -> u8 {
match run_login(args, io) {
Ok(Some(provider)) => {
let _ = writeln!(io.stderr, "logged in to `{provider}`");
0
}
Ok(None) => 0,
Err(e) => {
let _ = writeln!(io.stderr, "{}", e.message);
e.exit_code()
}
}
}
fn run_login(args: &Args, io: &mut LoginIo) -> Result<Option<String>, CanonicalError> {
let flags = crate::cli::parse_args(&args.argv)?;
if flags.help {
crate::run::emit(io.stdout, crate::run::HELP);
return Ok(None);
}
if flags.skill {
crate::run::emit(io.stdout, crate::run::SKILL);
return Ok(None);
}
if flags.version {
crate::run::emit(io.stdout, crate::run::VERSION_LINE);
return Ok(None);
}
let browser = flags.browser;
let (provider, cfg) = resolve_oauth(flags, args)?;
let cred = if browser {
browser_flow(&cfg, io)?
} else {
device_flow(&cfg, io)?
};
io.store.put(&provider, &cred).map_err(persist_failed)?;
Ok(Some(provider))
}
fn resolve_oauth(flags: Flags, args: &Args) -> Result<(String, OAuthConfig), CanonicalError> {
let file = read_config_file(&config_path(flags.config_path, &args.env))?;
let env = partial_from_env(&args.env).map_err(CanonicalError::from)?;
let merged = flags.config.or(env).or(file).or(defaults());
if merged.provider.is_none() {
return Err(ConfigError::NoProvider.into());
}
let resolved: ResolvedConfig = merged
.into_resolved(None, None)
.map_err(CanonicalError::from)?;
let name = resolved.provider.name.clone();
let oauth = resolved.provider.oauth.ok_or_else(|| {
config_err(format!(
"provider `{name}` has no `oauth` config; add an `oauth` block to its row"
))
})?;
Ok((name, oauth))
}
pub(super) fn config_err(message: String) -> CanonicalError {
CanonicalError {
kind: ErrorKind::Config,
message,
provider_detail: None,
retry_after_seconds: None,
}
}
fn persist_failed(e: io::Error) -> CanonicalError {
auth_error(&format!("could not persist credential: {e}"))
}