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