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: succeed_symbol(),
183 msg: succeed_message("Doing nothing."),
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: succeed_symbol(),
214 msg: succeed_message("Verification email sent!"),
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 succeed_message("Cancel operation."),
240 );
241 return Ok(CommandResult {
242 spinner,
243 symbol: succeed_symbol(),
244 msg: succeed_message("Doing nothing."),
245 });
246 }
247
248 let spinner = Spinner::new(
249 spinners::Spinners::SimpleDotsScrolling,
250 succeed_message("Linking your GitHub account..."),
251 );
252
253 let response = Client::new()
254 .post(build_smb_connect_github_url(env))
255 .json(&auth)
256 .header("Accept", "application/json")
257 .header("Content-Type", "application/x-www-form-urlencoded")
258 .send()
259 .await?;
260
261 match response.status() {
262 reqwest::StatusCode::OK => Ok(CommandResult {
263 spinner,
264 symbol: succeed_symbol(),
265 msg: succeed_message("GitHub account linked!"),
266 }),
267 _ => {
268 let error = anyhow!("Failed to link GitHub account.");
269 Err(error)
270 }
271 }
272}
273
274async fn login_with_email(env: Environment) -> Result<CommandResult> {
275 println!("Provide your login credentials.");
276 let username = match Input::<String>::with_theme(&ColorfulTheme::default())
277 .with_prompt("Email")
278 .validate_with(|email: &String| email_validation(email))
279 .interact()
280 {
281 Ok(email) => email,
282 Err(_) => {
283 let error = anyhow!("Invalid email.");
284 return Err(error);
285 }
286 };
287 let password = match Password::with_theme(&ColorfulTheme::default())
288 .with_prompt("Password")
289 .interact()
290 {
291 Ok(password) => password,
292 Err(_) => {
293 let error = anyhow!("Invalid password.");
294 return Err(error);
295 }
296 };
297 do_process_login(env, LoginArgs { username, password }).await
298}
299
300async fn do_process_login(env: Environment, args: LoginArgs) -> Result<CommandResult> {
301 let login_params = LoginParams {
302 user: UserParam {
303 email: args.username,
304 password: args.password,
305 },
306 };
307
308 let response = match Client::new()
309 .post(build_smb_login_url(env))
310 .json(&login_params)
311 .send()
312 .await
313 {
314 Ok(response) => response,
315 Err(_) => return Err(anyhow!(fail_message("Check your internet connection."))),
316 };
317
318 match response.status() {
319 StatusCode::OK => {
320 save_token(env, &response).await?;
322 Ok(CommandResult {
323 spinner: Spinner::new(
324 spinners::Spinners::SimpleDotsScrolling,
325 style("Loading...").green().bold().to_string(),
326 ),
327 symbol: succeed_symbol(),
328 msg: succeed_message("You are logged in!"),
329 })
330 }
331 StatusCode::NOT_FOUND => {
332 Ok(CommandResult {
334 spinner: Spinner::new(
335 spinners::Spinners::SimpleDotsScrolling,
336 style("Account not found.").green().bold().to_string(),
337 ),
338 symbol: fail_symbol(),
339 msg: fail_message("Please signup!"),
340 })
341 }
342 StatusCode::UNPROCESSABLE_ENTITY => {
343 let result: SmbAuthorization = response.json().await?;
345 verify_or_set_password(&env, result).await
347 }
348 _ => Err(anyhow!(fail_message(
349 "Login failed. Check your username and password."
350 ))),
351 }
352}
353
354async fn verify_or_set_password(
355 env: &Environment,
356 result: SmbAuthorization,
357) -> Result<CommandResult> {
358 match result.error_code {
359 Some(error_code) => {
360 debug!("{}", error_code);
361 match error_code {
362 ErrorCode::EmailUnverified => send_email_verification(*env, result.user).await,
363 ErrorCode::PasswordNotSet => send_reset_password(*env, result.user).await,
364 _ => Err(anyhow!("Shouldn't be here.")),
365 }
366 }
367 None => Err(anyhow!("Shouldn't be here.")),
368 }
369}
370
371async fn send_reset_password(env: Environment, user: Option<User>) -> Result<CommandResult> {
372 if let Some(user) = user {
374 let confirm = match Confirm::with_theme(&ColorfulTheme::default())
375 .with_prompt("Do you want to reset your password?")
376 .interact()
377 {
378 Ok(confirm) => confirm,
379 Err(_) => {
380 let error = anyhow!("Invalid input.");
381 return Err(error);
382 }
383 };
384
385 if !confirm {
387 let spinner = Spinner::new(
388 spinners::Spinners::SimpleDotsScrolling,
389 style("Cancel operation.").green().bold().to_string(),
390 );
391 return Ok(CommandResult {
392 spinner,
393 symbol: succeed_symbol(),
394 msg: succeed_message("Doing nothing."),
395 });
396 }
397 resend_reset_password_instruction(env, user).await
398 } else {
399 let error = anyhow!("Failed to get user.");
400 Err(error)
401 }
402}
403
404async fn resend_reset_password_instruction(env: Environment, user: User) -> Result<CommandResult> {
405 let mut spinner = Spinner::new(
406 spinners::Spinners::SimpleDotsScrolling,
407 succeed_message("Sending reset password instruction..."),
408 );
409 let response = Client::new()
410 .post(build_smb_resend_reset_password_instructions_url(env))
411 .body(format!("id={}", user.id))
412 .header("Accept", "application/json")
413 .header("Content-Type", "application/x-www-form-urlencoded")
414 .send()
415 .await?;
416
417 match response.status() {
418 StatusCode::OK => {
419 spinner.stop_and_persist(
420 "✅",
421 "Reset password instruction sent! Please check your email.".to_owned(),
422 );
423 input_reset_password_token(env).await
424 }
425 _ => {
426 let error = anyhow!("Failed to send reset password instruction.");
427 Err(error)
428 }
429 }
430}
431
432async fn input_reset_password_token(env: Environment) -> Result<CommandResult> {
433 let token = match Input::<String>::with_theme(&ColorfulTheme::default())
434 .with_prompt("Input reset password token")
435 .interact()
436 {
437 Ok(token) => token,
438 Err(_) => {
439 let error = anyhow!("Invalid token.");
440 return Err(error);
441 }
442 };
443 let password = match Password::with_theme(&ColorfulTheme::default())
444 .with_prompt("New password.")
445 .with_confirmation("Repeat password.", "Error: the passwords don't match.")
446 .interact()
447 {
448 Ok(password) => password,
449 Err(_) => {
450 let error = anyhow!("Invalid password.");
451 return Err(error);
452 }
453 };
454
455 let spinner = Spinner::new(
456 spinners::Spinners::SimpleDotsScrolling,
457 style("Resetting password...").green().bold().to_string(),
458 );
459
460 let password_confirmation = password.clone();
461
462 let params = Param {
463 user: UserUpdatePassword {
464 reset_password_token: token,
465 password,
466 password_confirmation,
467 },
468 };
469
470 let response = Client::new()
471 .put(build_smb_reset_password_url(env))
472 .json(¶ms)
473 .header("Accept", "application/json")
474 .header("Content-Type", "application/x-www-form-urlencoded")
475 .send()
476 .await?;
477
478 match response.status() {
479 StatusCode::OK => Ok(CommandResult {
480 spinner,
481 symbol: succeed_symbol(),
482 msg: succeed_message("Password reset!"),
483 }),
484 _ => Err(anyhow!(fail_message("Failed to reset password."))),
485 }
486}
487
488fn build_smb_login_url(env: Environment) -> String {
489 let mut url_builder = smb_base_url_builder(env);
490 url_builder.add_route(PATH_USERS_SIGN_IN);
491 url_builder.build()
492}
493
494fn build_smb_resend_email_verification_url(env: Environment) -> String {
495 let mut url_builder = smb_base_url_builder(env);
496 url_builder.add_route(PATH_RESEND_CONFIRMATION);
497 url_builder.build()
498}
499
500fn build_smb_resend_reset_password_instructions_url(env: Environment) -> String {
501 let mut url_builder = smb_base_url_builder(env);
502 url_builder.add_route(PATH_RESET_PASSWORD_INSTRUCTIONS);
503 url_builder.build()
504}
505
506fn build_smb_reset_password_url(env: Environment) -> String {
507 let mut url_builder = smb_base_url_builder(env);
508 url_builder.add_route(PATH_USERS_PASSWORD);
509 url_builder.build()
510}
511
512fn build_smb_connect_github_url(env: Environment) -> String {
513 let mut url_builder = smb_base_url_builder(env);
514 url_builder.add_route(PATH_LINK_GITHUB_ACCOUNT);
515 url_builder.build()
516}