@smbcloud / cli / commits / dfa0600

WIP

Seto Elkahfi committed Aug 20, 2023 at 15:05 UTC dfa06008d6bd1ae9470a9a3e319067837764db5c
6 files changed +263 -212
.vscode/settings.json new
+3
@@ -0,0 +1,3 @@
1 +{
2 + "rust-analyzer.server.path": null
3 +}
\ No newline at end of file
smbpndk-cli/src/account/lib.rs new
+193
@@ -0,0 +1,193 @@
1 +use console::style;
2 +use log::debug;
3 +use reqwest::{Client, StatusCode};
4 +use serde::{Deserialize, Serialize};
5 +use smbpndk_model::CommandResult;
6 +use spinners::Spinner;
7 +use anyhow::{anyhow, Result};
8 +use url_builder::URLBuilder;
9 +use std::{
10 + fmt::{Display, Formatter},
11 + fs,
12 + io::{BufRead, BufReader, Write},
13 + net::{TcpListener, TcpStream},
14 + sync::mpsc::{self, Receiver, Sender}, env,
15 +};
16 +use regex::Regex;
17 +
18 +// This is smb authorization model.
19 +#[derive(Debug, Serialize, Deserialize)]
20 +pub struct SmbAuthorization {
21 + access_token: String,
22 + scope: String,
23 + token_type: String,
24 +}
25 +
26 +pub async fn authorize_github() -> Result<SmbAuthorization> {
27 + // Spin up a simple localhost server to listen for the GitHub OAuth callback
28 + // setup_oauth_callback_server();
29 + // Open the GitHub OAuth URL in the user's browser
30 + let mut spinner = Spinner::new(
31 + spinners::Spinners::BouncingBall,
32 + style("🚀 Getting your GitHub information...")
33 + .green()
34 + .bold()
35 + .to_string(),
36 + );
37 +
38 + let rx = match open::that(build_github_oauth_url()) {
39 + Ok(_) => {
40 + let (tx, rx): (Sender<String>, Receiver<String>) = mpsc::channel();
41 + debug!(
42 + "Setting up OAuth callback server... (tx: {:#?}, rx: {:#?})",
43 + &tx, &rx
44 + );
45 + tokio::spawn(async move {
46 + setup_oauth_callback_server(tx);
47 + });
48 + rx
49 + }
50 + Err(_) => {
51 + let error = anyhow!("Failed to open a browser.");
52 + return Err(error);
53 + }
54 + };
55 +
56 + spinner.stop_and_persist("⌛", "Waiting for the authorization.".into());
57 +
58 + debug!("Waiting for code from channel...");
59 +
60 + match rx.recv() {
61 + Ok(code) => {
62 + debug!("Got code from channel: {:#?}", &code);
63 + process_connect_github(code).await
64 + }
65 + Err(e) => {
66 + let error = anyhow!("Failed to get code from channel: {e}");
67 + Err(error)
68 + }
69 + }
70 +}
71 +
72 +fn setup_oauth_callback_server(tx: Sender<String>) {
73 + let port = env::var("GH_OAUTH_REDIRECT_PORT").expect("Please set GH_OAUTH_REDIRECT_PORT");
74 + let listener = TcpListener::bind(format!("127.0.0.1:{}", port)).unwrap();
75 + for stream in listener.incoming() {
76 + let stream = stream.unwrap();
77 + handle_connection(stream, tx.clone());
78 + }
79 +}
80 +
81 +fn handle_connection(mut stream: TcpStream, tx: Sender<String>) {
82 + let buf_reader = BufReader::new(&stream);
83 + let request_line = &buf_reader.lines().next().unwrap().unwrap();
84 +
85 + debug!("Request: {:#?}", request_line);
86 +
87 + let code_regex = Regex::new(r"code=([^&]*)").unwrap();
88 +
89 + let (status_line, filename) = match code_regex.captures(request_line) {
90 + Some(group) => {
91 + let code = group.get(1).unwrap().as_str();
92 + debug!("Code: {:#?}", code);
93 + debug!("Sending code to channel...");
94 + debug!("Channel: {:#?}", &tx);
95 + match tx.send(code.to_string()) {
96 + Ok(_) => {
97 + debug!("Code sent to channel.");
98 + }
99 + Err(e) => {
100 + debug!("Failed to send code to channel: {e}");
101 + }
102 + }
103 + ("HTTP/1.1 200 OK", "./src/account/hello.html")
104 + }
105 + None => {
106 + debug!("Code not found.");
107 + ("HTTP/1.1 404 NOT FOUND", "./src/account/404.html")
108 + }
109 + };
110 +
111 + let contents = fs::read_to_string(filename).unwrap();
112 + let length = contents.len();
113 + let response = format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");
114 + stream.write_all(response.as_bytes()).unwrap();
115 + stream.flush().unwrap();
116 +}
117 +
118 +
119 +// Get access token
120 +pub async fn process_connect_github(code: String) -> Result<SmbAuthorization> {
121 + let response = Client::new()
122 + .post(build_authorize_smb_url(code))
123 + .header("Accept", "application/json")
124 + .send()
125 + .await?;
126 + let mut spinner = Spinner::new(
127 + spinners::Spinners::BouncingBall,
128 + style("🚀 Requesting GitHub token...")
129 + .green()
130 + .bold()
131 + .to_string(),
132 + );
133 + match response.status() {
134 + StatusCode::OK => {
135 + spinner.stop_and_persist("✅", "Finished requesting GitHub token!".into());
136 + debug!("Response: {:#?}", &response);
137 + let result: SmbAuthorization = response.json().await?;
138 + debug!("Result: {:#?}", &result);
139 + Ok(result)
140 + }
141 + _ => {
142 + let error = anyhow!("Error while requesting GitHub token.");
143 + Err(error)
144 + }
145 + }
146 +}
147 +
148 +fn build_authorize_smb_url(code: String) -> String {
149 + let mut url_builder = smb_base_url_builder();
150 + url_builder
151 + .add_route("v1/authorize")
152 + .add_param("gh_code", &code);
153 + url_builder.build()
154 +}
155 +
156 +fn build_github_oauth_url() -> String {
157 + let mut url_builder = github_base_url_builder();
158 + url_builder
159 + .add_route("login/oauth/authorize")
160 + .add_param("scope", "user")
161 + .add_param("state", "smbpndk");
162 + url_builder.build()
163 +}
164 +
165 +fn smb_base_url_builder() -> URLBuilder {
166 + let client_id =
167 + env::var("SMB_CLIENT_ID").expect("Please set SMB_CLIENT_ID");
168 + let client_secret =
169 + env::var("SMB_CLIENT_SECRET").expect("Please set SMB_CLIENT_SECRET");
170 + let mut url_builder = URLBuilder::new();
171 + url_builder
172 + .set_protocol("https")
173 + .set_host("api.smbpndk.com")
174 + .add_param("client_id", &client_id)
175 + .add_param("client_secret", &client_secret);
176 + url_builder
177 +}
178 +
179 +fn github_base_url_builder() -> URLBuilder {
180 + let client_id = env::var("GH_OAUTH_CLIENT_ID").expect("Please set GH_OAUTH_CLIENT_ID");
181 + let redirect_host =
182 + env::var("GH_OAUTH_REDIRECT_HOST").expect("Please set GH_OAUTH_REDIRECT_HOST");
183 + let redirect_port = env::var("GH_OAUTH_REDIRECT_PORT").expect("Please set GH_OAUTH_REDIRECT_PORT");
184 + let redirect_url = format!("{}:{}", &redirect_host, &redirect_port);
185 +
186 + let mut url_builder = URLBuilder::new();
187 + url_builder
188 + .set_protocol("https")
189 + .set_host("github.com")
190 + .add_param("client_id", &client_id)
191 + .add_param("redirect_uri", &redirect_url);
192 + url_builder
193 +}
\ No newline at end of file
smbpndk-cli/src/account/login/process.rs
+38 -3
@@ -1,7 +1,7 @@
1 -use crate::account::model::{Data, Status, User};
1 +use crate::account::{model::{Data, Status, User}, signup::SignupMethod, lib::authorize_github};
2 use anyhow::{anyhow, Result};
3 -use console::style;
4 -use dialoguer::{theme::ColorfulTheme, Input, Password};
3 +use console::{style, Term};
4 +use dialoguer::{theme::ColorfulTheme, Input, Password, Select};
5 use log::debug;
6 use reqwest::Client;
7 use serde::{Deserialize, Serialize};
@@ -31,6 +31,41 @@ struct LoginResult {
31 }
32
33 pub async fn process_login() -> Result<CommandResult> {
34 + let signup_methods = vec![SignupMethod::Email, SignupMethod::GitHub];
35 + let selection = Select::with_theme(&ColorfulTheme::default())
36 + .items(&signup_methods)
37 + .default(0)
38 + .interact_on_opt(&Term::stderr())
39 + .map(|i| signup_methods[i.unwrap()])
40 + .unwrap();
41 +
42 + match selection {
43 + SignupMethod::Email => login_with_email().await,
44 + SignupMethod::GitHub => login_with_github().await,
45 + }
46 +}
47 +
48 +async fn login_with_github() -> Result<CommandResult> {
49 + match authorize_github().await {
50 + Ok(_) => {
51 + let spinner = Spinner::new(
52 + spinners::Spinners::SimpleDotsScrolling,
53 + style("Logging you in...").green().bold().to_string(),
54 + );
55 + Ok(CommandResult {
56 + spinner,
57 + symbol: "✅".to_owned(),
58 + msg: "You are logged in!".to_owned(),
59 + })
60 + },
61 + Err(_) => {
62 + let error = anyhow!("Failed to login with GitHub.");
63 + Err(error)
64 + }
65 + }
66 +}
67 +
68 +async fn login_with_email() -> Result<CommandResult> {
69 println!("Provide your login credentials.");
70 let username = Input::<String>::with_theme(&ColorfulTheme::default())
71 .with_prompt("Username")
smbpndk-cli/src/account/mod.rs
+1
@@ -3,6 +3,7 @@ pub mod forgot;
3 pub mod login;
4 pub mod model;
5 pub mod signup;
6 +pub mod lib;
7
8 use self::{
9 cli::Commands,
smbpndk-cli/src/account/signup/process.rs
+14 -209
@@ -1,23 +1,15 @@
1 -use crate::account::model::{Data, Status, User};
1 +use crate::account::{model::{Data, Status, User}, lib::{process_connect_github, authorize_github}};
2 use anyhow::{anyhow, Result};
3 use console::{style, Term};
4 use dialoguer::{theme::ColorfulTheme, Input, Password, Select};
5 use log::debug;
6 -use regex::Regex;
6 use reqwest::{Client, StatusCode};
8 -use serde::{Deserialize, Serialize};
7 +use serde::{Deserialize, Serialize, de};
8 use smbpndk_model::CommandResult;
9 use smbpndk_networking::constants::BASE_URL;
10 use smbpndk_utils::email_validation;
11 use spinners::Spinner;
13 -use std::{
14 - fmt::{Display, Formatter},
15 - fs,
16 - io::{BufRead, BufReader, Write},
17 - net::{TcpListener, TcpStream},
18 - sync::mpsc::{self, Receiver, Sender},
19 -};
20 -use url_builder::URLBuilder;
12 +use std::fmt::{Display, Formatter};
13
14 use super::SignupMethod;
15 pub struct SignupArgs {
@@ -99,44 +91,18 @@ async fn signup_with_email(email: Option<String>) -> Result<CommandResult> {
91 }
92
93 async fn signup_with_github() -> Result<CommandResult> {
102 - // Spin up a simple localhost server to listen for the GitHub OAuth callback
103 - // setup_oauth_callback_server();
104 - // Open the GitHub OAuth URL in the user's browser
105 - let mut spinner = Spinner::new(
106 - spinners::Spinners::BouncingBall,
107 - style("🚀 Getting your GitHub information...")
108 - .green()
109 - .bold()
110 - .to_string(),
111 - );
112 -
113 - let rx = match open::that(build_github_oauth_url()) {
114 - Ok(_) => {
115 - let (tx, rx): (Sender<String>, Receiver<String>) = mpsc::channel();
116 - debug!(
117 - "Setting up OAuth callback server... (tx: {:#?}, rx: {:#?})",
118 - &tx, &rx
119 - );
120 - tokio::spawn(async move {
121 - setup_oauth_callback_server(tx);
122 - });
123 - rx
124 - }
125 - Err(_) => {
126 - let error = anyhow!("Failed to open a browser.");
127 - return Err(error);
128 - }
129 - };
130 -
131 - spinner.stop_and_persist("⌛", "Waiting for the authorization.".into());
132 -
133 - debug!("Waiting for code from channel...");
134 -
135 - match rx.recv() {
94 + match authorize_github().await {
95 Ok(code) => {
137 - debug!("Got code from channel: {:#?}", &code);
138 - process_signup_github(code).await
139 - }
96 + debug!("Code: {:#?}", code);
97 + Ok(CommandResult {
98 + spinner: Spinner::new(
99 + spinners::Spinners::BouncingBall,
100 + style("Requesting GitHub token...").green().bold().to_string(),
101 + ),
102 + symbol: style("✅".to_string()).for_stderr().green().to_string(),
103 + msg: "Finished requesting GitHub token!".to_owned(),
104 + })
105 + },
106 Err(e) => {
107 let error = anyhow!("Failed to get code from channel: {e}");
108 Err(error)
@@ -144,57 +110,6 @@ async fn signup_with_github() -> Result<CommandResult> {
110 }
111 }
112
147 -fn setup_oauth_callback_server(tx: Sender<String>) {
148 - let listener = TcpListener::bind("127.0.0.1:8808").unwrap();
149 - for stream in listener.incoming() {
150 - let stream = stream.unwrap();
151 - handle_connection(stream, tx.clone());
152 - }
153 -}
154 -
155 -fn handle_connection(mut stream: TcpStream, tx: Sender<String>) {
156 - let buf_reader = BufReader::new(&stream);
157 - let request_line = &buf_reader.lines().next().unwrap().unwrap();
158 -
159 - debug!("Request: {:#?}", request_line);
160 -
161 - let code_regex = Regex::new(r"code=([^&]*)").unwrap();
162 -
163 - let (status_line, filename) = match code_regex.captures(request_line) {
164 - Some(group) => {
165 - let code = group.get(1).unwrap().as_str();
166 - debug!("Code: {:#?}", code);
167 - debug!("Sending code to channel...");
168 - debug!("Channel: {:#?}", &tx);
169 - match tx.send(code.to_string()) {
170 - Ok(_) => {
171 - debug!("Code sent to channel.");
172 - }
173 - Err(e) => {
174 - debug!("Failed to send code to channel: {e}");
175 - }
176 - }
177 - ("HTTP/1.1 200 OK", "./src/account/hello.html")
178 - }
179 - None => {
180 - debug!("Code not found.");
181 - ("HTTP/1.1 404 NOT FOUND", "./src/account/404.html")
182 - }
183 - };
184 -
185 - let contents = fs::read_to_string(filename).unwrap();
186 - let length = contents.len();
187 - let response = format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");
188 - stream.write_all(response.as_bytes()).unwrap();
189 - stream.flush().unwrap();
190 -}
191 -
192 -#[derive(Debug, Serialize, Deserialize)]
193 -struct GithubToken {
194 - access_token: String,
195 - scope: String,
196 - token_type: String,
197 -}
113 #[derive(Debug, Serialize, Deserialize)]
114 struct GithubUser {
115 email: Option<String>,
@@ -215,72 +130,6 @@ impl Display for GithubEmail {
130 }
131 }
132
218 -// Get access token
219 -async fn process_signup_github(code: String) -> Result<CommandResult> {
220 - let response = Client::new()
221 - .post(build_github_access_token_url(code))
222 - .header("Accept", "application/json")
223 - .send()
224 - .await?;
225 - let mut spinner = Spinner::new(
226 - spinners::Spinners::BouncingBall,
227 - style("🚀 Requesting GitHub token...")
228 - .green()
229 - .bold()
230 - .to_string(),
231 - );
232 - match response.status() {
233 - StatusCode::OK => {
234 - spinner.stop_and_persist("✅", "Finished requesting GitHub token!".into());
235 - debug!("Response: {:#?}", &response);
236 - let result: GithubToken = response.json().await?;
237 - debug!("Result: {:#?}", &result);
238 - get_github_data(result.access_token).await
239 - }
240 - _ => {
241 - let error = anyhow!("Error while requesting GitHub token.");
242 - Err(error)
243 - }
244 - }
245 -}
246 -
247 -// Request user data
248 -async fn get_github_data(token: String) -> Result<CommandResult> {
249 - let mut spinner = Spinner::new(
250 - spinners::Spinners::BouncingBall,
251 - style("🚀 Requesting GitHub data...")
252 - .green()
253 - .bold()
254 - .to_string(),
255 - );
256 - let response = Client::new()
257 - .get(build_github_access_data_url())
258 - .header("Accept", "application/json")
259 - .header("User-Agent", "Rust")
260 - .bearer_auth(token)
261 - .send()
262 - .await?;
263 -
264 - debug!("Response: {:#?}", &response);
265 -
266 - match response.status() {
267 - StatusCode::OK => {
268 - spinner.stop_and_persist("✅", "Finished requesting GitHub data!".into());
269 - debug!("Response: {:#?}", &response);
270 - let mut emails: Vec<GithubEmail> = response.json().await?;
271 - debug!("{:#?}", &emails);
272 - emails.retain(|e| !e.email.contains("@users.noreply.github.com"));
273 -
274 - let email = select_github_emails(emails)?;
275 - signup_with_email(Some(email.email)).await
276 - }
277 - status_code => {
278 - let error = anyhow!("Error while requesting GitHub data: {status_code}");
279 - Err(error)
280 - }
281 - }
282 -}
283 -
133 fn select_github_emails(github_emails: Vec<GithubEmail>) -> Result<GithubEmail> {
134 let selection = Select::with_theme(&ColorfulTheme::default())
135 .with_prompt("Select your email")
@@ -293,50 +142,6 @@ fn select_github_emails(github_emails: Vec<GithubEmail>) -> Result<GithubEmail>
142 Ok(selection)
143 }
144
296 -fn github_url_builder() -> URLBuilder {
297 - let client_id = option_env!("GH_OAUTH_CLIENT_ID").unwrap_or("Please set GH_OAUTH_CLIENT_ID");
298 - let redirect_uri =
299 - option_env!("GH_OAUTH_REDIRECT_URI").unwrap_or("Please set GH_OAUTH_REDIRECT_URI");
300 -
301 - let mut url_builder = URLBuilder::new();
302 - url_builder
303 - .set_protocol("https")
304 - .set_host("github.com")
305 - .add_param("client_id", client_id)
306 - .add_param("redirect_uri", redirect_uri);
307 - url_builder
308 -}
309 -
310 -fn build_github_oauth_url() -> String {
311 - let mut url_builder = github_url_builder();
312 - url_builder
313 - .add_route("login/oauth/authorize")
314 - .add_param("scope", "user")
315 - .add_param("state", "smbpndk");
316 - url_builder.build()
317 -}
318 -
319 -fn build_github_access_token_url(code: String) -> String {
320 - let client_secret =
321 - option_env!("GH_OAUTH_CLIENT_SECRET").unwrap_or("Please set GH_OAUTH_CLIENT_SECRET");
322 -
323 - let mut url_builder = github_url_builder();
324 - url_builder
325 - .add_route("login/oauth/access_token")
326 - .add_param("client_secret", client_secret)
327 - .add_param("code", &code);
328 - url_builder.build()
329 -}
330 -
331 -fn build_github_access_data_url() -> String {
332 - let mut url_builder = URLBuilder::new();
333 - url_builder
334 - .set_protocol("https")
335 - .set_host("api.github.com")
336 - .add_route("user/emails");
337 - url_builder.build()
338 -}
339 -
145 async fn do_signup(args: SignupArgs) -> Result<()> {
146 let signup_params = SignupParams {
147 user: User {
smbpndk-cli/src/account/smbpndk.code-workspace new
+14
@@ -0,0 +1,14 @@
1 +{
2 + "folders": [
3 + {
4 + "path": "../../.."
5 + },
6 + {
7 + "path": "../../../../smbpndk-api"
8 + },
9 + {
10 + "path": "../../../../smbpndk-web"
11 + }
12 + ],
13 + "settings": {}
14 +}
\ No newline at end of file