use crate::client::{OAuth1Token, OAuth2Token, SsoClient};
use crate::config::CredentialStore;
use crate::error::{GarminError, Result};
use std::io::{self, Write};
pub async fn login(email: Option<String>, profile: Option<String>) -> Result<()> {
let store = CredentialStore::new(profile.clone())?;
if store.has_credentials() {
if let Some((_, oauth2)) = store.load_tokens()? {
if !oauth2.is_expired() {
println!("Already logged in. Use 'garmin auth logout' to log out first.");
return Ok(());
}
}
}
let email = match email {
Some(e) => e,
None => {
print!("Email: ");
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
input.trim().to_string()
}
};
let password = rpassword_prompt("Password: ")?;
println!("Logging in...");
let mut sso_client = SsoClient::new()?;
let (oauth1, oauth2) = sso_client
.login(&email, &password, Some(prompt_mfa))
.await?;
store.save_tokens(&oauth1, &oauth2)?;
println!("Successfully logged in!");
println!("Profile: {}", store.profile());
Ok(())
}
pub async fn logout(profile: Option<String>) -> Result<()> {
let store = CredentialStore::new(profile)?;
if !store.has_credentials() {
println!("Not logged in.");
return Ok(());
}
store.clear()?;
let _ = store.delete_secret_from_keyring();
println!("Successfully logged out.");
Ok(())
}
pub async fn status(profile: Option<String>) -> Result<()> {
let store = CredentialStore::new(profile)?;
if !store.has_credentials() {
println!("Status: Not logged in");
println!("Run 'garmin auth login' to authenticate.");
return Ok(());
}
match store.load_tokens()? {
Some((oauth1, oauth2)) => {
println!("Status: Logged in");
println!("Profile: {}", store.profile());
if oauth2.is_expired() {
println!("Access Token: Expired (will refresh on next request)");
} else {
let expires_in = oauth2.expires_at - chrono::Utc::now().timestamp();
if expires_in > 3600 {
println!(
"Access Token: Valid (expires in {} hours)",
expires_in / 3600
);
} else if expires_in > 60 {
println!(
"Access Token: Valid (expires in {} minutes)",
expires_in / 60
);
} else {
println!("Access Token: Valid (expires in {} seconds)", expires_in);
}
}
if oauth1.mfa_token.is_some() {
println!("MFA: Enabled");
}
}
None => {
println!("Status: Credentials corrupted");
println!("Run 'garmin auth logout' then 'garmin auth login' to fix.");
}
}
Ok(())
}
pub async fn refresh_token(store: &CredentialStore) -> Result<(OAuth1Token, OAuth2Token)> {
let (oauth1, oauth2) = store.load_tokens()?.ok_or(GarminError::NotAuthenticated)?;
if !oauth2.is_expired() {
return Ok((oauth1, oauth2));
}
println!("Refreshing access token...");
let sso_client = SsoClient::new()?;
let new_oauth2 = sso_client.refresh_oauth2(&oauth1).await?;
store.save_oauth2(&new_oauth2)?;
Ok((oauth1, new_oauth2))
}
fn rpassword_prompt(prompt: &str) -> Result<String> {
print!("{}", prompt);
io::stdout().flush()?;
let password =
rpassword::read_password().map_err(|e| GarminError::Io(io::Error::other(e.to_string())))?;
Ok(password)
}
fn prompt_mfa() -> String {
print!("MFA Code: ");
io::stdout().flush().unwrap();
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();
input.trim().to_string()
}
#[cfg(test)]
mod tests {
}