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