1 use crate::account::{
2 forgot::{Param, UserUpdatePassword},
3 lib::{authorize_github, smb_base_url_builder, ErrorCode, GithubInfo, SmbAuthorization},
4 model::{Data, Status, User},
5 signup::{
6 do_signup, GithubEmail, Provider, SignupGithubParams, SignupMethod, SignupUserGithub,
7 },
8 };
9 use anyhow::{anyhow, Result};
10 use console::{style, Term};
11 use dialoguer::{theme::ColorfulTheme, Confirm, Input, Password, Select};
12 use log::debug;
13 use reqwest::{Client, Response, StatusCode};
14 use serde::{Deserialize, Serialize};
15 use serde_json::json;
16 use smbpndk_model::CommandResult;
17 use smbpndk_utils::email_validation;
18 use spinners::Spinner;
19 use std::{
20 fs::{self, create_dir_all, OpenOptions},
21 io::Write,
22 };
23
24 pub struct LoginArgs {
25 pub username: String,
26 pub password: String,
27 }
28
29 #[derive(Debug, Serialize)]
30 struct LoginParams {
31 user: UserParam,
32 }
33
34 #[derive(Debug, Serialize)]
35 struct UserParam {
36 email: String,
37 password: String,
38 }
39
40 #[derive(Debug, Serialize, Deserialize)]
41 struct LoginResult {
42 status: Status,
43 data: Data,
44 }
45
46 pub async fn process_login() -> Result<CommandResult> {
47 let signup_methods = vec![SignupMethod::Email, SignupMethod::GitHub];
48 let selection = Select::with_theme(&ColorfulTheme::default())
49 .items(&signup_methods)
50 .default(0)
51 .interact_on_opt(&Term::stderr())
52 .map(|i| signup_methods[i.unwrap()])
53 .unwrap();
54
55 match selection {
56 SignupMethod::Email => login_with_email().await,
57 SignupMethod::GitHub => login_with_github().await,
58 }
59 }
60
61 async fn login_with_github() -> Result<CommandResult> {
62 match authorize_github().await {
63 Ok(result) => process_authorization(result).await,
64 Err(err) => {
65 let error = anyhow!("Failed to authorize your GitHub account. {}", err);
66 Err(error)
67 }
68 }
69 }
70
71 async fn process_authorization(auth: SmbAuthorization) -> Result<CommandResult> {
72 // What to do if not logged in with GitHub?
73 // Check error_code first
74 if let Some(error_code) = auth.error_code {
75 debug!("{}", error_code);
76 match error_code {
77 ErrorCode::EmailNotFound => {
78 return create_new_account(auth.user_email, auth.user_info).await
79 }
80 ErrorCode::EmailUnverified => return send_email_verification(auth.user).await,
81 ErrorCode::PasswordNotSet => {
82 // Only for email and password login
83 let error = anyhow!("Password not set.");
84 return Err(error);
85 }
86 }
87 }
88
89 // Logged in with GitHub
90 if let Some(user) = auth.user {
91 println!("You are logged in with GitHub as {}.", user);
92 let spinner = Spinner::new(
93 spinners::Spinners::SimpleDotsScrolling,
94 style("Logging you in...").green().bold().to_string(),
95 );
96 // We're logged in with GitHub, but not with SMB.
97 return Ok(CommandResult {
98 spinner,
99 symbol: "✅".to_owned(),
100 msg: "You are logged in!".to_owned(),
101 });
102 }
103
104 let error: anyhow::Error = anyhow!("Failed to login with GitHub.");
105 Err(error)
106 }
107
108 async fn create_new_account(
109 user_email: Option<GithubEmail>,
110 user_info: Option<GithubInfo>,
111 ) -> Result<CommandResult> {
112 let confirm = Confirm::with_theme(&ColorfulTheme::default())
113 .with_prompt("Do you want to create a new account?")
114 .interact()
115 .unwrap();
116
117 // Create account if user confirms
118 if !confirm {
119 let spinner = Spinner::new(
120 spinners::Spinners::SimpleDotsScrolling,
121 style("Logging you in...").green().bold().to_string(),
122 );
123 return Ok(CommandResult {
124 spinner,
125 symbol: "✅".to_owned(),
126 msg: "Please accept to link your GitHub account.".to_owned(),
127 });
128 }
129
130 if let (Some(email), Some(info)) = (user_email, user_info) {
131 let params = SignupGithubParams {
132 user: SignupUserGithub {
133 email: email.email,
134 authorizations_attributes: vec![Provider {
135 uid: info.id.to_string(),
136 provider: 0,
137 }],
138 },
139 };
140
141 return do_signup(&params).await;
142 }
143
144 Err(anyhow!("Shouldn't be here."))
145 }
146
147 async fn send_email_verification(user: Option<User>) -> Result<CommandResult> {
148 // Return early if user is null
149 if let Some(user) = user {
150 let confirm = Confirm::with_theme(&ColorfulTheme::default())
151 .with_prompt("Do you want to send a new verification email?")
152 .interact()
153 .unwrap();
154
155 // Send verification email if user confirms
156 if !confirm {
157 let spinner = Spinner::new(
158 spinners::Spinners::SimpleDotsScrolling,
159 style("Cancel operation.").green().bold().to_string(),
160 );
161 return Ok(CommandResult {
162 spinner,
163 symbol: "✅".to_owned(),
164 msg: "Doing nothing.".to_owned(),
165 });
166 }
167 resend_email_verification(user).await
168 } else {
169 let error = anyhow!("Failed to get user.");
170 Err(error)
171 }
172 }
173
174 async fn resend_email_verification(user: User) -> Result<CommandResult> {
175 let spinner = Spinner::new(
176 spinners::Spinners::SimpleDotsScrolling,
177 style("Sending verification email...")
178 .green()
179 .bold()
180 .to_string(),
181 );
182
183 let response = Client::new()
184 .post(build_smb_resend_email_verification_url())
185 .body(format!("id={}", user.id))
186 .header("Accept", "application/json")
187 .header("Content-Type", "application/x-www-form-urlencoded")
188 .send()
189 .await?;
190
191 match response.status() {
192 reqwest::StatusCode::OK => Ok(CommandResult {
193 spinner,
194 symbol: "✅".to_owned(),
195 msg: "Verification email sent!".to_owned(),
196 }),
197 _ => {
198 let error = anyhow!("Failed to send verification email.");
199 Err(error)
200 }
201 }
202 }
203
204 async fn login_with_email() -> Result<CommandResult> {
205 println!("Provide your login credentials.");
206 let username = Input::<String>::with_theme(&ColorfulTheme::default())
207 .with_prompt("Email")
208 .validate_with(|email: &String| email_validation(email))
209 .interact()
210 .unwrap();
211 let password = Password::with_theme(&ColorfulTheme::default())
212 .with_prompt("Password")
213 .interact()
214 .unwrap();
215 do_process_login(LoginArgs { username, password }).await
216 }
217
218 async fn do_process_login(args: LoginArgs) -> Result<CommandResult> {
219 let login_params = LoginParams {
220 user: UserParam {
221 email: args.username,
222 password: args.password,
223 },
224 };
225
226 let response = Client::new()
227 .post(build_smb_login_url())
228 .json(&login_params)
229 .send()
230 .await?;
231
232 match response.status() {
233 StatusCode::OK => {
234 // Login successful
235 save_token(response).await
236 }
237 StatusCode::NOT_FOUND => {
238 // Account not found and we show signup option
239 Ok(CommandResult {
240 spinner: Spinner::new(
241 spinners::Spinners::SimpleDotsScrolling,
242 style("Account not found.").green().bold().to_string(),
243 ),
244 symbol: "✅".to_owned(),
245 msg: "Please signup!".to_owned(),
246 })
247 }
248 StatusCode::UNPROCESSABLE_ENTITY => {
249 // Account found but email not verified / password not set
250 let result: SmbAuthorization = response.json().await?;
251 println!("Result: {:#?}", &result);
252 return verify_or_set_password(result).await;
253 }
254 _ => {
255 let error = anyhow!("Login failed. Check your username and password.");
256 return Err(error);
257 }
258 }
259 }
260
261 async fn save_token(response: Response) -> Result<CommandResult> {
262 let headers = response.headers();
263 match headers.get("Authorization") {
264 Some(token) => {
265 debug!("{}", token.to_str()?);
266 match home::home_dir() {
267 Some(path) => {
268 debug!("{}", path.to_str().unwrap());
269 create_dir_all(path.join(".smb"))?;
270 let mut file = OpenOptions::new()
271 .create(true)
272 .write(true)
273 .open([path.to_str().unwrap(), "/.smb/token"].join(""))?;
274 file.write_all(token.to_str()?.as_bytes())?;
275
276 Ok(CommandResult {
277 spinner: Spinner::new(
278 spinners::Spinners::SimpleDotsScrolling,
279 style("Logging you in...").green().bold().to_string(),
280 ),
281 symbol: "✅".to_owned(),
282 msg: "You are logged in!".to_owned(),
283 })
284 }
285 None => {
286 let error = anyhow!("Failed to get home directory.");
287 return Err(error);
288 }
289 }
290 }
291 None => {
292 let error = anyhow!("Failed to get token. Probably a backend issue.");
293 return Err(error);
294 }
295 }
296 }
297
298 async fn verify_or_set_password(result: SmbAuthorization) -> Result<CommandResult> {
299 match result.error_code {
300 Some(error_code) => {
301 debug!("{}", error_code);
302 match error_code {
303 ErrorCode::EmailUnverified => return send_email_verification(result.user).await,
304 ErrorCode::PasswordNotSet => return send_reset_password(result.user).await,
305 _ => {
306 let error = anyhow!("Shouldn't be here.");
307 return Err(error);
308 }
309 }
310 }
311 None => {
312 let error = anyhow!("Shouldn't be here.");
313 return Err(error);
314 }
315 }
316 }
317
318 async fn send_reset_password(user: Option<User>) -> Result<CommandResult> {
319 // Return early if user is null
320 if let Some(user) = user {
321 let confirm = Confirm::with_theme(&ColorfulTheme::default())
322 .with_prompt("Do you want to reset your password?")
323 .interact()
324 .unwrap();
325
326 // Send verification email if user confirms
327 if !confirm {
328 let spinner = Spinner::new(
329 spinners::Spinners::SimpleDotsScrolling,
330 style("Cancel operation.").green().bold().to_string(),
331 );
332 return Ok(CommandResult {
333 spinner,
334 symbol: "✅".to_owned(),
335 msg: "Doing nothing.".to_owned(),
336 });
337 }
338 resend_reset_password_instruction(user).await
339 } else {
340 let error = anyhow!("Failed to get user.");
341 Err(error)
342 }
343 }
344
345 async fn resend_reset_password_instruction(user: User) -> Result<CommandResult> {
346 let mut spinner = Spinner::new(
347 spinners::Spinners::SimpleDotsScrolling,
348 style("Sending reset password instruction...")
349 .green()
350 .bold()
351 .to_string(),
352 );
353 let response = Client::new()
354 .post(build_smb_resend_reset_password_instructions_url())
355 .body(format!("id={}", user.id))
356 .header("Accept", "application/json")
357 .header("Content-Type", "application/x-www-form-urlencoded")
358 .send()
359 .await?;
360
361 match response.status() {
362 StatusCode::OK => {
363 spinner.stop_and_persist(
364 "✅",
365 "Reset password instruction sent! Please check your email.".to_owned(),
366 );
367 input_reset_password_token().await
368 }
369 _ => {
370 let error = anyhow!("Failed to send reset password instruction.");
371 Err(error)
372 }
373 }
374 }
375
376 async fn input_reset_password_token() -> Result<CommandResult> {
377 let token = Input::<String>::with_theme(&ColorfulTheme::default())
378 .with_prompt("Input reset password token")
379 .interact()
380 .unwrap();
381 let password = Password::with_theme(&ColorfulTheme::default())
382 .with_prompt("New password.")
383 .with_confirmation("Repeat password.", "Error: the passwords don't match.")
384 .interact()
385 .unwrap();
386
387 let spinner = Spinner::new(
388 spinners::Spinners::SimpleDotsScrolling,
389 style("Resetting password...").green().bold().to_string(),
390 );
391
392 let password_confirmation = password.clone();
393
394 let params = Param {
395 user: UserUpdatePassword {
396 reset_password_token: token,
397 password,
398 password_confirmation,
399 },
400 };
401
402 let response = Client::new()
403 .put(build_smb_reset_password_url())
404 .json(&params)
405 .header("Accept", "application/json")
406 .header("Content-Type", "application/x-www-form-urlencoded")
407 .send()
408 .await?;
409
410 match response.status() {
411 StatusCode::OK => Ok(CommandResult {
412 spinner,
413 symbol: "✅".to_owned(),
414 msg: "Password reset!".to_owned(),
415 }),
416 _ => {
417 let error = anyhow!("Failed to reset password.");
418 Err(error)
419 }
420 }
421 }
422
423 fn build_smb_login_url() -> String {
424 let mut url_builder = smb_base_url_builder();
425 url_builder.add_route("v1/users/sign_in");
426 url_builder.build()
427 }
428
429 fn build_smb_resend_email_verification_url() -> String {
430 let mut url_builder = smb_base_url_builder();
431 url_builder.add_route("v1/resend_confirmation");
432 url_builder.build()
433 }
434
435 fn build_smb_resend_reset_password_instructions_url() -> String {
436 let mut url_builder = smb_base_url_builder();
437 url_builder.add_route("v1/resend_reset_password_instructions");
438 url_builder.build()
439 }
440
441 fn build_smb_reset_password_url() -> String {
442 let mut url_builder = smb_base_url_builder();
443 url_builder.add_route("v1/users/password");
444 url_builder.build()
445 }
446
447 pub async fn process_logout() -> Result<CommandResult> {
448 let spinner = Spinner::new(
449 spinners::Spinners::SimpleDotsScrolling,
450 style("Logging you out...").green().bold().to_string(),
451 );
452 match home::home_dir() {
453 Some(path) => {
454 debug!("{}", path.to_str().unwrap());
455 fs::remove_file([path.to_str().unwrap(), "/.smb/token"].join(""))?;
456
457 Ok(CommandResult {
458 spinner,
459 symbol: "✅".to_owned(),
460 msg: "You are logged out!".to_owned(),
461 })
462 }
463 None => {
464 let error = anyhow!("Failed to get home directory. Are you logged in?");
465 Err(error)
466 }
467 }
468 }