1 use anyhow::{anyhow, Result};
2 use console::style;
3 use log::{debug, info};
4 use regex::Regex;
5 use reqwest::{Client, StatusCode};
6 use serde::{Deserialize, Serialize};
7
8 use serde_repr::Deserialize_repr;
9 use spinners::Spinner;
10 use std::{
11 env,
12 fmt::{Display, Formatter},
13 fs,
14 io::{BufRead, BufReader, Write},
15 net::{TcpListener, TcpStream},
16 sync::mpsc::{self, Receiver, Sender},
17 };
18 use url_builder::URLBuilder;
19
20 use super::{model::User, signup::GithubEmail};
21
22 // This is smb authorization model.
23 #[derive(Debug, Serialize, Deserialize)]
24 pub struct SmbAuthorization {
25 pub message: String,
26 pub user: Option<User>,
27 pub user_email: Option<GithubEmail>,
28 pub user_info: Option<GithubInfo>,
29 pub error_code: Option<ErrorCode>,
30 }
31
32 #[derive(Debug, serde_repr::Serialize_repr, Deserialize_repr, PartialEq)]
33 #[repr(u32)]
34 pub enum ErrorCode {
35 EmailNotFound = 1000,
36 EmailUnverified = 1001,
37 PasswordNotSet = 1003,
38 }
39
40 impl Display for ErrorCode {
41 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
42 match self {
43 ErrorCode::EmailNotFound => write!(f, "Email not found."),
44 ErrorCode::EmailUnverified => write!(f, "Email not verified."),
45 ErrorCode::PasswordNotSet => write!(f, "Password not set."),
46 }
47 }
48 }
49
50 #[derive(Debug, Serialize, Deserialize)]
51 pub struct GithubInfo {
52 pub id: i64,
53 pub login: String,
54 pub name: String,
55 pub avatar_url: String,
56 pub html_url: String,
57 pub email: Option<String>,
58 pub created_at: String,
59 pub updated_at: String,
60 }
61
62 pub async fn authorize_github() -> Result<SmbAuthorization> {
63 // Spin up a simple localhost server to listen for the GitHub OAuth callback
64 // setup_oauth_callback_server();
65 // Open the GitHub OAuth URL in the user's browser
66 let mut spinner = Spinner::new(
67 spinners::Spinners::BouncingBall,
68 style("🚀 Getting your GitHub information...")
69 .green()
70 .bold()
71 .to_string(),
72 );
73
74 let rx = match open::that(build_github_oauth_url()) {
75 Ok(_) => {
76 let (tx, rx): (Sender<String>, Receiver<String>) = mpsc::channel();
77 debug!(
78 "Setting up OAuth callback server... (tx: {:#?}, rx: {:#?})",
79 &tx, &rx
80 );
81 tokio::spawn(async move {
82 setup_oauth_callback_server(tx);
83 });
84 rx
85 }
86 Err(_) => {
87 let error = anyhow!("Failed to open a browser.");
88 return Err(error);
89 }
90 };
91
92 spinner.stop_and_persist("⌛", "Waiting for the authorization.".into());
93
94 debug!("Waiting for code from channel...");
95
96 match rx.recv() {
97 Ok(code) => {
98 debug!("Got code from channel: {:#?}", &code);
99 //Err(anyhow!("Failed to get code from channel."))
100 process_connect_github(code).await
101 }
102 Err(e) => {
103 let error = anyhow!("Failed to get code from channel: {e}");
104 Err(error)
105 }
106 }
107 }
108
109 fn setup_oauth_callback_server(tx: Sender<String>) {
110 let port = env::var("GH_OAUTH_REDIRECT_PORT").expect("Please set GH_OAUTH_REDIRECT_PORT");
111 let listener = TcpListener::bind(format!("127.0.0.1:{}", port)).unwrap();
112 for stream in listener.incoming() {
113 let stream = stream.unwrap();
114 handle_connection(stream, tx.clone());
115 }
116 }
117
118 fn handle_connection(mut stream: TcpStream, tx: Sender<String>) {
119 let buf_reader = BufReader::new(&stream);
120 let request_line = &buf_reader.lines().next().unwrap().unwrap();
121
122 debug!("Request: {:#?}", request_line);
123
124 let code_regex = Regex::new(r"code=([^&]*)").unwrap();
125
126 let (status_line, filename) = match code_regex.captures(request_line) {
127 Some(group) => {
128 let code = group.get(1).unwrap().as_str();
129 debug!("Code: {:#?}", code);
130 debug!("Sending code to channel...");
131 debug!("Channel: {:#?}", &tx);
132 match tx.send(code.to_string()) {
133 Ok(_) => {
134 debug!("Code sent to channel.");
135 }
136 Err(e) => {
137 debug!("Failed to send code to channel: {e}");
138 }
139 }
140 ("HTTP/1.1 200 OK", "./smbpndk-cli/src/account/hello.html")
141 }
142 None => {
143 debug!("Code not found.");
144 (
145 "HTTP/1.1 404 NOT FOUND",
146 "./smbpndk-cli/src/account/404.html",
147 )
148 }
149 };
150
151 let contents = fs::read_to_string(filename).unwrap();
152 let length = contents.len();
153 let response = format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");
154 stream.write_all(response.as_bytes()).unwrap();
155 stream.flush().unwrap();
156 }
157
158 // Get access token
159 pub async fn process_connect_github(code: String) -> Result<SmbAuthorization> {
160 let response = Client::new()
161 .post(build_authorize_smb_url())
162 .body(format!("gh_code={}", code))
163 .header("Accept", "application/json")
164 .header("Content-Type", "application/x-www-form-urlencoded")
165 .send()
166 .await?;
167 let mut spinner = Spinner::new(
168 spinners::Spinners::BouncingBall,
169 style("🚀 Authorizing your account...")
170 .green()
171 .bold()
172 .to_string(),
173 );
174 println!("Response: {:#?}", &response);
175 match response.status() {
176 StatusCode::OK => {
177 // Account authorized and token received
178 spinner.stop_and_persist("✅", "You're logged in with your GitHub account!".into());
179 let result = response.json().await?;
180 println!("Result: {:#?}", &result);
181 Ok(result)
182 }
183 StatusCode::NOT_FOUND => {
184 // Account not found and we show signup option
185 spinner.stop_and_persist("🥲", "Account not found. Please signup!".into());
186 let result = response.json().await?;
187 println!("Result: {:#?}", &result);
188 Ok(result)
189 }
190 StatusCode::UNPROCESSABLE_ENTITY => {
191 // Account found but email not verified
192 spinner.stop_and_persist("🥹", "Unverified email!".into());
193 let result = response.json().await?;
194 println!("Result: {:#?}", &result);
195 Ok(result)
196 }
197 _ => {
198 // Other errors
199 let error = anyhow!("Error while authorizing token.");
200 Err(error)
201 }
202 }
203 }
204
205 fn build_authorize_smb_url() -> String {
206 let mut url_builder = smb_base_url_builder();
207 url_builder.add_route("v1/authorize");
208 url_builder.build()
209 }
210
211 fn build_github_oauth_url() -> String {
212 let mut url_builder = github_base_url_builder();
213 url_builder
214 .add_route("login/oauth/authorize")
215 .add_param("scope", "user")
216 .add_param("state", "smbpndk");
217 url_builder.build()
218 }
219
220 pub fn smb_base_url_builder() -> URLBuilder {
221 let client_id = env::var("SMB_CLIENT_ID").expect("Please set SMB_CLIENT_ID");
222 let client_secret = env::var("SMB_CLIENT_SECRET").expect("Please set SMB_CLIENT_SECRET");
223 let api_host = env::var("SMB_API_HOST").expect("Please set SMB_API_HOST");
224 let api_protocol = env::var("SMB_API_PROTOCOL").expect("Please set SMB_API_PROTOCOL");
225 let mut url_builder = URLBuilder::new();
226 url_builder
227 .set_protocol(&api_protocol)
228 .set_host(&api_host)
229 .add_param("client_id", &client_id)
230 .add_param("client_secret", &client_secret);
231 url_builder
232 }
233
234 fn github_base_url_builder() -> URLBuilder {
235 let client_id = env::var("GH_OAUTH_CLIENT_ID").expect("Please set GH_OAUTH_CLIENT_ID");
236 let redirect_host =
237 env::var("GH_OAUTH_REDIRECT_HOST").expect("Please set GH_OAUTH_REDIRECT_HOST");
238 let redirect_port =
239 env::var("GH_OAUTH_REDIRECT_PORT").expect("Please set GH_OAUTH_REDIRECT_PORT");
240 let redirect_url = format!("{}:{}", &redirect_host, &redirect_port);
241
242 let mut url_builder = URLBuilder::new();
243 url_builder
244 .set_protocol("https")
245 .set_host("github.com")
246 .add_param("client_id", &client_id)
247 .add_param("redirect_uri", &redirect_url);
248 url_builder
249 }