1use crate::{
2 account::{
3 lib::{authorize_github, is_logged_in, 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;
11use dialoguer::{console::Term, 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_network::environment::Environment;
21use 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};
28use smbcloud_utils::email_validation;
29use spinners::Spinner;
30
31pub async fn process_login(env: Environment) -> Result<CommandResult> {
32 if is_logged_in(env) {
34 return Ok(CommandResult {
35 spinner: Spinner::new(
36 spinners::Spinners::SimpleDotsScrolling,
37 succeed_message("Loading"),
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 mut spinner = Spinner::new(
302 spinners::Spinners::SimpleDotsScrolling,
303 succeed_message("Loading"),
304 );
305
306 let login_params = LoginParams {
307 user: UserParam {
308 email: args.username,
309 password: args.password,
310 },
311 };
312
313 let response = match Client::new()
314 .post(build_smb_login_url(env))
315 .json(&login_params)
316 .send()
317 .await
318 {
319 Ok(response) => response,
320 Err(_) => return Err(anyhow!(fail_message("Check your internet connection."))),
321 };
322
323 match response.status() {
324 StatusCode::OK => {
325 save_token(env, &response).await?;
327 Ok(CommandResult {
328 spinner,
329 symbol: succeed_symbol(),
330 msg: succeed_message("You are logged in!"),
331 })
332 }
333 StatusCode::NOT_FOUND => {
334 Ok(CommandResult {
336 spinner,
337 symbol: fail_symbol(),
338 msg: fail_message("Account not found. Please signup!"),
339 })
340 }
341 StatusCode::UNPROCESSABLE_ENTITY => {
342 spinner.stop_and_persist(
343 &succeed_symbol(),
344 succeed_message("Please complete registration"),
345 );
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: succeed_symbol(),
397 msg: succeed_message("Doing nothing."),
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 succeed_message("Sending reset password instruction..."),
411 );
412 let response = Client::new()
413 .post(build_smb_resend_reset_password_instructions_url(env))
414 .body(format!("id={}", user.id))
415 .header("Accept", "application/json")
416 .header("Content-Type", "application/x-www-form-urlencoded")
417 .send()
418 .await?;
419
420 match response.status() {
421 StatusCode::OK => {
422 spinner.stop_and_persist(
423 "✅",
424 "Reset password instruction sent! Please check your email.".to_owned(),
425 );
426 input_reset_password_token(env).await
427 }
428 _ => {
429 let error = anyhow!("Failed to send reset password instruction.");
430 Err(error)
431 }
432 }
433}
434
435async fn input_reset_password_token(env: Environment) -> Result<CommandResult> {
436 let token = match Input::<String>::with_theme(&ColorfulTheme::default())
437 .with_prompt("Input reset password token")
438 .interact()
439 {
440 Ok(token) => token,
441 Err(_) => {
442 let error = anyhow!("Invalid token.");
443 return Err(error);
444 }
445 };
446 let password = match Password::with_theme(&ColorfulTheme::default())
447 .with_prompt("New password.")
448 .with_confirmation("Repeat password.", "Error: the passwords don't match.")
449 .interact()
450 {
451 Ok(password) => password,
452 Err(_) => {
453 let error = anyhow!("Invalid password.");
454 return Err(error);
455 }
456 };
457
458 let spinner = Spinner::new(
459 spinners::Spinners::SimpleDotsScrolling,
460 style("Resetting password...").green().bold().to_string(),
461 );
462
463 let password_confirmation = password.clone();
464
465 let params = Param {
466 user: UserUpdatePassword {
467 reset_password_token: token,
468 password,
469 password_confirmation,
470 },
471 };
472
473 let response = Client::new()
474 .put(build_smb_reset_password_url(env))
475 .json(¶ms)
476 .header("Accept", "application/json")
477 .header("Content-Type", "application/x-www-form-urlencoded")
478 .send()
479 .await?;
480
481 match response.status() {
482 StatusCode::OK => Ok(CommandResult {
483 spinner,
484 symbol: succeed_symbol(),
485 msg: succeed_message("Password reset!"),
486 }),
487 _ => Err(anyhow!(fail_message("Failed to reset password."))),
488 }
489}
490
491fn build_smb_login_url(env: Environment) -> String {
492 let mut url_builder = smb_base_url_builder(env);
493 url_builder.add_route(PATH_USERS_SIGN_IN);
494 url_builder.build()
495}
496
497fn build_smb_resend_email_verification_url(env: Environment) -> String {
498 let mut url_builder = smb_base_url_builder(env);
499 url_builder.add_route(PATH_RESEND_CONFIRMATION);
500 url_builder.build()
501}
502
503fn build_smb_resend_reset_password_instructions_url(env: Environment) -> String {
504 let mut url_builder = smb_base_url_builder(env);
505 url_builder.add_route(PATH_RESET_PASSWORD_INSTRUCTIONS);
506 url_builder.build()
507}
508
509fn build_smb_reset_password_url(env: Environment) -> String {
510 let mut url_builder = smb_base_url_builder(env);
511 url_builder.add_route(PATH_USERS_PASSWORD);
512 url_builder.build()
513}
514
515fn build_smb_connect_github_url(env: Environment) -> String {
516 let mut url_builder = smb_base_url_builder(env);
517 url_builder.add_route(PATH_LINK_GITHUB_ACCOUNT);
518 url_builder.build()
519}