smbcloud_cli/account/
lib.rs

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