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