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