4 files changed
+209
-79
smbpndk-cli/src/account/lib.rs
+68
-19
@@ -4,24 +4,54 @@ use log::debug;
4
use regex::Regex;
5
use reqwest::{Client, StatusCode};
6
use serde::{Deserialize, Serialize};
7
-use smbpndk_model::CommandResult;
7
+
8
use spinners::Spinner;
9
use std::{
10
env,
11
- fmt::{Display, Formatter},
11
fs,
12
io::{BufRead, BufReader, Write},
13
net::{TcpListener, TcpStream},
15
- sync::mpsc::{self, Receiver, Sender},
14
+ sync::mpsc::{self, Receiver, Sender}, fmt::{Display, Formatter},
15
};
16
use url_builder::URLBuilder;
17
18
+use super::{model::User, signup::GithubEmail};
19
+
20
// This is smb authorization model.
21
#[derive(Debug, Serialize, Deserialize)]
22
pub struct SmbAuthorization {
22
- access_token: String,
23
- scope: String,
24
- token_type: String,
23
+ pub message: String,
24
+ pub user: Option<User>,
25
+ pub user_email: Option<GithubEmail>,
26
+ pub user_info: Option<GithubInfo>,
27
+ pub error_code: Option<ErrorCode>,
28
+}
29
+
30
+#[derive(Debug, Serialize, Deserialize)]
31
+pub enum ErrorCode {
32
+ EmailNotFound = 1000,
33
+ EmailUnverified = 1001,
34
+}
35
+
36
+impl Display for ErrorCode {
37
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
38
+ match self {
39
+ ErrorCode::EmailNotFound => write!(f, "Email not found."),
40
+ ErrorCode::EmailUnverified => write!(f, "Email not verified."),
41
+ }
42
+ }
43
+}
44
+
45
+#[derive(Debug, Serialize, Deserialize)]
46
+pub struct GithubInfo {
47
+ pub id: i64,
48
+ pub login: String,
49
+ pub name: String,
50
+ pub avatar_url: String,
51
+ pub html_url: String,
52
+ pub email: Option<String>,
53
+ pub created_at: String,
54
+ pub updated_at: String,
55
}
56
57
pub async fn authorize_github() -> Result<SmbAuthorization> {
@@ -101,11 +131,11 @@ fn handle_connection(mut stream: TcpStream, tx: Sender<String>) {
131
debug!("Failed to send code to channel: {e}");
132
}
133
}
104
- ("HTTP/1.1 200 OK", "./src/account/hello.html")
134
+ ("HTTP/1.1 200 OK", "./smbpndk-cli/src/account/hello.html")
135
}
136
None => {
137
debug!("Code not found.");
108
- ("HTTP/1.1 404 NOT FOUND", "./src/account/404.html")
138
+ ("HTTP/1.1 404 NOT FOUND", "./smbpndk-cli/src/account/404.html")
139
}
140
};
141
@@ -119,37 +149,54 @@ fn handle_connection(mut stream: TcpStream, tx: Sender<String>) {
149
// Get access token
150
pub async fn process_connect_github(code: String) -> Result<SmbAuthorization> {
151
let response = Client::new()
122
- .post(build_authorize_smb_url(code))
152
+ .post(build_authorize_smb_url())
153
+ .body(format!("gh_code={}", code))
154
.header("Accept", "application/json")
155
+ .header("Content-Type", "application/x-www-form-urlencoded")
156
.send()
157
.await?;
158
let mut spinner = Spinner::new(
159
spinners::Spinners::BouncingBall,
128
- style("🚀 Requesting GitHub token...")
160
+ style("🚀 Authorizing your account...")
161
.green()
162
.bold()
163
.to_string(),
164
);
165
match response.status() {
166
StatusCode::OK => {
135
- spinner.stop_and_persist("✅", "Finished requesting GitHub token!".into());
167
+ // Account authorized and token received
168
+ spinner.stop_and_persist("✅", "Finished authorizing your GitHub account!".into());
169
debug!("Response: {:#?}", &response);
170
let result: SmbAuthorization = response.json().await?;
171
debug!("Result: {:#?}", &result);
172
Ok(result)
140
- }
173
+ },
174
+ StatusCode::NOT_FOUND => {
175
+ // Account not found and we show signup option
176
+ spinner.stop_and_persist("✅", "Finished requesting GitHub token!".into());
177
+ let result: SmbAuthorization = response.json().await?;
178
+ debug!("Result: {:#?}", &result);
179
+ Ok(result)
180
+ },
181
+ StatusCode::UNPROCESSABLE_ENTITY => {
182
+ // Account found but email not verified
183
+ spinner.stop_and_persist("✅", "Finished requesting GitHub token!".into());
184
+ let result: SmbAuthorization = response.json().await?;
185
+ debug!("Result: {:#?}", &result);
186
+ Ok(result)
187
+ },
188
_ => {
142
- let error = anyhow!("Error while requesting GitHub token.");
189
+ // Other errors
190
+ let error = anyhow!("Error while authorizing token.");
191
Err(error)
192
}
193
}
194
}
195
148
-fn build_authorize_smb_url(code: String) -> String {
196
+fn build_authorize_smb_url() -> String {
197
let mut url_builder = smb_base_url_builder();
198
url_builder
151
- .add_route("v1/authorize")
152
- .add_param("gh_code", &code);
199
+ .add_route("v1/authorize");
200
url_builder.build()
201
}
202
@@ -162,13 +209,15 @@ fn build_github_oauth_url() -> String {
209
url_builder.build()
210
}
211
165
-fn smb_base_url_builder() -> URLBuilder {
212
+pub fn smb_base_url_builder() -> URLBuilder {
213
let client_id = env::var("SMB_CLIENT_ID").expect("Please set SMB_CLIENT_ID");
214
let client_secret = env::var("SMB_CLIENT_SECRET").expect("Please set SMB_CLIENT_SECRET");
215
+ let api_host = env::var("SMB_API_HOST").expect("Please set SMB_API_HOST");
216
+ let api_protocol = env::var("SMB_API_PROTOCOL").expect("Please set SMB_API_PROTOCOL");
217
let mut url_builder = URLBuilder::new();
218
url_builder
170
- .set_protocol("https")
171
- .set_host("api.smbpndk.com")
219
+ .set_protocol(&api_protocol)
220
+ .set_host(&api_host)
221
.add_param("client_id", &client_id)
222
.add_param("client_secret", &client_secret);
223
url_builder
smbpndk-cli/src/account/login/process.rs
+84
-17
@@ -1,11 +1,11 @@
1
use crate::account::{
2
- lib::authorize_github,
2
+ lib::{authorize_github, SmbAuthorization, ErrorCode, GithubInfo, smb_base_url_builder},
3
model::{Data, Status, User},
4
- signup::SignupMethod,
4
+ signup::{SignupMethod, GithubEmail, do_signup, SignupGithubParams, SignupUserGithub, Provider},
5
};
6
use anyhow::{anyhow, Result};
7
use console::{style, Term};
8
-use dialoguer::{theme::ColorfulTheme, Input, Password, Select};
8
+use dialoguer::{theme::ColorfulTheme, Input, Password, Select, Confirm};
9
use log::debug;
10
use reqwest::Client;
11
use serde::{Deserialize, Serialize};
@@ -51,28 +51,89 @@ pub async fn process_login() -> Result<CommandResult> {
51
52
async fn login_with_github() -> Result<CommandResult> {
53
match authorize_github().await {
54
- Ok(_) => {
55
- let spinner = Spinner::new(
56
- spinners::Spinners::SimpleDotsScrolling,
57
- style("Logging you in...").green().bold().to_string(),
58
- );
59
- Ok(CommandResult {
60
- spinner,
61
- symbol: "✅".to_owned(),
62
- msg: "You are logged in!".to_owned(),
63
- })
54
+ Ok(result) => {
55
+ process_authorization(result).await
56
}
65
- Err(_) => {
66
- let error = anyhow!("Failed to login with GitHub.");
57
+ Err(err) => {
58
+ let error = anyhow!("Failed to authorize your GitHub account. {}", err);
59
Err(error)
60
}
61
}
62
}
63
64
+async fn process_authorization(auth: SmbAuthorization) -> Result<CommandResult> {
65
+ // Logged in with GitHub
66
+ if let Some(user) = auth.user {
67
+ let spinner = Spinner::new(
68
+ spinners::Spinners::SimpleDotsScrolling,
69
+ style("Logging you in...").green().bold().to_string(),
70
+ );
71
+ return Ok(CommandResult {
72
+ spinner,
73
+ symbol: "✅".to_owned(),
74
+ msg: "You are logged in!".to_owned(),
75
+ })
76
+ }
77
+
78
+ // What to do if not logged in with GitHub?
79
+ if let Some(error_code) = auth.error_code {
80
+ debug!("{}", error_code);
81
+ match error_code {
82
+ ErrorCode::EmailNotFound => {
83
+ return create_new_account(auth.user_email, auth.user_info).await
84
+ },
85
+ ErrorCode::EmailUnverified => {
86
+ return send_email_verification(auth.user_email).await
87
+ }
88
+ }
89
+ }
90
+
91
+ let error = anyhow!("Failed to login with GitHub.");
92
+ Err(error)
93
+
94
+}
95
+
96
+async fn create_new_account(user_email: Option<GithubEmail>, user_info: Option<GithubInfo>) -> Result<CommandResult> {
97
+ let confirm = Confirm::with_theme(&ColorfulTheme::default())
98
+ .with_prompt("Do you want to create a new account?")
99
+ .interact()
100
+ .unwrap();
101
+
102
+ // Create account if user confirms
103
+ if !confirm {
104
+ let spinner = Spinner::new(
105
+ spinners::Spinners::SimpleDotsScrolling,
106
+ style("Logging you in...").green().bold().to_string(),
107
+ );
108
+ return Ok(CommandResult {
109
+ spinner,
110
+ symbol: "✅".to_owned(),
111
+ msg: "You are logged in!".to_owned(),
112
+ })
113
+ }
114
+
115
+ if let (Some(email), Some(info)) = (user_email, user_info) {
116
+ let params = SignupGithubParams {
117
+ user: SignupUserGithub {
118
+ email: email.email,
119
+ authorizations_attributes: vec![Provider { uid: info.id.to_string(), provider: 0 }],
120
+ }
121
+ };
122
+
123
+ return do_signup(¶ms).await
124
+ }
125
+
126
+ Err(anyhow!("Shouldn't be here."))
127
+}
128
+
129
+async fn send_email_verification(user_email: Option<GithubEmail>) -> Result<CommandResult> {
130
+ Err(anyhow!("Failed to send email verification."))
131
+}
132
+
133
async fn login_with_email() -> Result<CommandResult> {
134
println!("Provide your login credentials.");
135
let username = Input::<String>::with_theme(&ColorfulTheme::default())
75
- .with_prompt("Username")
136
+ .with_prompt("Email")
137
.validate_with(|email: &String| email_validation(email))
138
.interact()
139
.unwrap();
@@ -109,7 +170,7 @@ async fn do_process_login(args: LoginArgs) -> Result<()> {
170
};
171
172
let response = Client::new()
112
- .post([BASE_URL, "/v1/users/sign_in"].join(""))
173
+ .post(build_smb_login_url())
174
.json(&login_params)
175
.send()
176
.await?;
@@ -151,6 +212,12 @@ async fn do_process_login(args: LoginArgs) -> Result<()> {
212
Ok(())
213
}
214
215
+fn build_smb_login_url() -> String {
216
+ let mut url_builder = smb_base_url_builder();
217
+ url_builder.add_route("v1/users/sign_in");
218
+ url_builder.build()
219
+}
220
+
221
pub async fn process_logout() -> Result<CommandResult> {
222
let spinner = Spinner::new(
223
spinners::Spinners::SimpleDotsScrolling,
smbpndk-cli/src/account/model/mod.rs
+1
-1
@@ -1,6 +1,6 @@
1
use serde::{Deserialize, Serialize};
2
3
-#[derive(Debug, Serialize)]
3
+#[derive(Debug, Serialize, Deserialize)]
4
pub struct User {
5
pub(crate) email: String,
6
pub(crate) password: String,
smbpndk-cli/src/account/signup/process.rs
+56
-42
@@ -1,27 +1,53 @@
1
use crate::account::{
2
- lib::{authorize_github, process_connect_github},
3
- model::{Data, Status, User},
2
+ lib::authorize_github,
3
+ model::{Data, Status},
4
};
5
use anyhow::{anyhow, Result};
6
use console::{style, Term};
7
use dialoguer::{theme::ColorfulTheme, Input, Password, Select};
8
use log::debug;
9
use reqwest::{Client, StatusCode};
10
-use serde::{de, Deserialize, Serialize};
10
+use serde::{Deserialize, Serialize};
11
use smbpndk_model::CommandResult;
12
use smbpndk_networking::constants::BASE_URL;
13
use smbpndk_utils::email_validation;
14
use spinners::Spinner;
15
use std::fmt::{Display, Formatter};
16
-
16
use super::SignupMethod;
17
+
18
pub struct SignupArgs {
19
- pub username: String,
20
- pub password: String,
19
+ pub email: String,
20
+ pub password: Option<String>,
21
+ pub password_confirmation: Option<String>,
22
+ pub authorizations_attributes: Vec<Provider>
23
+}
24
+
25
+#[derive(Debug, Serialize)]
26
+pub struct Provider {
27
+ pub uid: String,
28
+ pub provider: i8
29
+}
30
+
31
+#[derive(Debug, Serialize)]
32
+pub struct SignupGithubParams {
33
+ pub user: SignupUserGithub,
34
+}
35
+
36
+#[derive(Debug, Serialize)]
37
+pub struct SignupEmailParams {
38
+ pub user: SignupUserEmail,
39
}
40
+
41
+#[derive(Debug, Serialize)]
42
+pub struct SignupUserGithub {
43
+ pub email: String,
44
+ pub authorizations_attributes: Vec<Provider>
45
+}
46
+
47
#[derive(Debug, Serialize)]
23
-pub struct SignupParams {
24
- pub user: User,
48
+pub struct SignupUserEmail {
49
+ pub email: String,
50
+ pub password: String,
51
}
52
53
#[derive(Debug, Serialize, Deserialize)]
@@ -45,7 +71,7 @@ pub async fn process_signup() -> Result<CommandResult> {
71
}
72
}
73
48
-async fn signup_with_email(email: Option<String>) -> Result<CommandResult> {
74
+pub async fn signup_with_email(email: Option<String>) -> Result<CommandResult> {
75
let email = if let Some(email) = email {
76
email
77
} else {
@@ -74,12 +100,11 @@ async fn signup_with_email(email: Option<String>) -> Result<CommandResult> {
100
style("Signing up...").green().bold().to_string(),
101
);
102
77
- match do_signup(SignupArgs {
78
- username: email,
79
- password,
80
- })
81
- .await
82
- {
103
+ let params = SignupEmailParams {
104
+ user: SignupUserEmail { email, password },
105
+ };
106
+
107
+ match do_signup(¶ms).await {
108
Ok(_) => Ok(CommandResult {
109
spinner,
110
symbol: style("✅".to_string()).for_stderr().green().to_string(),
@@ -123,8 +148,8 @@ struct GithubUser {
148
}
149
150
#[derive(Debug, Serialize, Deserialize, Clone)]
126
-struct GithubEmail {
127
- email: String,
151
+pub struct GithubEmail {
152
+ pub email: String,
153
primary: bool,
154
verified: bool,
155
visibility: Option<String>,
@@ -136,44 +161,33 @@ impl Display for GithubEmail {
161
}
162
}
163
139
-fn select_github_emails(github_emails: Vec<GithubEmail>) -> Result<GithubEmail> {
140
- let selection = Select::with_theme(&ColorfulTheme::default())
141
- .with_prompt("Select your email")
142
- .items(&github_emails)
143
- .default(0)
144
- .interact_on_opt(&Term::stderr())
145
- .map(|i| &github_emails[i.unwrap()])
146
- .unwrap()
147
- .to_owned();
148
- Ok(selection)
149
-}
150
-
151
-async fn do_signup(args: SignupArgs) -> Result<()> {
152
- let signup_params = SignupParams {
153
- user: User {
154
- email: args.username,
155
- password: args.password,
156
- },
157
- };
164
+pub async fn do_signup<T: Serialize + ?Sized>(args: &T) -> Result<CommandResult> {
165
+ let spinner = Spinner::new(
166
+ spinners::Spinners::BouncingBall,
167
+ style("Signing you up...")
168
+ .green()
169
+ .bold()
170
+ .to_string(),
171
+ );
172
173
let response = Client::new()
174
.post([BASE_URL, "/v1/users"].join(""))
161
- .json(&signup_params)
175
+ .json(&args)
176
.send()
177
.await?;
178
179
match response.status() {
166
- StatusCode::OK => {}
180
+ StatusCode::OK => {
181
+ Ok(CommandResult { spinner, symbol: "Yes".to_owned(), msg: "Success. Check email".to_owned() })
182
+ }
183
StatusCode::UNPROCESSABLE_ENTITY => {
184
let result: SignupResult = response.json().await?;
185
let error = anyhow!("Failed to signup: {}", result.status.message);
170
- return Err(error);
186
+ Err(error)
187
}
188
_ => {
189
let error = anyhow!("Failed to signup: {}", response.status());
174
- return Err(error);
190
+ Err(error)
191
}
192
}
177
-
178
- Ok(())
193
}