smbcloud_cli/account/login/
process.rs

1use {
2    crate::account::signup::signup_with_email,
3    crate::account::{
4        lib::{authorize_github, is_logged_in, save_token},
5        signup::{do_signup, SignupMethod},
6    },
7    crate::cli::CommandResult,
8    crate::ui::{fail_message, fail_symbol, succeed_message, succeed_symbol},
9    anyhow::{anyhow, Result},
10    console::style,
11    dialoguer::{console::Term, theme::ColorfulTheme, Confirm, Input, Password, Select},
12    log::debug,
13    reqwest::{Client, StatusCode},
14    smbcloud_model::{
15        account::{ErrorCode, GithubInfo, SmbAuthorization, User},
16        forgot::{Param, UserUpdatePassword},
17        login::{LoginArgs, LoginParams, UserParam},
18        signup::{GithubEmail, Provider, SignupGithubParams, SignupUserGithub},
19    },
20    smbcloud_network::environment::Environment,
21    smbcloud_networking::{
22        constants::{
23            PATH_LINK_GITHUB_ACCOUNT, PATH_RESEND_CONFIRMATION, PATH_RESET_PASSWORD_INSTRUCTIONS,
24            PATH_USERS_PASSWORD, PATH_USERS_SIGN_IN,
25        },
26        smb_base_url_builder,
27    },
28    smbcloud_networking_account::signup::check_email,
29    smbcloud_utils::email_validation,
30    spinners::Spinner,
31};
32
33pub async fn process_login(env: Environment) -> Result<CommandResult> {
34    // Check if token file exists
35    if is_logged_in(env) {
36        return Ok(CommandResult {
37            spinner: Spinner::new(
38                spinners::Spinners::SimpleDotsScrolling,
39                succeed_message("Loading"),
40            ),
41            symbol: fail_symbol(),
42            msg: fail_message("You are already logged in. Please logout first."),
43        });
44    }
45
46    let signup_methods = vec![SignupMethod::Email, SignupMethod::GitHub];
47    let selection = match Select::with_theme(&ColorfulTheme::default())
48        .items(&signup_methods)
49        .default(0)
50        .interact_on_opt(&Term::stderr())
51        .map(|i| signup_methods[i.unwrap()])
52    {
53        Ok(method) => method,
54        Err(_) => {
55            let error = anyhow!("No selection made.");
56            return Err(error);
57        }
58    };
59
60    match selection {
61        SignupMethod::Email => login_with_email(env).await,
62        SignupMethod::GitHub => login_with_github(env).await,
63    }
64}
65
66// Private functions
67
68async fn login_with_github(env: Environment) -> Result<CommandResult> {
69    match authorize_github(&env).await {
70        Ok(result) => process_authorization(env, result).await,
71        Err(err) => {
72            let error = anyhow!("Failed to authorize your GitHub account. {}", err);
73            Err(error)
74        }
75    }
76}
77
78async fn process_authorization(env: Environment, auth: SmbAuthorization) -> Result<CommandResult> {
79    // What to do if not logged in with GitHub?
80    // Check error_code first
81    if let Some(error_code) = auth.error_code {
82        debug!("{}", error_code);
83        match error_code {
84            ErrorCode::EmailNotFound => {
85                return create_new_account(env, auth.user_email, auth.user_info).await
86            }
87            ErrorCode::EmailUnverified => return send_email_verification(env, auth.user).await,
88            ErrorCode::PasswordNotSet => {
89                // Only for email and password login
90                let error = anyhow!("Password not set.");
91                return Err(error);
92            }
93            ErrorCode::GithubNotLinked => return connect_github_account(env, auth).await,
94        }
95    }
96
97    // Logged in with GitHub!
98    // Token handling is in the lib.rs account module.
99    if let Some(user) = auth.user {
100        let spinner = Spinner::new(
101            spinners::Spinners::SimpleDotsScrolling,
102            style("Logging you in...").green().bold().to_string(),
103        );
104        // We're logged in with GitHub.
105        return Ok(CommandResult {
106            spinner,
107            symbol: "✅".to_owned(),
108            msg: format!("You are logged in with GitHub as {}.", user.email),
109        });
110    }
111
112    let error: anyhow::Error = anyhow!("Failed to login with GitHub.");
113    Err(error)
114}
115
116async fn create_new_account(
117    env: Environment,
118    user_email: Option<GithubEmail>,
119    user_info: Option<GithubInfo>,
120) -> Result<CommandResult> {
121    let confirm = match Confirm::with_theme(&ColorfulTheme::default())
122        .with_prompt("Do you want to create a new account?")
123        .interact()
124    {
125        Ok(confirm) => confirm,
126        Err(_) => {
127            let error = anyhow!("Invalid input.");
128            return Err(error);
129        }
130    };
131
132    // Create account if user confirms
133    if !confirm {
134        let spinner = Spinner::new(
135            spinners::Spinners::SimpleDotsScrolling,
136            style("Logging you in...").green().bold().to_string(),
137        );
138        return Ok(CommandResult {
139            spinner,
140            symbol: "✅".to_owned(),
141            msg: "Please accept to link your GitHub account.".to_owned(),
142        });
143    }
144
145    if let (Some(email), Some(info)) = (user_email, user_info) {
146        let params = SignupGithubParams {
147            user: SignupUserGithub {
148                email: email.email,
149                authorizations_attributes: vec![Provider {
150                    uid: info.id.to_string(),
151                    provider: 0,
152                }],
153            },
154        };
155
156        return do_signup(env, &params).await;
157    }
158
159    Err(anyhow!("Shouldn't be here."))
160}
161
162async fn send_email_verification(env: Environment, user: Option<User>) -> Result<CommandResult> {
163    // Return early if user is null
164    if let Some(user) = user {
165        let confirm = match Confirm::with_theme(&ColorfulTheme::default())
166            .with_prompt("Do you want to send a new verification email?")
167            .interact()
168        {
169            Ok(confirm) => confirm,
170            Err(_) => {
171                let error = anyhow!("Invalid input.");
172                return Err(error);
173            }
174        };
175
176        // Send verification email if user confirms
177        if !confirm {
178            let spinner = Spinner::new(
179                spinners::Spinners::SimpleDotsScrolling,
180                style("Cancel operation.").green().bold().to_string(),
181            );
182            return Ok(CommandResult {
183                spinner,
184                symbol: succeed_symbol(),
185                msg: succeed_message("Doing nothing."),
186            });
187        }
188        resend_email_verification(env, user).await
189    } else {
190        let error = anyhow!("Failed to get user.");
191        Err(error)
192    }
193}
194
195async fn resend_email_verification(env: Environment, user: User) -> Result<CommandResult> {
196    let spinner = Spinner::new(
197        spinners::Spinners::SimpleDotsScrolling,
198        style("Sending verification email...")
199            .green()
200            .bold()
201            .to_string(),
202    );
203
204    let response = Client::new()
205        .post(build_smb_resend_email_verification_url(env))
206        .body(format!("id={}", user.id))
207        .header("Accept", "application/json")
208        .header("Content-Type", "application/x-www-form-urlencoded")
209        .send()
210        .await?;
211
212    match response.status() {
213        reqwest::StatusCode::OK => Ok(CommandResult {
214            spinner,
215            symbol: succeed_symbol(),
216            msg: succeed_message("Verification email sent!"),
217        }),
218        _ => {
219            let error = anyhow!("Failed to send verification email.");
220            Err(error)
221        }
222    }
223}
224
225async fn connect_github_account(env: Environment, auth: SmbAuthorization) -> Result<CommandResult> {
226    let confirm = match Confirm::with_theme(&ColorfulTheme::default())
227        .with_prompt("Do you want to link your GitHub account?")
228        .interact()
229    {
230        Ok(confirm) => confirm,
231        Err(_) => {
232            let error = anyhow!("Invalid input.");
233            return Err(error);
234        }
235    };
236
237    // Link GitHub account if user confirms
238    if !confirm {
239        let spinner = Spinner::new(
240            spinners::Spinners::SimpleDotsScrolling,
241            succeed_message("Cancel operation."),
242        );
243        return Ok(CommandResult {
244            spinner,
245            symbol: succeed_symbol(),
246            msg: succeed_message("Doing nothing."),
247        });
248    }
249
250    let spinner = Spinner::new(
251        spinners::Spinners::SimpleDotsScrolling,
252        succeed_message("Linking your GitHub account..."),
253    );
254
255    let response = Client::new()
256        .post(build_smb_connect_github_url(env))
257        .json(&auth)
258        .header("Accept", "application/json")
259        .header("Content-Type", "application/x-www-form-urlencoded")
260        .send()
261        .await?;
262
263    match response.status() {
264        reqwest::StatusCode::OK => Ok(CommandResult {
265            spinner,
266            symbol: succeed_symbol(),
267            msg: succeed_message("GitHub account linked!"),
268        }),
269        _ => {
270            let error = anyhow!("Failed to link GitHub account.");
271            Err(error)
272        }
273    }
274}
275
276async fn login_with_email(env: Environment) -> Result<CommandResult> {
277    println!("Provide your login credentials.");
278    let username = match Input::<String>::with_theme(&ColorfulTheme::default())
279        .with_prompt("Email")
280        .validate_with(|email: &String| email_validation(email))
281        .interact()
282    {
283        Ok(email) => email,
284        Err(_) => {
285            let error = anyhow!("Invalid email.");
286            return Err(error);
287        }
288    };
289    match check_email(env, &username).await {
290        Ok(auth) => {
291            // Only continue with password input if email is found and confirmed.
292            if let Some(_) = auth.error_code {
293                // Check if email is in the database, unconfirmed. Only presents password input if email is found and confirmed.
294                let spinner = Spinner::new(
295                    spinners::Spinners::SimpleDotsScrolling,
296                    succeed_message("Checking email"),
297                );
298                after_checking_email_step(&env, spinner, auth, Some(username)).await
299            } else {
300                let password = match Password::with_theme(&ColorfulTheme::default())
301                    .with_prompt("Password")
302                    .interact()
303                {
304                    Ok(password) => password,
305                    Err(_) => {
306                        let error = anyhow!("Invalid password.");
307                        return Err(error);
308                    }
309                };
310                do_process_login(env, LoginArgs { username, password }).await
311            }
312        }
313        Err(_) => Err(anyhow!(fail_message(
314            "Server error. Please try again later."
315        ))),
316    }
317}
318
319async fn do_process_login(env: Environment, args: LoginArgs) -> Result<CommandResult> {
320    let spinner = Spinner::new(
321        spinners::Spinners::SimpleDotsScrolling,
322        succeed_message("Loading"),
323    );
324
325    let login_params = LoginParams {
326        user: UserParam {
327            email: args.username,
328            password: args.password,
329        },
330    };
331
332    let response = match Client::new()
333        .post(build_smb_login_url(env))
334        .json(&login_params)
335        .send()
336        .await
337    {
338        Ok(response) => response,
339        Err(_) => return Err(anyhow!(fail_message("Check your internet connection."))),
340    };
341
342    match response.status() {
343        StatusCode::OK => {
344            // Login successful
345            save_token(env, &response).await?;
346            Ok(CommandResult {
347                spinner,
348                symbol: succeed_symbol(),
349                msg: succeed_message("You are logged in!"),
350            })
351        }
352        StatusCode::NOT_FOUND => {
353            // Account not found and we show signup option
354            Ok(CommandResult {
355                spinner,
356                symbol: fail_symbol(),
357                msg: fail_message("Account not found. Please signup!"),
358            })
359        }
360        StatusCode::UNPROCESSABLE_ENTITY => {
361            // Account found but email not verified / password not set
362            let result: SmbAuthorization = response.json().await?;
363            // println!("Result: {:#?}", &result);
364            after_checking_email_step(&env, spinner, result, None).await
365        }
366        _ => Err(anyhow!(fail_message(
367            "Login failed. Check your username and password."
368        ))),
369    }
370}
371
372async fn after_checking_email_step(
373    env: &Environment,
374    mut spinner: Spinner,
375    result: SmbAuthorization,
376    username: Option<String>,
377) -> Result<CommandResult> {
378    match result.error_code {
379        Some(error_code) => {
380            debug!("{}", error_code);
381            match error_code {
382                ErrorCode::EmailNotFound => {
383                    spinner.stop_and_persist(
384                        &succeed_symbol(),
385                        succeed_message(
386                            "Account not found. Please continue with setting up your account.",
387                        ),
388                    );
389                    signup_with_email(env.to_owned(), username).await
390                }
391                ErrorCode::EmailUnverified => {
392                    spinner.stop_and_persist(
393                        &succeed_symbol(),
394                        succeed_message("Email not verified. Please verify your email."),
395                    );
396                    send_email_verification(*env, result.user).await
397                }
398                ErrorCode::PasswordNotSet => {
399                    spinner.stop_and_persist(
400                        &succeed_symbol(),
401                        succeed_message("Password not set. Please reset your password."),
402                    );
403                    send_reset_password(*env, result.user).await
404                }
405                _ => {
406                    spinner.stop_and_persist(&fail_symbol(), fail_message("An error occurred."));
407                    Err(anyhow!("Idk what happened."))
408                }
409            }
410        }
411        None => Err(anyhow!("Shouldn't be here.")),
412    }
413}
414
415async fn send_reset_password(env: Environment, user: Option<User>) -> Result<CommandResult> {
416    // Return early if user is null
417    if let Some(user) = user {
418        let confirm = match Confirm::with_theme(&ColorfulTheme::default())
419            .with_prompt("Do you want to reset your password?")
420            .interact()
421        {
422            Ok(confirm) => confirm,
423            Err(_) => {
424                let error = anyhow!("Invalid input.");
425                return Err(error);
426            }
427        };
428
429        // Send verification email if user confirms
430        if !confirm {
431            let spinner = Spinner::new(
432                spinners::Spinners::SimpleDotsScrolling,
433                style("Cancel operation.").green().bold().to_string(),
434            );
435            return Ok(CommandResult {
436                spinner,
437                symbol: succeed_symbol(),
438                msg: succeed_message("Doing nothing."),
439            });
440        }
441        resend_reset_password_instruction(env, user).await
442    } else {
443        let error = anyhow!("Failed to get user.");
444        Err(error)
445    }
446}
447
448async fn resend_reset_password_instruction(env: Environment, user: User) -> Result<CommandResult> {
449    let mut spinner = Spinner::new(
450        spinners::Spinners::SimpleDotsScrolling,
451        succeed_message("Sending reset password instruction..."),
452    );
453    let response = Client::new()
454        .post(build_smb_resend_reset_password_instructions_url(env))
455        .body(format!("id={}", user.id))
456        .header("Accept", "application/json")
457        .header("Content-Type", "application/x-www-form-urlencoded")
458        .send()
459        .await?;
460
461    match response.status() {
462        StatusCode::OK => {
463            spinner.stop_and_persist(
464                "✅",
465                "Reset password instruction sent! Please check your email.".to_owned(),
466            );
467            input_reset_password_token(env).await
468        }
469        _ => {
470            let error = anyhow!("Failed to send reset password instruction.");
471            Err(error)
472        }
473    }
474}
475
476async fn input_reset_password_token(env: Environment) -> Result<CommandResult> {
477    let token = match Input::<String>::with_theme(&ColorfulTheme::default())
478        .with_prompt("Input reset password token")
479        .interact()
480    {
481        Ok(token) => token,
482        Err(_) => {
483            let error = anyhow!("Invalid token.");
484            return Err(error);
485        }
486    };
487    let password = match Password::with_theme(&ColorfulTheme::default())
488        .with_prompt("New password.")
489        .with_confirmation("Repeat password.", "Error: the passwords don't match.")
490        .interact()
491    {
492        Ok(password) => password,
493        Err(_) => {
494            let error = anyhow!("Invalid password.");
495            return Err(error);
496        }
497    };
498
499    let spinner = Spinner::new(
500        spinners::Spinners::SimpleDotsScrolling,
501        style("Resetting password...").green().bold().to_string(),
502    );
503
504    let password_confirmation = password.clone();
505
506    let params = Param {
507        user: UserUpdatePassword {
508            reset_password_token: token,
509            password,
510            password_confirmation,
511        },
512    };
513
514    let response = Client::new()
515        .put(build_smb_reset_password_url(env))
516        .json(&params)
517        .header("Accept", "application/json")
518        .header("Content-Type", "application/x-www-form-urlencoded")
519        .send()
520        .await?;
521
522    match response.status() {
523        StatusCode::OK => Ok(CommandResult {
524            spinner,
525            symbol: succeed_symbol(),
526            msg: succeed_message("Password reset!"),
527        }),
528        _ => Err(anyhow!(fail_message("Failed to reset password."))),
529    }
530}
531
532fn build_smb_login_url(env: Environment) -> String {
533    let mut url_builder = smb_base_url_builder(env);
534    url_builder.add_route(PATH_USERS_SIGN_IN);
535    url_builder.build()
536}
537
538fn build_smb_resend_email_verification_url(env: Environment) -> String {
539    let mut url_builder = smb_base_url_builder(env);
540    url_builder.add_route(PATH_RESEND_CONFIRMATION);
541    url_builder.build()
542}
543
544fn build_smb_resend_reset_password_instructions_url(env: Environment) -> String {
545    let mut url_builder = smb_base_url_builder(env);
546    url_builder.add_route(PATH_RESET_PASSWORD_INSTRUCTIONS);
547    url_builder.build()
548}
549
550fn build_smb_reset_password_url(env: Environment) -> String {
551    let mut url_builder = smb_base_url_builder(env);
552    url_builder.add_route(PATH_USERS_PASSWORD);
553    url_builder.build()
554}
555
556fn build_smb_connect_github_url(env: Environment) -> String {
557    let mut url_builder = smb_base_url_builder(env);
558    url_builder.add_route(PATH_LINK_GITHUB_ACCOUNT);
559    url_builder.build()
560}