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 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
66async 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 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 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 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 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 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, ¶ms).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 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 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 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 if let Some(_) = auth.error_code {
293 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 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 Ok(CommandResult {
355 spinner,
356 symbol: fail_symbol(),
357 msg: fail_message("Account not found. Please signup!"),
358 })
359 }
360 StatusCode::UNPROCESSABLE_ENTITY => {
361 let result: SmbAuthorization = response.json().await?;
363 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 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 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(¶ms)
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}