smbcloud_cli/account/
lib.rs

1use 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    // Spin up a simple localhost server to listen for the GitHub OAuth callback
25    // setup_oauth_callback_server();
26    // Open the GitHub OAuth URL in the user's browser
27    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            //Err(anyhow!("Failed to get code from channel."))
61            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
143// Get access token
144pub 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    // println!("Response: {:#?}", &response);
160    match response.status() {
161        StatusCode::OK => {
162            // Account authorized and token received
163            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            // println!("Result: {:#?}", &result);
167            Ok(result)
168        }
169        StatusCode::NOT_FOUND => {
170            // Account not found and we show signup option
171            spinner.stop_and_persist("🥲", "Account not found. Please signup!".into());
172            let result = response.json().await?;
173            // println!("Result: {:#?}", &result);
174            Ok(result)
175        }
176        StatusCode::UNPROCESSABLE_ENTITY => {
177            // Account found but email not verified
178            spinner.stop_and_persist("🥹", "Unverified email!".into());
179            let result = response.json().await?;
180            // println!("Result: {:#?}", &result);
181            Ok(result)
182        }
183        _ => {
184            // Other errors
185            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    // println!("Headers: {:#?}", &headers);
221    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    // Check if token file exists
245    smb_token_file_path(env).is_some()
246}