1use crate::commands::api;
9use crate::core::auth::GoogleAuth;
10use crate::discovery;
11use crate::output::{OutputFormat, print_output};
12use anyhow::Result;
13use clap::Subcommand;
14use serde_json::{Value, json};
15
16#[derive(Subcommand, Debug)]
17pub enum AuthCommands {
18 Login(LoginArgs),
21 Status(StatusArgs),
23 Setup(crate::commands::setup::SetupArgs),
26}
27
28#[derive(clap::Args, Debug)]
29pub struct LoginArgs {
30 #[arg(long)]
32 pub device: bool,
33
34 #[arg(long)]
37 pub account: Option<String>,
38
39 #[arg(short, long, value_enum, default_value = "json")]
41 pub format: OutputFormat,
42}
43
44#[derive(clap::Args, Debug)]
45pub struct StatusArgs {
46 #[arg(long)]
48 pub account: Option<String>,
49
50 #[arg(short, long, value_enum, default_value = "json")]
52 pub format: OutputFormat,
53}
54
55fn token_preview(token: &str) -> String {
56 format!("{}...", &token[..std::cmp::min(20, token.len())])
57}
58
59pub async fn handle_auth_cmd(auth: &GoogleAuth, cmd: AuthCommands) -> Result<()> {
60 match cmd {
61 AuthCommands::Setup(_) => unreachable!("auth setup handled before client construction"),
64 AuthCommands::Login(login) => {
65 let auth = match &login.account {
70 Some(name) => auth.with_account_store(name).await?,
71 None => auth.with_default_account_store().await?,
72 };
73 if login.device {
74 return handle_device_login(&auth, login.format).await;
75 }
76 let storage = auth.login().await?;
77 print_output(
78 &json!({
79 "authenticated": true,
80 "account": login.account,
81 "token_backend": auth.token_backend(),
82 "token_preview": token_preview(&storage.access_token)
83 }),
84 login.format,
85 )?;
86 Ok(())
87 }
88 AuthCommands::Status(status) => {
89 let auth = match &status.account {
90 Some(name) => auth.with_account_store(name).await?,
91 None => auth.with_default_account_store().await?,
92 };
93 let profile = fetch_profile(&auth).await?;
94 print_output(
95 &json!({
96 "authenticated": true,
97 "account": status.account,
98 "email": field(&profile, "emailAddress"),
99 "messages_total": field(&profile, "messagesTotal"),
100 "threads_total": field(&profile, "threadsTotal"),
101 "history_id": field(&profile, "historyId"),
102 }),
103 status.format,
104 )?;
105 Ok(())
106 }
107 }
108}
109
110async fn fetch_profile(auth: &GoogleAuth) -> Result<Value> {
116 let (service, method) =
117 discovery::resolve("gmail.users.getProfile").map_err(anyhow::Error::msg)?;
118 let mut params = serde_json::Map::new();
119 params.insert("userId".into(), json!("me"));
120 api::call_method(
121 Some(auth),
122 service,
123 method,
124 params,
125 api::CallOptions::default(),
126 )
127 .await
128}
129
130fn field(profile: &Value, key: &str) -> Value {
131 profile.get(key).cloned().unwrap_or(Value::Null)
132}
133
134async fn handle_device_login(auth: &GoogleAuth, format: OutputFormat) -> Result<()> {
135 let mut challenge = auth.request_device_code().await?;
136 println!(
137 "Visit {} and enter code: {}",
138 challenge.verification_url, challenge.user_code
139 );
140 loop {
141 tokio::time::sleep(challenge.retry_after()).await;
142 if challenge.is_expired() {
143 anyhow::bail!("device code expired; rerun `grr auth login --device`");
144 }
145 if let Some(storage) = auth.poll_device_code(&mut challenge).await? {
146 print_output(
147 &json!({
148 "authenticated": true,
149 "token_backend": auth.token_backend(),
150 "token_preview": token_preview(&storage.access_token)
151 }),
152 format,
153 )?;
154 return Ok(());
155 }
156 }
157}