garmin_cli/cli/commands/
auth.rs1use crate::client::{OAuth1Token, OAuth2Token, SsoClient};
4use crate::config::CredentialStore;
5use crate::error::{GarminError, Result};
6use std::io::{self, Write};
7
8pub async fn login(email: Option<String>, profile: Option<String>) -> Result<()> {
10 let store = CredentialStore::new(profile.clone())?;
11
12 if store.has_credentials() {
14 if let Some((_, oauth2)) = store.load_tokens()? {
15 if !oauth2.is_expired() {
16 println!("Already logged in. Use 'garmin auth logout' to log out first.");
17 return Ok(());
18 }
19 }
20 }
21
22 let email = match email {
24 Some(e) => e,
25 None => {
26 print!("Email: ");
27 io::stdout().flush()?;
28 let mut input = String::new();
29 io::stdin().read_line(&mut input)?;
30 input.trim().to_string()
31 }
32 };
33
34 let password = rpassword_prompt("Password: ")?;
36
37 println!("Logging in...");
38
39 let mut sso_client = SsoClient::new(None)?;
41 let (oauth1, oauth2) = sso_client
42 .login(&email, &password, Some(prompt_mfa))
43 .await?;
44
45 store.save_tokens(&oauth1, &oauth2)?;
47
48 println!("Successfully logged in!");
49 println!("Profile: {}", store.profile());
50
51 Ok(())
52}
53
54pub async fn logout(profile: Option<String>) -> Result<()> {
56 let store = CredentialStore::new(profile)?;
57
58 if !store.has_credentials() {
59 println!("Not logged in.");
60 return Ok(());
61 }
62
63 store.clear()?;
64 let _ = store.delete_secret_from_keyring();
66
67 println!("Successfully logged out.");
68 Ok(())
69}
70
71pub async fn status(profile: Option<String>) -> Result<()> {
73 let store = CredentialStore::new(profile)?;
74
75 if !store.has_credentials() {
76 println!("Status: Not logged in");
77 println!("Run 'garmin auth login' to authenticate.");
78 return Ok(());
79 }
80
81 match store.load_tokens()? {
82 Some((oauth1, oauth2)) => {
83 println!("Status: Logged in");
84 println!("Profile: {}", store.profile());
85 println!("Domain: {}", oauth1.domain);
86
87 if oauth2.is_expired() {
88 println!("Access Token: Expired (will refresh on next request)");
89 } else {
90 let expires_in = oauth2.expires_at - chrono::Utc::now().timestamp();
91 if expires_in > 3600 {
92 println!(
93 "Access Token: Valid (expires in {} hours)",
94 expires_in / 3600
95 );
96 } else if expires_in > 60 {
97 println!(
98 "Access Token: Valid (expires in {} minutes)",
99 expires_in / 60
100 );
101 } else {
102 println!("Access Token: Valid (expires in {} seconds)", expires_in);
103 }
104 }
105
106 if oauth1.mfa_token.is_some() {
107 println!("MFA: Enabled");
108 }
109 }
110 None => {
111 println!("Status: Credentials corrupted");
112 println!("Run 'garmin auth logout' then 'garmin auth login' to fix.");
113 }
114 }
115
116 Ok(())
117}
118
119pub async fn refresh_token(store: &CredentialStore) -> Result<(OAuth1Token, OAuth2Token)> {
121 let (oauth1, oauth2) = store.load_tokens()?.ok_or(GarminError::NotAuthenticated)?;
122
123 if !oauth2.is_expired() {
124 return Ok((oauth1, oauth2));
125 }
126
127 println!("Refreshing access token...");
128 let sso_client = SsoClient::new(Some(&oauth1.domain))?;
129 let new_oauth2 = sso_client.refresh_oauth2(&oauth1).await?;
130
131 store.save_oauth2(&new_oauth2)?;
132
133 Ok((oauth1, new_oauth2))
134}
135
136fn rpassword_prompt(prompt: &str) -> Result<String> {
138 print!("{}", prompt);
139 io::stdout().flush()?;
140
141 let password =
143 rpassword::read_password().map_err(|e| GarminError::Io(io::Error::other(e.to_string())))?;
144
145 Ok(password)
146}
147
148fn prompt_mfa() -> String {
150 print!("MFA Code: ");
151 io::stdout().flush().unwrap();
152
153 let mut input = String::new();
154 io::stdin().read_line(&mut input).unwrap();
155 input.trim().to_string()
156}
157
158#[cfg(test)]
159mod tests {
160 }