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(long, conflicts_with = "all_scopes")]
47 pub service: Option<String>,
48
49 #[arg(long)]
54 pub all_scopes: bool,
55
56 #[arg(short, long, value_enum, default_value = "json")]
58 pub format: OutputFormat,
59}
60
61#[derive(clap::Args, Debug)]
62pub struct StatusArgs {
63 #[arg(long)]
65 pub account: Option<String>,
66
67 #[arg(short, long, value_enum, default_value = "json")]
69 pub format: OutputFormat,
70}
71
72fn token_preview(token: &str) -> String {
73 format!("{}...", &token[..std::cmp::min(20, token.len())])
74}
75
76fn requested_login_scopes(login: &LoginArgs) -> Result<Vec<String>> {
85 if login.all_scopes {
86 return Ok(crate::core::auth::scopes().to_vec());
87 }
88 let mut set = crate::core::auth::login_scopes();
89 if let Some(service) = &login.service {
90 let scopes = crate::core::auth::scopes_for_service(service).ok_or_else(|| {
91 crate::core::error::GrrError::Config(format!(
92 "unknown service `{service}`: `grr api list` shows the ids the index knows"
93 ))
94 })?;
95 set.extend(scopes);
96 set.sort_unstable();
97 set.dedup();
98 }
99 Ok(set)
100}
101
102pub async fn handle_auth_cmd(auth: &GoogleAuth, cmd: AuthCommands) -> Result<()> {
103 match cmd {
104 AuthCommands::Setup(_) => unreachable!("auth setup handled before client construction"),
107 AuthCommands::Login(login) => {
108 let auth = match &login.account {
113 Some(name) => auth.with_account_store(name).await?,
114 None => auth.with_default_account_store().await?,
115 };
116 if login.device {
117 return handle_device_login(&auth, login.format).await;
118 }
119 let scopes = requested_login_scopes(&login)?;
120 let storage = auth.login(&scopes).await?;
121 print_output(
122 &json!({
123 "authenticated": true,
124 "account": login.account,
125 "token_backend": auth.token_backend(),
126 "scopes_requested": scopes.len(),
127 "token_preview": token_preview(&storage.access_token)
128 }),
129 login.format,
130 )?;
131 Ok(())
132 }
133 AuthCommands::Status(status) => {
134 let auth = match &status.account {
135 Some(name) => auth.with_account_store(name).await?,
136 None => auth.with_default_account_store().await?,
137 };
138 let profile = fetch_profile(&auth).await?;
139 print_output(
140 &json!({
141 "authenticated": true,
142 "account": status.account,
143 "email": field(&profile, "emailAddress"),
144 "messages_total": field(&profile, "messagesTotal"),
145 "threads_total": field(&profile, "threadsTotal"),
146 "history_id": field(&profile, "historyId"),
147 }),
148 status.format,
149 )?;
150 Ok(())
151 }
152 }
153}
154
155async fn fetch_profile(auth: &GoogleAuth) -> Result<Value> {
161 let (service, method) =
162 discovery::resolve("gmail.users.getProfile").map_err(anyhow::Error::msg)?;
163 let mut params = serde_json::Map::new();
164 params.insert("userId".into(), json!("me"));
165 api::call_method(
166 Some(auth),
167 service,
168 method,
169 params,
170 api::CallOptions::default(),
171 )
172 .await
173}
174
175fn field(profile: &Value, key: &str) -> Value {
176 profile.get(key).cloned().unwrap_or(Value::Null)
177}
178
179async fn handle_device_login(auth: &GoogleAuth, format: OutputFormat) -> Result<()> {
180 let mut challenge = auth.request_device_code().await?;
181 eprintln!(
184 "Visit {} and enter code: {}",
185 challenge.verification_url, challenge.user_code
186 );
187 loop {
188 tokio::time::sleep(challenge.retry_after()).await;
189 if challenge.is_expired() {
190 anyhow::bail!("device code expired; rerun `grr auth login --device`");
191 }
192 if let Some(storage) = auth.poll_device_code(&mut challenge).await? {
193 print_output(
194 &json!({
195 "authenticated": true,
196 "token_backend": auth.token_backend(),
197 "token_preview": token_preview(&storage.access_token)
198 }),
199 format,
200 )?;
201 return Ok(());
202 }
203 }
204}