1use crate::{Login, error::GithubError};
2use colored::Colorize;
3use serde_json::Value;
4use std::{
5 collections::HashMap,
6 thread,
7 time::{self, Duration},
8};
9
10pub fn get_login() -> Result<Login, GithubError> {
11 let client_id = String::from("Ov23liAXHnUzobAF9AuF");
12 let client = reqwest::blocking::Client::new();
13
14 let device_code = get_device_code(&client, &client_id)?;
15 let token = poll_for_auth(&client, &device_code, &client_id)?;
16 let username = query_username(&client, &token)?;
17 let email = query_email(&client, &token)?;
18
19 let login = Login::new(username, String::from("github.com"), email);
20 login.set_password(&token)?;
21 Ok(login)
22}
23
24fn get_device_code(
25 reqwest_client: &reqwest::blocking::Client,
26 client_id: &str,
27) -> Result<String, GithubError> {
28 let params = [
29 ("scope", "repo read:user user:email"),
30 ("client_id", client_id),
31 ];
32 let response: HashMap<String, Value> = reqwest_client
33 .post("https://github.com/login/device/code")
34 .header("Accept", "application/vnd.github+json ")
35 .form(¶ms)
36 .send()?
37 .json()?;
38
39 eprintln!(
40 "Copy this code <{}> and follow the instructions at the link\n\t{}",
41 response
42 .get("user_code")
43 .ok_or(GithubError::MissingField(String::from("user_code")))?
44 .as_str()
45 .ok_or(GithubError::InvalidField(String::from("user_code")))?
46 .green()
47 .bold(),
48 response
49 .get("verification_uri")
50 .ok_or(GithubError::MissingField(String::from("verification_uri")))?
51 .as_str()
52 .ok_or(GithubError::InvalidField(String::from("verification_uri")))?
53 .blue()
54 .underline()
55 );
56
57 Ok(response
59 .get("device_code")
60 .ok_or(GithubError::MissingField(String::from("device_code")))?
61 .as_str()
62 .ok_or(GithubError::InvalidField(String::from("device_code")))?
63 .to_string())
64}
65
66fn poll_for_auth(
67 reqwest_client: &reqwest::blocking::Client,
68 device_code: &str,
69 client_id: &str,
70) -> Result<String, GithubError> {
71 let poll_params = [
72 ("device_code", device_code),
73 ("client_id", client_id),
74 ("grant_type", "urn:ietf:params:oauth:grant-type:device_code"),
75 ];
76 let loop_start = time::SystemTime::now();
77 loop {
78 let res: HashMap<String, Value> = reqwest_client
79 .post("https://github.com/login/oauth/access_token")
80 .header("Accept", "application/vnd.github+json")
81 .form(&poll_params)
82 .send()?
83 .json()?;
84 match res.get("access_token") {
85 Some(token) => {
86 break Ok(token
87 .as_str()
88 .ok_or(GithubError::InvalidField(String::from("access_token")))?
89 .to_string());
90 }
91 None => {
92 if time::SystemTime::now()
94 .duration_since(loop_start)
95 .expect("now cannot be before loop_start")
96 .as_secs()
97 > 900
98 {
99 eprintln!("Code has expired, exiting");
100 break Err(GithubError::Timeout(900));
101 }
102 thread::sleep(Duration::from_secs(5));
103 }
104 }
105 }
106}
107
108fn query_username(
109 reqwest_client: &reqwest::blocking::Client,
110 token: &str,
111) -> Result<String, GithubError> {
112 let res: HashMap<String, Value> = reqwest_client
113 .get("https://api.github.com/user")
114 .header("User-Agent", "git-auth")
115 .header("Accept", "application/vnd.github+json")
116 .header("Authorization", format!("token {token}"))
117 .send()?
118 .json()?;
119 Ok(res
120 .get("login")
121 .ok_or(GithubError::MissingField(String::from("login")))?
122 .as_str()
123 .ok_or(GithubError::InvalidField(String::from("login")))?
124 .to_string())
125}
126
127fn query_email(
128 reqwest_client: &reqwest::blocking::Client,
129 token: &str,
130) -> Result<Option<String>, GithubError> {
131 let res: Vec<Value> = reqwest_client
132 .get("https://api.github.com/user/emails")
133 .header("User-Agent", "git-auth")
134 .header("Accept", "application/vnd.github+json")
135 .header("Authorization", format!("token {token}"))
136 .send()?
137 .json()?;
138 for email in res {
139 if let Some(Value::Bool(true)) = email.get("primary") {
140 return Ok(email
141 .get("email")
142 .and_then(|e| e.as_str().map(|e_str| e_str.to_string())));
143 }
144 }
145
146 Ok(None)
147}