smbcloud_cli/account/
lib.rs1use crate::token::smb_token_file_path;
2use anyhow::{anyhow, Result};
3use console::style;
4use log::debug;
5use regex::Regex;
6use reqwest::{Client, Response, StatusCode};
7use smbcloud_model::account::SmbAuthorization;
8use smbcloud_network::environment::Environment;
9use smbcloud_networking::{
10 constants::{
11 GH_OAUTH_CLIENT_ID, GH_OAUTH_REDIRECT_HOST, GH_OAUTH_REDIRECT_PORT, PATH_AUTHORIZE,
12 },
13 smb_base_url_builder,
14};
15use spinners::Spinner;
16use std::{
17 fs::{create_dir_all, OpenOptions},
18 io::{BufRead, BufReader, Write},
19 net::{TcpListener, TcpStream},
20 sync::mpsc::{self, Receiver, Sender},
21};
22use url_builder::URLBuilder;
23
24pub async fn authorize_github(env: &Environment) -> Result<SmbAuthorization> {
25 let mut spinner = Spinner::new(
29 spinners::Spinners::BouncingBall,
30 style("🚀 Getting your GitHub information...")
31 .green()
32 .bold()
33 .to_string(),
34 );
35
36 let rx = match open::that(build_github_oauth_url()) {
37 Ok(_) => {
38 let (tx, rx): (Sender<String>, Receiver<String>) = mpsc::channel();
39 debug!(
40 "Setting up OAuth callback server... (tx: {:#?}, rx: {:#?})",
41 &tx, &rx
42 );
43 tokio::spawn(async move {
44 setup_oauth_callback_server(tx);
45 });
46 rx
47 }
48 Err(_) => {
49 let error = anyhow!("Failed to open a browser.");
50 return Err(error);
51 }
52 };
53
54 spinner.stop_and_persist("⌛", "Waiting for the authorization.".into());
55
56 debug!("Waiting for code from channel...");
57
58 match rx.recv() {
59 Ok(code) => {
60 debug!("Got code from channel: {:#?}", &code);
61 process_connect_github(*env, code).await
63 }
64 Err(e) => {
65 let error = anyhow!("Failed to get code from channel: {e}");
66 Err(error)
67 }
68 }
69}
70
71fn setup_oauth_callback_server(tx: Sender<String>) {
72 let listener = TcpListener::bind(format!("127.0.0.1:{}", GH_OAUTH_REDIRECT_PORT)).unwrap();
73 for stream in listener.incoming() {
74 let stream = stream.unwrap();
75 handle_connection(stream, tx.clone());
76 }
77}
78
79fn handle_connection(mut stream: TcpStream, tx: Sender<String>) {
80 let buf_reader = BufReader::new(&stream);
81 let request_line = &buf_reader.lines().next().unwrap().unwrap();
82
83 debug!("Request: {:#?}", request_line);
84
85 let code_regex = Regex::new(r"code=([^&]*)").unwrap();
86
87 let (status_line, contents) = match code_regex.captures(request_line) {
88 Some(group) => {
89 let code = group.get(1).unwrap().as_str();
90 debug!("Code: {:#?}", code);
91 debug!("Sending code to channel...");
92 debug!("Channel: {:#?}", &tx);
93 match tx.send(code.to_string()) {
94 Ok(_) => {
95 debug!("Code sent to channel.");
96 }
97 Err(e) => {
98 debug!("Failed to send code to channel: {e}");
99 }
100 }
101 (
102 "HTTP/1.1 200 OK",
103 "<!DOCTYPE html>
104
105 <head>
106 <meta charset='utf-8'>
107 <title>Hello!</title>
108 </head>
109
110 <body>
111 <h1>Authenticated!</h1>
112 <p>Back to the terminal console to finish your registration.</p>
113 </body>",
114 )
115 }
116 None => {
117 debug!("Code not found.");
118 (
119 "HTTP/1.1 404 NOT FOUND",
120 "<!DOCTYPE html>
121 <html lang='en'>
122
123 <head>
124 <meta charset='utf-8'>
125 <title>404 Not found</title>
126 </head>
127
128 <body>
129 <h1>Oops!</h1>
130 <p>Sorry, I don't know what you're asking for.</p>
131 </body>
132
133 </html>",
134 )
135 }
136 };
137
138 debug!("Contents: {:#?}", &contents);
139 let response = format!("{status_line}\r\n\r\n{contents}");
140 stream.write_all(response.as_bytes()).unwrap();
141 stream.flush().unwrap();
142}
143
144pub async fn process_connect_github(env: Environment, code: String) -> Result<SmbAuthorization> {
146 let response = Client::new()
147 .post(build_authorize_smb_url(env))
148 .body(format!("gh_code={}", code))
149 .header("Accept", "application/json")
150 .header("Content-Type", "application/x-www-form-urlencoded")
151 .send()
152 .await?;
153 let mut spinner = Spinner::new(
154 spinners::Spinners::BouncingBall,
155 style("🚀 Authorizing your account...")
156 .green()
157 .bold()
158 .to_string(),
159 );
160 match response.status() {
162 StatusCode::OK => {
163 spinner.stop_and_persist("✅", "You are logged in with your GitHub account!".into());
165 save_token(env, &response).await?;
166 let result = response.json().await?;
167 Ok(result)
169 }
170 StatusCode::NOT_FOUND => {
171 spinner.stop_and_persist("🥲", "Account not found. Please signup!".into());
173 let result = response.json().await?;
174 Ok(result)
176 }
177 StatusCode::UNPROCESSABLE_ENTITY => {
178 spinner.stop_and_persist("🥹", "Unverified email!".into());
180 let result = response.json().await?;
181 Ok(result)
183 }
184 _ => {
185 let error = anyhow!("Error while authorizing with GitHub.");
187 Err(error)
188 }
189 }
190}
191
192fn build_authorize_smb_url(env: Environment) -> String {
193 let mut url_builder = smb_base_url_builder(env);
194 url_builder.add_route(PATH_AUTHORIZE);
195 url_builder.build()
196}
197
198fn build_github_oauth_url() -> String {
199 let mut url_builder = github_base_url_builder();
200 url_builder
201 .add_route("login/oauth/authorize")
202 .add_param("scope", "user")
203 .add_param("state", "smbcloud");
204 url_builder.build()
205}
206
207fn github_base_url_builder() -> URLBuilder {
208 let redirect_url = format!("{}:{}", GH_OAUTH_REDIRECT_HOST, GH_OAUTH_REDIRECT_PORT);
209
210 let mut url_builder = URLBuilder::new();
211 url_builder
212 .set_protocol("https")
213 .set_host("github.com")
214 .add_param("client_id", GH_OAUTH_CLIENT_ID)
215 .add_param("redirect_uri", &redirect_url);
216 url_builder
217}
218
219pub async fn save_token(env: Environment, response: &Response) -> Result<()> {
220 let headers = response.headers();
221 match headers.get("Authorization") {
223 Some(token) => {
224 debug!("{}", token.to_str()?);
225 match home::home_dir() {
226 Some(path) => {
227 debug!("{}", path.to_str().unwrap());
228 create_dir_all(path.join(env.smb_dir()))?;
229 let mut file = OpenOptions::new()
230 .create(true)
231 .truncate(true)
232 .write(true)
233 .open([path.to_str().unwrap(), "/", &env.smb_dir(), "/token"].join(""))?;
234 file.write_all(token.to_str()?.as_bytes())?;
235 Ok(())
236 }
237 None => Err(anyhow!("Failed to get home directory.")),
238 }
239 }
240 None => Err(anyhow!("Failed to get token. Probably a backend issue.")),
241 }
242}
243
244pub fn is_logged_in(env: Environment) -> bool {
245 smb_token_file_path(env).is_some()
247}