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)]
35 pub device: bool,
36
37 #[arg(long)]
40 pub account: Option<String>,
41
42 #[arg(short, long, value_enum, default_value = "json")]
44 pub format: OutputFormat,
45}
46
47#[derive(clap::Args, Debug)]
48pub struct StatusArgs {
49 #[arg(long)]
51 pub account: Option<String>,
52
53 #[arg(short, long, value_enum, default_value = "json")]
55 pub format: OutputFormat,
56}
57
58fn token_preview(token: &str) -> String {
59 format!("{}...", &token[..std::cmp::min(20, token.len())])
60}
61
62pub async fn handle_auth_cmd(auth: &GoogleAuth, cmd: AuthCommands) -> Result<()> {
63 match cmd {
64 AuthCommands::Setup(_) => unreachable!("auth setup handled before client construction"),
67 AuthCommands::Login(login) => {
68 let auth = match &login.account {
73 Some(name) => auth.with_account_store(name).await?,
74 None => auth.with_default_account_store().await?,
75 };
76 if login.device {
77 return handle_device_login(&auth, login.format).await;
78 }
79 let storage = auth.login().await?;
80 print_output(
81 &json!({
82 "authenticated": true,
83 "account": login.account,
84 "token_backend": auth.token_backend(),
85 "token_preview": token_preview(&storage.access_token)
86 }),
87 login.format,
88 )?;
89 Ok(())
90 }
91 AuthCommands::Status(status) => {
92 let auth = match &status.account {
93 Some(name) => auth.with_account_store(name).await?,
94 None => auth.with_default_account_store().await?,
95 };
96 let profile = fetch_profile(&auth).await?;
97 print_output(
98 &json!({
99 "authenticated": true,
100 "account": status.account,
101 "email": field(&profile, "emailAddress"),
102 "messages_total": field(&profile, "messagesTotal"),
103 "threads_total": field(&profile, "threadsTotal"),
104 "history_id": field(&profile, "historyId"),
105 }),
106 status.format,
107 )?;
108 Ok(())
109 }
110 }
111}
112
113async fn fetch_profile(auth: &GoogleAuth) -> Result<Value> {
119 let (service, method) =
120 discovery::resolve("gmail.users.getProfile").map_err(anyhow::Error::msg)?;
121 let mut params = serde_json::Map::new();
122 params.insert("userId".into(), json!("me"));
123 api::call_method(
124 Some(auth),
125 service,
126 method,
127 params,
128 api::CallOptions::default(),
129 )
130 .await
131}
132
133fn field(profile: &Value, key: &str) -> Value {
134 profile.get(key).cloned().unwrap_or(Value::Null)
135}
136
137async fn handle_device_login(auth: &GoogleAuth, format: OutputFormat) -> Result<()> {
138 let mut challenge = auth.request_device_code().await?;
139 println!(
140 "Visit {} and enter code: {}",
141 challenge.verification_url, challenge.user_code
142 );
143 loop {
144 tokio::time::sleep(challenge.retry_after()).await;
145 if challenge.is_expired() {
146 anyhow::bail!("device code expired; rerun `grr auth login --device`");
147 }
148 if let Some(storage) = auth.poll_device_code(&mut challenge).await? {
149 print_output(
150 &json!({
151 "authenticated": true,
152 "token_backend": auth.token_backend(),
153 "token_preview": token_preview(&storage.access_token)
154 }),
155 format,
156 )?;
157 return Ok(());
158 }
159 }
160}