@smbcloud / cli / commits / 960fb8c

WIP login

Seto Elkahfi committed Aug 25, 2023 at 23:57 UTC 960fb8ca887de7fb80f6b139224eadbfd33a8dea
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(&params).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(&params).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 }