1#![allow(clippy::print_stdout)]
6
7use crate::auth::{device, token};
8use crate::cli::AuthCommand;
9use crate::client::Client;
10use crate::host::Host;
11use anyhow::Context;
12
13pub fn run(cmd: AuthCommand) -> anyhow::Result<()> {
20 match cmd {
21 AuthCommand::Login {
22 hostname,
23 scopes,
24 with_token,
25 } => login(
26 hostname.as_deref().unwrap_or("github.com"),
27 scopes.as_deref(),
28 with_token,
29 ),
30 AuthCommand::Logout { hostname } => logout(hostname.as_deref().unwrap_or("github.com")),
31 AuthCommand::Status { hostname } => status(hostname.as_deref().unwrap_or("github.com")),
32 AuthCommand::SetupGit { hostname } => {
33 setup_git(hostname.as_deref().unwrap_or("github.com"))
34 }
35 AuthCommand::Token {
36 hostname,
37 refresh,
38 scopes,
39 secure,
40 } => token(
41 hostname.as_deref().unwrap_or("github.com"),
42 refresh,
43 &scopes,
44 secure,
45 ),
46 }
47}
48
49fn setup_git(hostname: &str) -> anyhow::Result<()> {
58 use std::process::Command;
59
60 let helper_value = "!gor auth git-credential";
61 let cred_key = format!("credential.https://{hostname}.helper");
62
63 let existing = Command::new("git")
65 .args(["config", "--global", "--get", &cred_key])
66 .output()
67 .ok();
68
69 let already_configured = existing.as_ref().is_some_and(|o| {
70 o.status.success() && String::from_utf8_lossy(&o.stdout).trim() == helper_value
71 });
72
73 if already_configured {
74 println!("git is already configured to use gor for {hostname} credentials");
75 return Ok(());
76 }
77
78 let has_existing = existing.as_ref().is_some_and(|o| {
80 o.status.success() && !String::from_utf8_lossy(&o.stdout).trim().is_empty()
81 });
82
83 if has_existing {
84 println!("$ git config --global --add {cred_key} {helper_value}");
86 let output = Command::new("git")
87 .args(["config", "--global", "--add", &cred_key, helper_value])
88 .output()
89 .context("failed to run git config --add")?;
90 if !output.status.success() {
91 let stderr = String::from_utf8_lossy(&output.stderr);
92 anyhow::bail!("git config failed: {stderr}");
93 }
94 } else {
95 println!("$ git config --global {cred_key} {helper_value}");
97 let output = Command::new("git")
98 .args(["config", "--global", &cred_key, helper_value])
99 .output()
100 .context("failed to run git config")?;
101 if !output.status.success() {
102 let stderr = String::from_utf8_lossy(&output.stderr);
103 anyhow::bail!("git config failed: {stderr}");
104 }
105 }
106
107 let generic_key = "credential.helper";
109 let generic_existing = Command::new("git")
110 .args(["config", "--global", "--get", generic_key])
111 .output()
112 .ok();
113
114 let has_generic = generic_existing.as_ref().is_some_and(|o| {
115 o.status.success() && !String::from_utf8_lossy(&o.stdout).trim().is_empty()
116 });
117
118 if !has_generic {
119 println!("$ git config --global {generic_key} {helper_value}");
120 let output = Command::new("git")
121 .args(["config", "--global", generic_key, helper_value])
122 .output()
123 .context("failed to run git config")?;
124 if !output.status.success() {
125 let stderr = String::from_utf8_lossy(&output.stderr);
126 anyhow::bail!("git config failed: {stderr}");
127 }
128 }
129
130 println!("✓ git is now configured to use gor for {hostname} credentials");
131 Ok(())
132}
133
134fn login(hostname: &str, scopes: Option<&str>, with_token: bool) -> anyhow::Result<()> {
136 let host = Host::new(hostname);
137
138 if with_token {
139 let token = token::read_token_from_stdin()
141 .map_err(|e| anyhow::anyhow!("failed to read token from stdin: {e}"))?;
142 if token.is_empty() {
143 anyhow::bail!("no token provided on stdin");
144 }
145
146 let login = token::verify_token(&host, &token)
148 .map_err(|e| anyhow::anyhow!("token verification failed: {e}"))?;
149
150 let client = Client::with_token(hostname, &token)?;
152 client.save_token()?;
153 client.save_user(&login)?;
154
155 println!("Authenticated as {login}");
156 return Ok(());
157 }
158
159 let device_resp = device::request_device_code(&host, scopes)
161 .map_err(|e| anyhow::anyhow!("failed to request device code: {e}"))?;
162
163 device::display_instructions(&device_resp.user_code, &device_resp.verification_uri);
165
166 let access_token = device::poll_for_token(
168 &host,
169 &device_resp.device_code,
170 device_resp.interval,
171 device_resp.expires_in,
172 )
173 .map_err(|e| anyhow::anyhow!("{e}"))?;
174
175 let login = token::verify_token(&host, &access_token)
177 .map_err(|e| anyhow::anyhow!("token verification failed: {e}"))?;
178
179 let client = Client::with_token(hostname, &access_token)?;
181 client.save_token()?;
182 client.save_user(&login)?;
183
184 println!("Authenticated as {login}");
185 Ok(())
186}
187
188fn logout(hostname: &str) -> anyhow::Result<()> {
190 let client = Client::new(hostname)?;
191 if !client.is_authenticated() {
192 println!("Not logged in to {hostname}");
193 return Ok(());
194 }
195
196 crate::keyring_store::delete_token(hostname)
197 .map_err(|e| anyhow::anyhow!("failed to remove token: {e}"))?;
198 println!("Logged out of {hostname}");
199 Ok(())
200}
201
202fn token(hostname: &str, refresh: bool, scopes: &[String], secure: bool) -> anyhow::Result<()> {
213 if refresh {
214 let host = Host::new(hostname);
215
216 let scope_str = if scopes.is_empty() {
218 None
219 } else {
220 Some(scopes.join(","))
221 };
222
223 let device_resp = device::request_device_code(&host, scope_str.as_deref())
225 .map_err(|e| anyhow::anyhow!("failed to request device code: {e}"))?;
226
227 device::display_instructions(&device_resp.user_code, &device_resp.verification_uri);
229
230 let access_token = device::poll_for_token(
232 &host,
233 &device_resp.device_code,
234 device_resp.interval,
235 device_resp.expires_in,
236 )
237 .map_err(|e| anyhow::anyhow!("{e}"))?;
238
239 crate::keyring_store::set_token(hostname, &access_token)
241 .map_err(|e| anyhow::anyhow!("failed to store token: {e}"))?;
242
243 print_token(&access_token, secure);
245 return Ok(());
246 }
247
248 let token = crate::keyring_store::get_token(hostname)
250 .map_err(|e| anyhow::anyhow!("failed to read token: {e}"))?
251 .or_else(|| std::env::var("GITHUB_TOKEN").ok())
252 .or_else(|| std::env::var("GH_TOKEN").ok());
253
254 match token {
255 Some(t) => {
256 print_token(&t, secure);
257 Ok(())
258 }
259 None => anyhow::bail!(
260 "no token found for {hostname}. Run 'gor auth login' first or set GITHUB_TOKEN."
261 ),
262 }
263}
264
265fn print_token(token: &str, secure: bool) {
271 if secure && token.len() > 8 {
272 let first4 = &token[..4];
273 let last4 = &token[token.len() - 4..];
274 println!("{first4}...{last4}");
275 } else {
276 println!("{token}");
277 }
278}
279
280fn status(hostname: &str) -> anyhow::Result<()> {
282 let client = Client::new(hostname)?;
283 match client.token() {
284 Some(token) => {
285 let host = Host::new(hostname);
286 match token::verify_token(&host, token) {
287 Ok(login) => {
288 println!("Logged in to {hostname} as {login}");
289 }
290 Err(_) => {
291 println!("Logged in to {hostname} but token is invalid or expired");
292 }
293 }
294 }
295 None => {
296 println!("Not logged in to {hostname}");
297 }
298 }
299 Ok(())
300}