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