1use crate::{
2 app::{
3 app_helper::handle_go_to_previous_view, kanban::Boards, state::UserLoginData, App,
4 AppConfig,
5 },
6 constants::{
7 CONFIG_DIR_NAME, CONFIG_FILE_NAME, EMAIL_REGEX, ENCRYPTION_KEY_FILE_NAME,
8 MAX_PASSWORD_LENGTH, MIN_PASSWORD_LENGTH, MIN_TIME_BETWEEN_SENDING_RESET_LINK,
9 REFRESH_TOKEN_FILE_NAME, REFRESH_TOKEN_SEPARATOR, SAVE_DIR_NAME, SUPABASE_ANON_KEY,
10 SUPABASE_URL,
11 },
12 io::{
13 data_handler::{
14 get_available_local_save_files, get_default_save_directory, get_local_kanban_state,
15 get_saved_themes, save_kanban_state_locally,
16 },
17 IoEvent,
18 },
19 ui::{TextColorOptions, View},
20 util::{print_debug, print_error, print_info},
21};
22use aes_gcm::{
23 aead::{generic_array::GenericArray, Aead, OsRng},
24 AeadCore, Aes256Gcm, Key, KeyInit,
25};
26use base64::Engine;
27use chrono::{NaiveDate, NaiveDateTime};
28use eyre::{anyhow, Result};
29use linked_hash_map::LinkedHashMap;
30use log::{debug, error, info, warn};
31use ratatui::widgets::ListState;
32use reqwest::StatusCode;
33use serde::{Deserialize, Serialize};
34use serde_json::json;
35use std::{
36 env,
37 path::{Path, PathBuf},
38 sync::Arc,
39 time::{Duration, Instant},
40};
41
42pub struct IoAsyncHandler<'a> {
43 app: Arc<tokio::sync::Mutex<App<'a>>>,
44}
45
46impl IoAsyncHandler<'_> {
47 pub fn new(app: Arc<tokio::sync::Mutex<App>>) -> IoAsyncHandler {
48 IoAsyncHandler { app }
49 }
50
51 pub async fn handle_io_event(&mut self, io_event: IoEvent) {
52 let result = match io_event {
53 IoEvent::Initialize => self.do_initialize().await,
54 IoEvent::SaveLocalData => self.save_local_data().await,
55 IoEvent::LoadSaveLocal => self.load_save_file_local().await,
56 IoEvent::DeleteLocalSave => self.delete_local_save_file().await,
57 IoEvent::ResetVisibleBoardsandCards => self.refresh_visible_boards_and_cards().await,
58 IoEvent::AutoSave => self.auto_save().await,
59 IoEvent::LoadLocalPreview => self.load_local_preview().await,
60 IoEvent::Login(email_id, password) => self.cloud_login(email_id, password).await,
61 IoEvent::Logout => self.cloud_logout().await,
62 IoEvent::SignUp(email_id, password, confirm_password) => {
63 self.cloud_signup(email_id, password, confirm_password)
64 .await
65 }
66 IoEvent::SendResetPasswordEmail(email_id) => {
67 self.send_reset_password_email(email_id).await
68 }
69 IoEvent::ResetPassword(reset_link, new_password, confirm_password) => {
70 self.reset_password(reset_link, new_password, confirm_password)
71 .await
72 }
73 IoEvent::SyncLocalData => self.sync_local_data().await,
74 IoEvent::GetCloudData => self.get_cloud_data().await,
75 IoEvent::LoadSaveCloud => self.load_save_file_cloud().await,
76 IoEvent::LoadCloudPreview => self.preview_cloud_save().await,
77 IoEvent::DeleteCloudSave => self.delete_cloud_save().await,
78 };
79
80 let mut app = self.app.lock().await;
81 if let Err(err) = result {
82 error!("Oops, something wrong happened 😢: {:?}", err);
83 app.send_error_toast("Oops, something wrong happened 😢", None);
84 }
85
86 app.loaded();
87 }
88
89 async fn do_initialize(&mut self) -> Result<()> {
90 info!("🚀 Initialize the application");
91 let mut app = self.app.lock().await;
92 let default_ui_view = app.config.default_view;
93 let prepare_config_dir_status = prepare_config_dir();
94 if prepare_config_dir_status.is_err() {
95 error!("Cannot create config directory");
96 app.send_error_toast("Cannot create config directory", None);
97 }
98 if !prepare_save_dir() {
99 error!("Cannot create save directory");
100 app.send_error_toast("Cannot create save directory", None);
101 }
102 prepare_boards(&mut app);
103 app.dispatch(IoEvent::ResetVisibleBoardsandCards).await;
104 let saved_themes = get_saved_themes();
105 if let Some(saved_themes) = saved_themes {
106 app.all_themes.extend(saved_themes);
107 }
108 let default_theme = app.config.default_theme.clone();
109 for theme in &app.all_themes {
110 if theme.name == default_theme {
111 app.current_theme = theme.clone();
112 break;
113 }
114 }
115 if let Some(bg_color) = app.current_theme.general_style.bg {
116 app.state.term_background_color = TextColorOptions::from(bg_color).to_rgb();
117 } else {
118 app.state.term_background_color = (0, 0, 0)
119 }
120 app.set_view(default_ui_view);
121 info!("👍 Application initialized");
122 app.initialized();
123 if app.config.save_directory == get_default_save_directory() {
124 app.send_warning_toast(
125 "Save directory is set to a temporary directory,
126 your operating system may delete it at any time. Please change it in the settings.",
127 Some(Duration::from_secs(10)),
128 );
129 }
130 app.send_info_toast("Application initialized", None);
131 if app.config.auto_login {
132 app.send_info_toast("Attempting to auto login", None);
133 let user_login_data =
134 test_refresh_token_on_disk(app.state.encryption_key_from_arguments.clone()).await;
135 if user_login_data.is_err() {
136 let refresh_token_file_path = get_config_dir();
137 if refresh_token_file_path.is_err() {
138 error!("Cannot get config directory");
139 app.send_error_toast("Cannot get config directory", None);
140 return Ok(());
141 }
142 let mut refresh_token_file_path = refresh_token_file_path.unwrap();
143 refresh_token_file_path.push(REFRESH_TOKEN_FILE_NAME);
144 if refresh_token_file_path.exists() {
145 if let Err(err) = std::fs::remove_file(refresh_token_file_path) {
146 error!("Cannot delete refresh token file: {:?}", err);
147 app.send_error_toast("Cannot delete refresh token file", None);
148 return Ok(());
149 } else {
150 warn!("Previous access token has expired or does not exist. Please login again");
151 app.send_warning_toast("Previous access token has expired or does not exist. Please login again", None)
152 }
153 } else {
154 warn!(
155 "Previous access token has expired or does not exist. Please login again"
156 );
157 app.send_warning_toast(
158 "Previous access token has expired or does not exist. Please login again",
159 None,
160 )
161 }
162 } else {
163 let user_login_data = user_login_data.unwrap();
164 app.state.user_login_data = user_login_data;
165 app.main_menu.logged_in = true;
166 app.send_info_toast("👍 Auto login successful", None);
167 }
168 }
169 Ok(())
170 }
171
172 async fn save_local_data(&mut self) -> Result<()> {
173 info!("🚀 Saving local data");
174 let mut app = self.app.lock().await;
175 if save_required(&mut app) {
176 let board_data = app.boards.get_boards();
177 let status = save_kanban_state_locally(board_data.to_vec(), &app.config);
178 match status {
179 Ok(_) => {
180 info!("👍 Local data saved");
181 app.send_info_toast("👍 Local data saved", None);
182 }
183 Err(err) => {
184 debug!("Cannot save local data: {:?}", err);
185 app.send_error_toast("Cannot save local data", None);
186 }
187 }
188 Ok(())
189 } else {
190 warn!("No changes to save");
191 app.send_warning_toast("No changes to save", None);
192 Ok(())
193 }
194 }
195
196 async fn load_save_file_local(&mut self) -> Result<()> {
197 let mut app = self.app.lock().await;
198 let default_view = app.config.default_view;
199 let save_file_index = app.state.app_list_states.load_save.selected().unwrap_or(0);
200 let local_files = get_available_local_save_files(&app.config);
201 let local_files = if let Some(local_files) = local_files {
202 local_files
203 } else {
204 error!("Could not get local save files");
205 app.send_error_toast("Could not get local save files", None);
206 vec![]
207 };
208 if save_file_index >= local_files.len() {
209 error!("Cannot load save file: No such file");
210 app.send_error_toast("Cannot load save file: No such file", None);
211 return Ok(());
212 }
213 let save_file_name = local_files[save_file_index].clone();
214 info!("🚀 Loading save file: {}", save_file_name);
215 let board_data = get_local_kanban_state(save_file_name.clone(), false, &app.config);
216 match board_data {
217 Ok(boards) => {
218 app.boards.set_boards(boards);
219 app.action_history_manager.reset();
220 info!("👍 Save file {:?} loaded", save_file_name);
221 app.send_info_toast(&format!("👍 Save file {:?} loaded", save_file_name), None);
222 }
223 Err(err) => {
224 debug!("Cannot load save file: {:?}", err);
225 app.send_error_toast("Cannot load save file", None);
226 }
227 }
228 app.dispatch(IoEvent::ResetVisibleBoardsandCards).await;
229 app.set_view(default_view);
230 Ok(())
231 }
232
233 async fn delete_local_save_file(&mut self) -> Result<()> {
234 let mut app = self.app.lock().await;
235 let file_list = get_available_local_save_files(&app.config);
236 let file_list = if let Some(file_list) = file_list {
237 file_list
238 } else {
239 error!("Cannot delete save file: no save files found");
240 app.send_error_toast("Cannot delete save file: no save files found", None);
241 return Ok(());
242 };
243 if app.state.app_list_states.load_save.selected().is_none() {
244 error!("Cannot delete save file: no save file selected");
245 app.send_error_toast("Cannot delete save file: no save file selected", None);
246 return Ok(());
247 }
248 let selected = app.state.app_list_states.load_save.selected().unwrap_or(0);
249 if selected >= file_list.len() {
250 debug!("Cannot delete save file: index out of range");
251 app.send_error_toast("Cannot delete save file: Something went wrong", None);
252 return Ok(());
253 }
254 let file_name = file_list[selected].clone();
255 info!("🚀 Deleting save file: {}", file_name);
256 let path = app.config.save_directory.join(file_name);
257 if !Path::new(&path).exists() {
258 error!("Cannot delete save file: file not found");
259 app.send_error_toast("Cannot delete save file: file not found", None);
260 return Ok(());
261 } else if let Err(err) = std::fs::remove_file(&path) {
262 debug!("Cannot delete save file: {:?}", err);
263 app.send_error_toast("Cannot delete save file: Something went wrong", None);
264 app.state.app_list_states.load_save = ListState::default();
265 return Ok(());
266 } else {
267 info!("👍 Save file deleted");
268 app.send_info_toast("👍 Save file deleted", None);
269 }
270 let file_list = get_available_local_save_files(&app.config);
271 let file_list = if let Some(file_list) = file_list {
272 file_list
273 } else {
274 app.state.app_list_states.load_save = ListState::default();
275 return Ok(());
276 };
277 if selected >= file_list.len() {
278 if file_list.is_empty() {
279 app.state.app_list_states.load_save = ListState::default();
280 } else {
281 app.state
282 .app_list_states
283 .load_save
284 .select(Some(file_list.len() - 1));
285 }
286 }
287 Ok(())
288 }
289
290 async fn refresh_visible_boards_and_cards(&mut self) -> Result<()> {
291 let mut app = self.app.lock().await;
292 refresh_visible_boards_and_cards(&mut app);
293 Ok(())
294 }
295
296 async fn auto_save(&mut self) -> Result<()> {
297 let mut app = self.app.lock().await;
298 match auto_save(&mut app).await {
299 Ok(_) => Ok(()),
300 Err(err) => Err(anyhow!(err)),
301 }
302 }
303
304 async fn load_local_preview(&mut self) -> Result<()> {
305 let mut app = self.app.lock().await;
306 if app.state.app_list_states.load_save.selected().is_none() {
307 return Ok(());
308 }
309 app.preview_boards_and_cards = None;
310
311 let save_file_index = app.state.app_list_states.load_save.selected().unwrap_or(0);
312 let local_files = get_available_local_save_files(&app.config);
313 let local_files = if let Some(local_files) = local_files {
314 local_files
315 } else {
316 error!("Could not get local save files");
317 app.send_error_toast("Could not get local save files", None);
318 vec![]
319 };
320 if save_file_index >= local_files.len() {
321 error!("Cannot load preview: No such file");
322 app.send_error_toast("Cannot load preview: No such file", None);
323 return Ok(());
324 }
325 let save_file_name = local_files[save_file_index].clone();
326 let board_data = get_local_kanban_state(save_file_name.clone(), true, &app.config);
327 match board_data {
328 Ok(boards) => {
329 app.preview_boards_and_cards = Some(boards);
330 let mut visible_boards_and_cards: LinkedHashMap<(u64, u64), Vec<(u64, u64)>> =
331 LinkedHashMap::new();
332 for (counter, board) in app
333 .preview_boards_and_cards
334 .as_ref()
335 .unwrap()
336 .get_boards()
337 .iter()
338 .enumerate()
339 {
340 if counter >= app.config.no_of_boards_to_show.into() {
341 break;
342 }
343 let mut visible_cards: Vec<(u64, u64)> = Vec::new();
344 if board.cards.len() > app.config.no_of_cards_to_show.into() {
345 for card in board
346 .cards
347 .get_all_cards()
348 .iter()
349 .take(app.config.no_of_cards_to_show.into())
350 {
351 visible_cards.push(card.id);
352 }
353 } else {
354 for card in board.cards.get_all_cards() {
355 visible_cards.push(card.id);
356 }
357 }
358
359 let mut visible_board: LinkedHashMap<(u64, u64), Vec<(u64, u64)>> =
360 LinkedHashMap::new();
361 visible_board.insert(board.id, visible_cards);
362 visible_boards_and_cards.extend(visible_board);
363 }
364 app.state.preview_visible_boards_and_cards = visible_boards_and_cards;
365 app.state.preview_file_name = Some(save_file_name);
366 }
367 Err(e) => {
368 error!("Error loading preview: {}", e);
369 app.send_error_toast("Error loading preview", None);
370 }
371 }
372 Ok(())
373 }
374
375 async fn cloud_login(&mut self, email_id: String, password: String) -> Result<()> {
376 {
377 let mut app = self.app.lock().await;
378 if app.state.user_login_data.auth_token.is_some() {
379 error!("Already logged in, Please logout first");
380 app.send_error_toast("Already logged in, Please logout first", None);
381 return Ok(());
382 } else {
383 info!("Logging in, please wait...");
384 app.send_info_toast("Logging in, please wait...", None);
385 }
386 if email_id.is_empty() {
387 error!("Email cannot be empty");
388 app.send_error_toast("Email cannot be empty", None);
389 return Ok(());
390 } else if password.is_empty() {
391 error!("Password cannot be empty");
392 app.send_error_toast("Password cannot be empty", None);
393 return Ok(());
394 }
395 }
396
397 let login_for_user_status = login_for_user(&email_id, &password, false).await;
398 if let Err(err) = login_for_user_status {
399 debug!("Error logging in: {:?}", err);
400 let mut app = self.app.lock().await;
401 if err == "Error logging in: \"Invalid login credentials\"" {
402 error!("Invalid login credentials");
403 app.send_error_toast("Invalid login credentials", None);
404 } else {
405 error!("Error logging in");
406 app.send_error_toast("Error logging in", None);
407 }
408 return Ok(());
409 }
410 let (access_token, user_id, refresh_token) = login_for_user_status.unwrap();
411 let mut app = self.app.lock().await;
412 app.state.user_login_data.auth_token = Some(access_token.to_string());
413 app.state.user_login_data.refresh_token = Some(refresh_token.to_string());
414 app.state.user_login_data.email_id = Some(email_id.to_string());
415 app.state.user_login_data.user_id = Some(user_id.to_string());
416 app.main_menu.logged_in = true;
417
418 if app.config.auto_login {
419 save_refresh_token_to_disk(
420 &refresh_token,
421 &email_id,
422 app.state.encryption_key_from_arguments.clone(),
423 )
424 .await?;
425 }
426
427 if app.state.current_view == View::Login {
428 handle_go_to_previous_view(&mut app).await;
429 }
430
431 info!("👍 Logged in");
432 app.send_info_toast("👍 Logged in", None);
433
434 Ok(())
435 }
436
437 async fn cloud_logout(&mut self) -> Result<()> {
438 {
439 let mut app = self.app.lock().await;
440 if app.state.user_login_data.auth_token.is_none() {
441 error!("Not logged in");
442 app.send_error_toast("Not logged in", None);
443 return Ok(());
444 } else {
445 info!("Logging out, please wait...");
446 app.send_info_toast("Logging out, please wait...", None);
447 }
448 }
449 let client = reqwest::Client::new();
450 let response = client
451 .post(format!("{}/auth/v1/logout", SUPABASE_URL))
452 .header("apikey", SUPABASE_ANON_KEY)
453 .header("Content-Type", "application/json")
454 .header(
455 "Authorization",
456 format!(
457 "Bearer {}",
458 self.app
459 .lock()
460 .await
461 .state
462 .user_login_data
463 .auth_token
464 .as_ref()
465 .unwrap()
466 ),
467 )
468 .send()
469 .await?;
470
471 let status = response.status();
472 if status == StatusCode::NO_CONTENT {
473 let mut app = self.app.lock().await;
474 app.state.user_login_data = UserLoginData::default();
475 app.main_menu.logged_in = false;
476 info!("👍 Logged out");
477 app.send_info_toast("👍 Logged out", None);
478 } else {
479 error!("Error logging out");
480 let mut app = self.app.lock().await;
481 app.send_error_toast("Error logging out", None);
482 }
483 delete_refresh_token_from_disk().await?;
484 Ok(())
485 }
486
487 async fn cloud_signup(
488 &mut self,
489 email_id: String,
490 password: String,
491 confirm_password: String,
492 ) -> Result<()> {
493 {
494 let mut app = self.app.lock().await;
495 if app.state.user_login_data.auth_token.is_some() {
496 error!("Already logged in, Please logout first");
497 app.send_error_toast("Already logged in, Please logout first", None);
498 return Ok(());
499 }
500 if email_id.is_empty() {
501 error!("Email cannot be empty");
502 app.send_error_toast("Email cannot be empty", None);
503 return Ok(());
504 }
505 if password.is_empty() || confirm_password.is_empty() {
506 error!("Password cannot be empty");
507 app.send_error_toast("Password cannot be empty", None);
508 return Ok(());
509 }
510 if password != confirm_password {
511 error!("Passwords do not match");
512 app.send_error_toast("Passwords do not match", None);
513 return Ok(());
514 }
515 let email_regex =
516 regex::Regex::new(EMAIL_REGEX).expect("Invalid email regex in constants");
517 if !email_regex.is_match(&email_id) {
518 error!("Invalid email format");
519 app.send_error_toast("Invalid email format", None);
520 return Ok(());
521 }
522
523 let password_status = check_for_safe_password(&password);
524 match password_status {
525 PasswordStatus::Strong => {}
526 PasswordStatus::MissingLowercase => {
527 error!("Password must contain at least one lowercase character");
528 app.send_error_toast(
529 "Password must contain at least one lowercase character",
530 None,
531 );
532 return Ok(());
533 }
534 PasswordStatus::MissingUppercase => {
535 error!("Password must contain at least one uppercase character");
536 app.send_error_toast(
537 "Password must contain at least one uppercase character",
538 None,
539 );
540 return Ok(());
541 }
542 PasswordStatus::MissingNumber => {
543 error!("Password must contain at least one number");
544 app.send_error_toast("Password must contain at least one number", None);
545 return Ok(());
546 }
547 PasswordStatus::MissingSpecialChar => {
548 error!("Password must contain at least one special character");
549 app.send_error_toast(
550 "Password must contain at least one special character",
551 None,
552 );
553 return Ok(());
554 }
555 PasswordStatus::TooShort => {
556 error!(
557 "Password must be at least {} characters long",
558 MIN_PASSWORD_LENGTH
559 );
560 app.send_error_toast(
561 &format!(
562 "Password must be at least {} characters long",
563 MIN_PASSWORD_LENGTH
564 ),
565 None,
566 );
567 return Ok(());
568 }
569 PasswordStatus::TooLong => {
570 error!(
571 "Password must be at most {} characters long",
572 MAX_PASSWORD_LENGTH
573 );
574 app.send_error_toast(
575 &format!(
576 "Password must be at most {} characters long",
577 MAX_PASSWORD_LENGTH
578 ),
579 None,
580 );
581 }
582 }
583
584 if get_user_encryption_key(app.state.encryption_key_from_arguments.clone()).is_ok() {
586 warn!("Encryption key already exists, please delete it first or move it if you are trying to create a second account");
587 app.send_warning_toast("Encryption key already exists, please delete it first or move it if you are trying to create a second account", Some(Duration::from_secs(10)));
588 return Ok(());
589 }
590
591 info!("Signing up, please wait...");
592 app.send_info_toast("Signing up, please wait...", None);
593 }
594
595 let request_body = json!(
596 {
597 "email": email_id,
598 "password": password
599 }
600 );
601 let client = reqwest::Client::new();
602 let response = client
603 .post(format!("{}/auth/v1/signup", SUPABASE_URL))
604 .header("apikey", SUPABASE_ANON_KEY)
605 .header("Content-Type", "application/json")
606 .body(request_body.to_string())
607 .send()
608 .await?;
609 let status = response.status();
610 let body = response.json::<serde_json::Value>().await;
611 if status == StatusCode::OK {
612 match body {
613 Ok(body) => {
614 let confirmation_sent = body.get("confirmation_sent_at");
615 match confirmation_sent {
616 Some(confirmation_sent) => {
617 let confirmation_sent = confirmation_sent.as_str();
618 if confirmation_sent.is_none() {
619 error!("Error signing up");
620 let mut app = self.app.lock().await;
621 app.send_error_toast("Error signing up", None);
622 return Ok(());
623 }
624 info!("👍 Confirmation email sent");
625 let mut app = self.app.lock().await;
626 app.send_info_toast(
627 "👍 Confirmation email sent",
628 Some(Duration::from_secs(10)),
629 );
630 let key = generate_new_encryption_key();
631 let save_result = save_user_encryption_key(&key);
632 if save_result.is_err() {
633 error!("Error saving encryption key");
634 debug!("Error saving encryption key: {:?}", save_result);
635 app.send_error_toast("Error saving encryption key", None);
636 return Ok(());
637 } else {
638 let save_path = save_result.unwrap();
639 info!("👍 Encryption key saved at {}", save_path);
640 app.send_info_toast(
641 &format!("👍 Encryption key saved at {}", save_path),
642 Some(Duration::from_secs(10)),
643 );
644 warn!("Please keep this key safe, you will need it to decrypt your data, you will not be able to recover your data without it");
645 app.send_warning_toast(
646 "Please keep this key safe, you will need it to decrypt your data, you will not be able to recover your data without it",
647 Some(Duration::from_secs(10)),
648 );
649 let default_view = app.config.default_view;
650 app.set_view(default_view);
651 }
652 }
653 None => {
654 error!("Error signing up");
655 let mut app = self.app.lock().await;
656 app.send_error_toast("Error signing up", None);
657 }
658 }
659 }
660 Err(e) => {
661 error!("Error signing up: {}", e);
662 let mut app = self.app.lock().await;
663 app.send_error_toast("Error signing up", None);
664 }
665 }
666 } else if status == StatusCode::TOO_MANY_REQUESTS {
667 error!("Too many requests, please try again later. Due to the free nature of supabase i am limited to only 4 signup requests per hour. Sorry! 😢");
668 debug!("status code {}, response body: {:?}", status, body);
669 let mut app = self.app.lock().await;
670 app.send_error_toast("Too many requests, please try again later. Due to the free nature of supabase i am limited to only 4 signup requests per hour. Sorry! 😢", None);
671 } else {
672 error!("Error signing up");
673 debug!("status code {}, response body: {:?}", status, body);
674 let mut app = self.app.lock().await;
675 app.send_error_toast("Error signing up", None);
676 }
677 Ok(())
678 }
679
680 async fn send_reset_password_email(&mut self, email_id: String) -> Result<()> {
681 {
682 let mut app = self.app.lock().await;
683 if let Some(reset_time) = app.state.last_reset_password_link_sent_time {
684 if reset_time.elapsed() < Duration::from_secs(MIN_TIME_BETWEEN_SENDING_RESET_LINK) {
685 let remaining_time = Duration::from_secs(MIN_TIME_BETWEEN_SENDING_RESET_LINK)
686 .checked_sub(reset_time.elapsed())
687 .unwrap();
688 error!(
689 "Please wait for {} seconds before sending another reset password email",
690 remaining_time.as_secs()
691 );
692 app.send_error_toast(
693 &format!(
694 "Please wait for {} seconds before sending another reset password email",
695 remaining_time.as_secs()
696 ),
697 None,
698 );
699 return Ok(());
700 }
701 }
702 if email_id.is_empty() {
703 error!("Email cannot be empty");
704 app.send_error_toast("Email cannot be empty", None);
705 return Ok(());
706 } else {
707 info!("Sending reset password email, please wait...");
708 app.send_info_toast("Sending reset password email, please wait...", None);
709 }
710 }
711
712 let request_body = json!({ "email": email_id });
713
714 let client = reqwest::Client::new();
715 let response = client
716 .post(format!("{}/auth/v1/recover", SUPABASE_URL))
717 .header("apikey", SUPABASE_ANON_KEY)
718 .header("Content-Type", "application/json")
719 .body(request_body.to_string())
720 .send()
721 .await?;
722
723 let status = response.status();
724 if status == StatusCode::OK {
725 info!("👍 Reset password email sent");
726 let mut app = self.app.lock().await;
727 app.state.last_reset_password_link_sent_time = Some(Instant::now());
728 app.send_info_toast("👍 Reset password email sent", None);
729 } else if status == StatusCode::TOO_MANY_REQUESTS {
730 let body = response.json::<serde_json::Value>().await;
731 error!("Too many requests, please try again later. Due to the free nature of supabase i am limited to only 4 signup requests per hour. Sorry! 😢");
732 debug!("status code {}, response body: {:?}", status, body);
733 let mut app = self.app.lock().await;
734 app.send_error_toast("Too many requests, please try again later. Due to the free nature of supabase i am limited to only 4 signup requests per hour. Sorry! 😢", None);
735 } else {
736 error!("Error sending reset password email");
737 let mut app = self.app.lock().await;
738 app.send_error_toast("Error sending reset password email", None);
739 }
740 Ok(())
741 }
742
743 async fn reset_password(
744 &mut self,
745 reset_link: String,
746 new_password: String,
747 confirm_password: String,
748 ) -> Result<()> {
749 {
750 let mut app = self.app.lock().await;
751 if reset_link.is_empty() {
752 error!("Reset link cannot be empty");
753 app.send_error_toast("Reset link cannot be empty", None);
754 return Ok(());
755 }
756 if new_password.is_empty() || confirm_password.is_empty() {
757 error!("Password cannot be empty");
758 app.send_error_toast("Password cannot be empty", None);
759 return Ok(());
760 }
761 if new_password != confirm_password {
762 error!("Passwords do not match");
763 app.send_error_toast("Passwords do not match", None);
764 return Ok(());
765 }
766 let password_status = check_for_safe_password(&new_password);
767 match password_status {
768 PasswordStatus::Strong => {}
769 PasswordStatus::MissingLowercase => {
770 error!("Password must contain at least one lowercase character");
771 app.send_error_toast(
772 "Password must contain at least one lowercase character",
773 None,
774 );
775 return Ok(());
776 }
777 PasswordStatus::MissingUppercase => {
778 error!("Password must contain at least one uppercase character");
779 app.send_error_toast(
780 "Password must contain at least one uppercase character",
781 None,
782 );
783 return Ok(());
784 }
785 PasswordStatus::MissingNumber => {
786 error!("Password must contain at least one number");
787 app.send_error_toast("Password must contain at least one number", None);
788 return Ok(());
789 }
790 PasswordStatus::MissingSpecialChar => {
791 error!("Password must contain at least one special character");
792 app.send_error_toast(
793 "Password must contain at least one special character",
794 None,
795 );
796 return Ok(());
797 }
798 PasswordStatus::TooShort => {
799 error!(
800 "Password must be at least {} characters long",
801 MIN_PASSWORD_LENGTH
802 );
803 app.send_error_toast(
804 &format!(
805 "Password must be at least {} characters long",
806 MIN_PASSWORD_LENGTH
807 ),
808 None,
809 );
810 return Ok(());
811 }
812 PasswordStatus::TooLong => {
813 error!(
814 "Password must be at most {} characters long",
815 MAX_PASSWORD_LENGTH
816 );
817 app.send_error_toast(
818 &format!(
819 "Password must be at most {} characters long",
820 MAX_PASSWORD_LENGTH
821 ),
822 None,
823 );
824 }
825 }
826
827 info!("Resetting password, please wait...");
828 app.send_info_toast("Resetting password, please wait...", None);
829 }
830
831 let client = reqwest::Client::new();
832 let response = client.get(reset_link).send().await;
833 match response {
834 Ok(_) => {
835 error!("Error verifying reset password link");
836 let mut app = self.app.lock().await;
837 app.send_error_toast("Error verifying reset password link", None);
838 }
839 Err(e) => {
840 let mut app = self.app.lock().await;
841 let error_url = e.url();
842 if error_url.is_none() {
843 error!("Error verifying reset password link");
844 app.send_error_toast("Error verifying reset password link", None);
845 return Ok(());
846 }
847 let error_url = error_url.unwrap();
848 let error_url = error_url.to_string();
849 debug!("error url: {}", error_url);
850 let access_token = error_url.split("access_token=");
851 let access_token = access_token.last();
852 if access_token.is_none() {
853 error!("Error verifying reset password link");
854 app.send_error_toast("Error verifying reset password link", None);
855 return Ok(());
856 }
857 let mut access_token = access_token.unwrap().split("&expires_at");
858 let access_token = access_token.next();
859 debug!("access token: {:?}", access_token);
860 if access_token.is_none() {
861 error!("Error verifying reset password link");
862 app.send_error_toast("Error verifying reset password link", None);
863 return Ok(());
864 }
865 drop(app);
866 let access_token = access_token.unwrap();
867 let request_body = json!({ "password": new_password });
868 let reset_client = reqwest::Client::new();
869 let reset_response = reset_client
870 .put(format!("{}/auth/v1/user", SUPABASE_URL))
871 .header("apikey", SUPABASE_ANON_KEY)
872 .header("Content-Type", "application/json")
873 .header("Authorization", format!("Bearer {}", access_token))
874 .body(request_body.to_string())
875 .send()
876 .await?;
877
878 let status = reset_response.status();
879 let mut app = self.app.lock().await;
880 match status {
881 StatusCode::OK => {
882 info!("👍 Password reset successful");
883 if app.state.current_view == View::ResetPassword {
884 handle_go_to_previous_view(&mut app).await;
885 }
886 app.send_info_toast("👍 Password reset successful", None);
887 }
888 StatusCode::UNPROCESSABLE_ENTITY => {
889 error!(
890 "Error resetting password, new password cannot be same as old password"
891 );
892 debug!(
893 "Error resetting password: {:?}",
894 reset_response.text().await
895 );
896 app.send_error_toast(
897 "Error resetting password, new password cannot be same as old password",
898 None,
899 );
900 }
901 _ => {
902 error!("Error resetting password");
903 debug!(
904 "Error resetting password: {:?}",
905 reset_response.text().await
906 );
907 app.send_error_toast("Error resetting password", None);
908 }
909 }
910 }
911 }
912 Ok(())
913 }
914
915 async fn sync_local_data(&mut self) -> Result<()> {
916 {
917 let mut app = self.app.lock().await;
918 if app.state.user_login_data.auth_token.is_none() {
919 error!("Not logged in");
920 app.send_error_toast("Not logged in", None);
921 return Ok(());
922 } else {
923 info!("Syncing local data, please wait...");
924 app.send_info_toast("Syncing local data, please wait...", None);
925 }
926 }
927
928 let save_ids = self.get_save_ids_for_user().await?;
929 let max_save_id = save_ids.iter().max();
930 let max_save_id = if let Some(max_save_id) = max_save_id {
931 max_save_id + 1
932 } else {
933 0
934 };
935
936 let mut app = self.app.lock().await;
937 let key = get_user_encryption_key(app.state.encryption_key_from_arguments.clone());
938 if key.is_err() {
939 error!("Error syncing local data, Could not get encryption key, If you have lost it please generate a new one using the -g flag");
940 debug!(
941 "Error syncing local data: {:?}, Could not get encryption key",
942 key.err()
943 );
944 app.send_error_toast("Error syncing local data, Could not get encryption key, If you have lost it please generate a new one using the -g flag", None);
945 return Ok(());
946 }
947 let key = key.unwrap();
948 let encrypt_result = encrypt_save(&app.boards, &key);
949 if encrypt_result.is_err() {
950 error!("Error syncing local data");
951 debug!(
952 "Error syncing local data: {:?}, could not encrypt",
953 encrypt_result.err()
954 );
955 app.send_error_toast("Error syncing local data", None);
956 return Ok(());
957 }
958 let (encrypted_board_data, nonce) = encrypt_result.unwrap();
959 let auth_token = app.state.user_login_data.auth_token.clone().unwrap();
960 let user_id = app.state.user_login_data.user_id.clone().unwrap();
961 drop(app);
962 let client = reqwest::Client::new();
963 let response = client
964 .post(format!("{}/rest/v1/user_data", SUPABASE_URL))
965 .header("apikey", SUPABASE_ANON_KEY)
966 .header("Content-Type", "application/json")
967 .header("Authorization", format!("Bearer {}", auth_token))
968 .body(
969 json!(
970 {
971 "user_id": user_id,
972 "board_data": encrypted_board_data,
973 "save_id": max_save_id,
974 "nonce": nonce
975 }
976 )
977 .to_string(),
978 )
979 .send()
980 .await?;
981
982 let status = response.status();
983 let mut app = self.app.lock().await;
984 if status == StatusCode::CREATED {
985 info!("👍 Local data synced to the cloud");
986 app.send_info_toast("👍 Local data synced to the cloud", None);
987 if app.state.cloud_data.is_some() {
988 app.dispatch(IoEvent::GetCloudData).await;
989 }
990 } else {
991 error!("Error syncing local data");
992 debug!("Error syncing local data: {:?}", response.text().await);
993 app.send_error_toast("Error syncing local data", None);
994 }
995 Ok(())
996 }
997
998 async fn get_save_ids_for_user(&mut self) -> Result<Vec<usize>> {
999 {
1000 let mut app = self.app.lock().await;
1001 if app.state.user_login_data.auth_token.is_none() {
1002 error!("Not logged in");
1003 app.send_error_toast("Not logged in", None);
1004 return Ok(vec![]);
1005 }
1006 }
1007
1008 let (user_id, access_token) = {
1009 let app = self.app.lock().await;
1010 let user_id = app.state.user_login_data.user_id.as_ref().unwrap().clone();
1011 let access_token = app
1012 .state
1013 .user_login_data
1014 .auth_token
1015 .as_ref()
1016 .unwrap()
1017 .clone();
1018 (user_id, access_token)
1019 };
1020
1021 let result = get_all_save_ids_for_user(user_id, &access_token).await;
1022 if result.is_err() {
1023 let error_string = format!("{:?}", result.err());
1024 error!("{}", error_string);
1025 let mut app = self.app.lock().await;
1026 app.send_error_toast(&error_string, None);
1027 Ok(vec![])
1028 } else {
1029 let save_ids = result.unwrap();
1030 Ok(save_ids)
1031 }
1032 }
1033
1034 async fn get_cloud_data(&mut self) -> Result<()> {
1035 {
1036 let mut app = self.app.lock().await;
1037 if app.state.user_login_data.auth_token.is_none() {
1038 error!("Not logged in");
1039 app.send_error_toast("Not logged in", None);
1040 return Ok(());
1041 } else {
1042 info!("Refreshing cloud data, please wait...");
1043 app.send_info_toast("Refreshing cloud data, please wait...", None);
1044 app.state.cloud_data = None;
1045 }
1046 }
1047
1048 let app = self.app.lock().await;
1049 let auth_token = app.state.user_login_data.auth_token.clone().unwrap();
1050 drop(app);
1051 let client = reqwest::Client::new();
1052 let response = client
1053 .get(format!("{}/rest/v1/user_data", SUPABASE_URL))
1054 .header("apikey", SUPABASE_ANON_KEY)
1055 .header("Content-Type", "application/json")
1056 .header("Authorization", format!("Bearer {}", auth_token))
1057 .send()
1058 .await?;
1059
1060 let mut app = self.app.lock().await;
1061
1062 let status = response.status();
1063 if status == StatusCode::OK {
1064 let body = response.json::<Vec<CloudData>>().await;
1065 match body {
1066 Ok(cloud_data) => {
1067 app.state.cloud_data = Some(cloud_data);
1068 info!("👍 Cloud data loaded");
1069 app.send_info_toast("👍 Cloud data loaded", None);
1070 }
1071 Err(e) => {
1072 error!("Error Refreshing cloud data: {}", e);
1073 app.send_error_toast("Error Refreshing cloud data", None);
1074 }
1075 }
1076 } else {
1077 error!("Error Refreshing cloud data");
1078 app.send_error_toast("Error Refreshing cloud data", None);
1079 }
1080 Ok(())
1081 }
1082
1083 async fn preview_cloud_save(&mut self) -> Result<()> {
1084 {
1085 let mut app = self.app.lock().await;
1086 if app.state.app_list_states.load_save.selected().is_none() {
1087 error!("No save selected to preview");
1088 app.send_error_toast("No save selected to preview", None);
1089 return Ok(());
1090 }
1091 }
1092
1093 let mut app = self.app.lock().await;
1094 let selected_index = app.state.app_list_states.load_save.selected().unwrap();
1095 let cloud_data = app.state.cloud_data.clone();
1096 if cloud_data.is_none() {
1097 debug!("No cloud data preview found to select");
1098 return Ok(());
1099 }
1100 let cloud_data = cloud_data.unwrap();
1101 if selected_index >= cloud_data.len() {
1102 debug!("Selected index is out of bounds");
1103 return Ok(());
1104 }
1105 let save = cloud_data[selected_index].clone();
1106 let key = get_user_encryption_key(app.state.encryption_key_from_arguments.clone());
1107 if key.is_err() {
1108 error!("Error loading save file, Could not get user Encryption key .If lost please generate a new one by using the -g flag");
1109 debug!("Error loading save file: {:?}", key.err());
1110 app.send_error_toast(
1111 "Error loading save file, Could not get user Encryption key .If lost please generate a new one by using the -g flag",
1112 None,
1113 );
1114 return Ok(());
1115 }
1116 let key = key.unwrap();
1117 let decrypt_result = decrypt_save(save.board_data, key.as_slice(), &save.nonce);
1118 if decrypt_result.is_err() {
1119 error!("Error loading save file, Could not decrypt save file. The save file must have been created with a different encryption key, either generate a new one with the -g flag or replace the current encryption key with the one used to create the save file");
1120 debug!("Error loading save file: {:?}", decrypt_result.err());
1121 app.send_error_toast("Error loading save file, Could not decrypt save file. The save file must have been created with a different encryption key, either generate a new one with the -g flag or replace the current encryption key with the one used to create the save file", Some(Duration::from_secs(5)));
1122 return Ok(());
1123 }
1124 app.preview_boards_and_cards = Some(decrypt_result.unwrap());
1125 let mut visible_boards_and_cards: LinkedHashMap<(u64, u64), Vec<(u64, u64)>> =
1126 LinkedHashMap::new();
1127 for (counter, board) in app
1128 .preview_boards_and_cards
1129 .as_ref()
1130 .unwrap()
1131 .get_boards()
1132 .iter()
1133 .enumerate()
1134 {
1135 if counter >= app.config.no_of_boards_to_show.into() {
1136 break;
1137 }
1138 let mut visible_cards: Vec<(u64, u64)> = Vec::new();
1139 if board.cards.len() > app.config.no_of_cards_to_show.into() {
1140 for card in board
1141 .cards
1142 .get_all_cards()
1143 .iter()
1144 .take(app.config.no_of_cards_to_show.into())
1145 {
1146 visible_cards.push(card.id);
1147 }
1148 } else {
1149 for card in board.cards.get_all_cards() {
1150 visible_cards.push(card.id);
1151 }
1152 }
1153
1154 let mut visible_board: LinkedHashMap<(u64, u64), Vec<(u64, u64)>> =
1155 LinkedHashMap::new();
1156 visible_board.insert(board.id, visible_cards);
1157 visible_boards_and_cards.extend(visible_board);
1158 }
1159 let save_timestamp = save.created_at.split('.').next();
1160 if save_timestamp.is_none() {
1161 debug!("Error splitting {}", save.created_at);
1162 app.state.preview_visible_boards_and_cards = visible_boards_and_cards;
1163 app.state.preview_file_name = Some(format!("Cloud_save_{}", save.save_id,));
1164 return Ok(());
1165 }
1166 let save_timestamp = save_timestamp.unwrap();
1167 let save_date = NaiveDateTime::parse_from_str(save_timestamp, "%Y-%m-%dT%H:%M:%S");
1168 if save_date.is_ok() {
1169 let save_date = save_date.unwrap();
1170 let save_date = save_date.format(app.config.date_time_format.to_parser_string());
1171 app.state.preview_file_name =
1172 Some(format!("Cloud_save_{} - {}", save.save_id, save_date));
1173 } else {
1174 debug!("Error parsing save date {}", save.created_at);
1175 }
1176 app.state.preview_visible_boards_and_cards = visible_boards_and_cards;
1177 Ok(())
1178 }
1179
1180 async fn load_save_file_cloud(&mut self) -> Result<()> {
1181 let mut app = self.app.lock().await;
1182 let default_view = app.config.default_view;
1183 let save_file_index = app.state.app_list_states.load_save.selected().unwrap_or(0);
1184 let cloud_saves = app.state.cloud_data.clone();
1185 let local_files = if let Some(cloud_saves) = cloud_saves {
1186 cloud_saves
1187 } else {
1188 error!("Could not get local save files");
1189 app.send_error_toast("Could not get local save files", None);
1190 vec![]
1191 };
1192 if save_file_index >= local_files.len() {
1193 error!("Cannot load save file: No such file");
1194 app.send_error_toast("Cannot load save file: No such file", None);
1195 return Ok(());
1196 }
1197 let save_file_number = local_files[save_file_index].save_id;
1198 info!("🚀 Loading save file: cloud_save_{}", save_file_number);
1199 let encrypted_board_data = &local_files[save_file_index].board_data;
1200 let key = get_user_encryption_key(app.state.encryption_key_from_arguments.clone());
1201 if key.is_err() {
1202 error!("Error loading save file, Could not get user Encryption key. If lost please generate a new one by using the -g flag");
1203 debug!("Error loading save file: {:?}", key.err());
1204 app.send_error_toast(
1205 "Error loading save file, Could not get user Encryption key. If lost please generate a new one by using the -g flag",
1206 None,
1207 );
1208 return Ok(());
1209 }
1210 let key = key.unwrap();
1211 let decrypt_result = decrypt_save(
1212 encrypted_board_data.to_string(),
1213 key.as_slice(),
1214 &local_files[save_file_index].nonce,
1215 );
1216 if decrypt_result.is_err() {
1217 error!("Error loading save file, Could not decrypt save file. The save file must have been created with a different encryption key, either generate a new one with the -g flag or replace the current encryption key with the one used to create the save file");
1218 debug!("Error loading save file: {:?}", decrypt_result.err());
1219 app.send_error_toast("Error loading save file, Could not decrypt save file. The save file must have been created with a different encryption key, either generate a new one with the -g flag or replace the current encryption key with the one used to create the save file", Some(Duration::from_secs(5)));
1220 return Ok(());
1221 }
1222 let decrypt_result = decrypt_result.unwrap();
1223 app.boards.set_boards(decrypt_result);
1224 info!("👍 Save file cloud_save_{} loaded", save_file_number);
1225 app.send_info_toast(
1226 &format!("👍 Save file cloud_save_{} loaded", save_file_number),
1227 None,
1228 );
1229 app.dispatch(IoEvent::ResetVisibleBoardsandCards).await;
1230 app.set_view(default_view);
1231 Ok(())
1232 }
1233
1234 async fn delete_cloud_save(&mut self) -> Result<()> {
1235 {
1236 let mut app = self.app.lock().await;
1237 if app.state.user_login_data.auth_token.is_none() {
1238 error!("Not logged in");
1239 app.send_error_toast("Not logged in", None);
1240 return Ok(());
1241 } else {
1242 info!("Deleting cloud save, please wait...");
1243 app.send_info_toast("Deleting cloud save, please wait...", None);
1244 }
1245 }
1246
1247 let mut app = self.app.lock().await;
1248 let save_file_index = app.state.app_list_states.load_save.selected().unwrap_or(0);
1249 let user_access_token = app.state.user_login_data.auth_token.clone().unwrap();
1250 let cloud_saves = app.state.cloud_data.clone();
1251 let cloud_saves = if let Some(cloud_saves) = cloud_saves {
1252 cloud_saves
1253 } else {
1254 error!("Could not get local save files");
1255 app.send_error_toast("Could not get local save files", None);
1256 return Ok(());
1257 };
1258 if save_file_index >= cloud_saves.len() {
1259 error!("Cannot delete save file: No such file");
1260 app.send_error_toast("Cannot delete save file: No such file", None);
1261 return Ok(());
1262 }
1263 drop(app);
1264 let save_file_id = cloud_saves[save_file_index].id;
1265 let save_number = cloud_saves[save_file_index].save_id;
1266 let delete_status =
1267 delete_a_save_from_database(&user_access_token, false, save_file_id, Some(save_number))
1268 .await;
1269 let mut app = self.app.lock().await;
1270 if delete_status.is_err() {
1271 app.send_error_toast("Error deleting cloud save", None);
1272 } else {
1273 app.send_info_toast(
1274 &format!("👍 Cloud save cloud_save_{} deleted", save_number),
1275 None,
1276 );
1277 }
1278
1279 Ok(())
1280 }
1281}
1282
1283pub(crate) fn get_config_dir() -> Result<PathBuf, String> {
1284 let home_dir = home::home_dir();
1285 if home_dir.is_none() {
1286 return Err(String::from("Error getting home directory"));
1287 }
1288 let mut config_dir = home_dir.unwrap();
1289 if cfg!(windows) {
1290 config_dir.push("AppData");
1291 config_dir.push("Roaming");
1292 } else {
1293 config_dir.push(".config");
1294 }
1295 config_dir.push(CONFIG_DIR_NAME);
1296 Ok(config_dir)
1297}
1298
1299pub(crate) fn get_save_dir() -> PathBuf {
1300 let mut save_dir = env::temp_dir();
1301 save_dir.push(SAVE_DIR_NAME);
1302 save_dir
1303}
1304
1305pub fn prepare_config_dir() -> Result<(), String> {
1306 let config_dir = get_config_dir();
1307 if config_dir.is_err() {
1308 return Err(String::from("Error getting config directory"));
1309 }
1310 let config_dir = config_dir.unwrap();
1311 if !config_dir.exists() {
1312 let dir_creation_status = std::fs::create_dir_all(&config_dir);
1313 if dir_creation_status.is_err() {
1314 return Err(String::from("Error creating config directory"));
1315 }
1316 }
1317 let mut config_file = config_dir;
1318 config_file.push(CONFIG_FILE_NAME);
1319 if !config_file.exists() {
1320 let default_config = AppConfig::default();
1321 let config_json = serde_json::to_string_pretty(&default_config);
1322 if let Ok(config_json) = config_json {
1323 let file_creation_status = std::fs::write(&config_file, config_json);
1324 if file_creation_status.is_err() {
1325 return Err(String::from("Error creating config file"));
1326 }
1327 } else {
1328 return Err(String::from("Error creating config file"));
1329 }
1330 }
1331 Ok(())
1332}
1333
1334fn prepare_save_dir() -> bool {
1335 let save_dir = get_save_dir();
1336 if !save_dir.exists() {
1337 std::fs::create_dir_all(&save_dir).unwrap();
1338 }
1339 true
1340}
1341
1342fn prepare_boards(app: &mut App) {
1343 let boards = if app.config.always_load_last_save {
1344 let latest_save_file_info = get_latest_save_file(&app.config);
1345 if let Ok(latest_save_file) = latest_save_file_info {
1346 let local_data = get_local_kanban_state(latest_save_file.clone(), false, &app.config);
1347 match local_data {
1348 Ok(data) => {
1349 info!("👍 Local data loaded from {:?}", latest_save_file);
1350 app.send_info_toast(
1351 &format!("👍 Local data loaded from {:?}", latest_save_file),
1352 None,
1353 );
1354 data
1355 }
1356 Err(err) => {
1357 debug!("Cannot get local data: {:?}", err);
1358 error!("👎 Cannot get local data, Data might be corrupted or is not in the correct format");
1359 app.send_error_toast("👎 Cannot get local data, Data might be corrupted or is not in the correct format", None);
1360 Boards::default()
1361 }
1362 }
1363 } else {
1364 Boards::default()
1365 }
1366 } else {
1367 app.set_view(View::LoadLocalSave);
1368 Boards::default()
1369 };
1370 app.boards.set_boards(boards);
1371}
1372
1373fn get_latest_save_file(config: &AppConfig) -> Result<String, String> {
1374 let local_save_files = get_available_local_save_files(config);
1375 let local_save_files = if let Some(local_save_files) = local_save_files {
1376 local_save_files
1377 } else {
1378 return Err("No local save files found".to_string());
1379 };
1380 let fall_back_version = -1;
1381 if local_save_files.is_empty() {
1382 return Err("No local save files found".to_string());
1383 }
1384
1385 let latest_date = local_save_files
1386 .iter()
1387 .map(|file| {
1388 let date = file.split('_').collect::<Vec<&str>>()[1];
1389 NaiveDate::parse_from_str(date, "%d-%m-%Y").unwrap()
1390 })
1391 .max()
1392 .unwrap();
1393
1394 let latest_version = local_save_files
1395 .iter()
1396 .filter(|file| {
1397 let date = file.split('_').collect::<Vec<&str>>()[1];
1398 NaiveDate::parse_from_str(date, "%d-%m-%Y").unwrap() == latest_date
1399 })
1400 .map(|file| {
1401 let version = file.split("_v").collect::<Vec<&str>>()[1];
1402 let version = version.split('.').collect::<Vec<&str>>()[0];
1403 version.parse::<i32>().unwrap_or(fall_back_version)
1404 })
1405 .max()
1406 .unwrap_or(fall_back_version);
1407
1408 if latest_version == fall_back_version {
1409 return Err("No local save files found".to_string());
1410 }
1411 let latest_version = latest_version as u32;
1412
1413 let latest_save_file = format!(
1414 "kanban_{}_v{}.json",
1415 latest_date.format("%d-%m-%Y"),
1416 latest_version
1417 );
1418 Ok(latest_save_file)
1419}
1420
1421pub fn refresh_visible_boards_and_cards(app: &mut App) {
1422 let mut visible_boards_and_cards: LinkedHashMap<(u64, u64), Vec<(u64, u64)>> =
1423 LinkedHashMap::new();
1424 let boards = if app.filtered_boards.is_empty() {
1425 app.boards.get_boards()
1426 } else {
1427 app.filtered_boards.get_boards()
1428 };
1429 for (i, board) in boards.iter().enumerate() {
1430 if (i) as u16 == app.config.no_of_boards_to_show {
1431 break;
1432 }
1433 let mut visible_cards: Vec<(u64, u64)> = Vec::new();
1434 if board.cards.len() > app.config.no_of_cards_to_show.into() {
1435 for card in board
1436 .cards
1437 .get_all_cards()
1438 .iter()
1439 .take(app.config.no_of_cards_to_show.into())
1440 {
1441 visible_cards.push(card.id);
1442 }
1443 } else {
1444 for card in board.cards.get_all_cards() {
1445 visible_cards.push(card.id);
1446 }
1447 }
1448
1449 let mut visible_board: LinkedHashMap<(u64, u64), Vec<(u64, u64)>> = LinkedHashMap::new();
1450 visible_board.insert(board.id, visible_cards);
1451 visible_boards_and_cards.extend(visible_board);
1452 }
1453 app.visible_boards_and_cards = visible_boards_and_cards;
1454 if !app.visible_boards_and_cards.is_empty() {
1455 app.state.current_board_id = Some(*app.visible_boards_and_cards.keys().next().unwrap());
1456 if !app
1457 .visible_boards_and_cards
1458 .values()
1459 .next()
1460 .unwrap()
1461 .is_empty()
1462 {
1463 app.state.current_card_id =
1464 Some(app.visible_boards_and_cards.values().next().unwrap()[0]);
1465 }
1466 }
1467}
1468
1469pub fn make_file_system_safe_name(name: &str) -> String {
1470 let mut safe_name = name.to_string();
1471 let unsafe_chars = vec!["/", "\\", ":", "*", "?", "\"", "<", ">", "|", " "];
1472 for unsafe_char in unsafe_chars {
1473 safe_name = safe_name.replace(unsafe_char, "");
1474 }
1475 safe_name
1476}
1477
1478pub async fn auto_save(app: &mut App<'_>) -> Result<(), String> {
1479 if save_required(app) {
1480 save_kanban_state_locally(app.boards.get_boards().to_vec(), &app.config)
1481 } else {
1482 Ok(())
1483 }
1484}
1485
1486fn save_required(app: &mut App) -> bool {
1487 let latest_save_file_info = get_latest_save_file(&app.config);
1488 if let Ok(save_file_name) = latest_save_file_info {
1489 let board_data = get_local_kanban_state(save_file_name, false, &app.config);
1490 match board_data {
1491 Ok(boards) => app.boards != boards,
1492 Err(_) => true,
1493 }
1494 } else {
1495 true
1496 }
1497}
1498
1499#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1500pub struct CloudData {
1501 pub id: u64,
1502 pub created_at: String,
1503 pub user_id: String,
1504 pub board_data: String,
1505 pub nonce: String,
1506 pub save_id: usize,
1507}
1508
1509enum PasswordStatus {
1510 Strong,
1511 MissingUppercase,
1512 MissingLowercase,
1513 MissingNumber,
1514 MissingSpecialChar,
1515 TooShort,
1516 TooLong,
1517}
1518
1519fn check_for_safe_password(check_password: &str) -> PasswordStatus {
1520 let mut password_status = PasswordStatus::Strong;
1521 if check_password.len() < MIN_PASSWORD_LENGTH {
1522 password_status = PasswordStatus::TooShort;
1523 }
1524 if !check_password.chars().any(|c| c.is_uppercase()) {
1525 password_status = PasswordStatus::MissingUppercase;
1526 }
1527 if !check_password.chars().any(|c| c.is_lowercase()) {
1528 password_status = PasswordStatus::MissingLowercase;
1529 }
1530 if !check_password.chars().any(|c| c.is_numeric()) {
1531 password_status = PasswordStatus::MissingNumber;
1532 }
1533 if !check_password.chars().any(|c| c.is_ascii_punctuation()) {
1534 password_status = PasswordStatus::MissingSpecialChar;
1535 }
1536 if check_password.len() > MAX_PASSWORD_LENGTH {
1537 password_status = PasswordStatus::TooLong;
1538 }
1539 password_status
1540}
1541
1542fn encrypt_save(boards: &Boards, key: &[u8]) -> Result<(String, String), String> {
1543 let base64_engine = base64::engine::general_purpose::URL_SAFE_NO_PAD;
1544 let boards_json = serde_json::to_string(&boards);
1545 if boards_json.is_err() {
1546 return Err("Error serializing boards".to_string());
1547 }
1548 let boards_json = boards_json.unwrap();
1549 let key = Key::<Aes256Gcm>::from_slice(key);
1550 let cipher = Aes256Gcm::new(key);
1551 let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
1552 let nonce_vec = nonce.to_vec();
1553 let nonce_encoded = base64_engine.encode(nonce_vec);
1554 let encrypted_boards = cipher.encrypt(&nonce, boards_json.as_bytes());
1555 if encrypted_boards.is_err() {
1556 return Err("Error encrypting boards".to_string());
1557 }
1558 let encrypted_boards = encrypted_boards.unwrap();
1559 let encoded_boards = base64_engine.encode(encrypted_boards);
1560 Ok((encoded_boards, nonce_encoded))
1561}
1562
1563fn decrypt_save(
1564 encrypted_boards: String,
1565 key: &[u8],
1566 encoded_nonce: &str,
1567) -> Result<Boards, String> {
1568 let base64_engine = base64::engine::general_purpose::URL_SAFE_NO_PAD;
1569 let encrypted_boards = base64_engine.decode(encrypted_boards);
1570 if encrypted_boards.is_err() {
1571 return Err("Error decoding boards".to_string());
1572 }
1573 let encrypted_boards = encrypted_boards.unwrap();
1574 let key = Key::<Aes256Gcm>::from_slice(key);
1575 let cipher = Aes256Gcm::new(key);
1576 let nonce = base64_engine.decode(encoded_nonce);
1577 if nonce.is_err() {
1578 return Err("Error decoding nonce".to_string());
1579 }
1580 let nonce = nonce.unwrap();
1581 let nonce = GenericArray::from_slice(&nonce);
1582 let decrypted_board_data = cipher.decrypt(nonce, encrypted_boards.as_slice());
1583 if decrypted_board_data.is_err() {
1584 return Err("Error decrypting boards".to_string());
1585 }
1586 let decrypted_board_data = decrypted_board_data.unwrap();
1587 let decrypted_board_data = String::from_utf8(decrypted_board_data);
1588 if decrypted_board_data.is_err() {
1589 return Err("Error converting decrypted boards to string".to_string());
1590 }
1591 let decrypted_board_data = decrypted_board_data.unwrap();
1592 let boards = serde_json::from_str(&decrypted_board_data);
1593 if boards.is_err() {
1594 return Err("Error deserializing boards".to_string());
1595 }
1596 Ok(boards.unwrap())
1597}
1598
1599pub fn save_user_encryption_key(key: &[u8]) -> Result<String> {
1600 let base64_engine = base64::engine::general_purpose::URL_SAFE_NO_PAD;
1601 let key = base64_engine.encode(key);
1602 let mut config_dir = get_config_dir().unwrap();
1603 config_dir.push(ENCRYPTION_KEY_FILE_NAME);
1604 let file_creation_status = std::fs::write(&config_dir, key);
1605 if let Err(e) = file_creation_status {
1606 Err(anyhow!(e))
1607 } else {
1608 Ok(config_dir.to_str().unwrap().to_string())
1609 }
1610}
1611
1612fn get_user_encryption_key(encryption_key_from_arguments: Option<String>) -> Result<Vec<u8>> {
1613 let base64_engine = base64::engine::general_purpose::URL_SAFE_NO_PAD;
1614 if let Some(encryption_key_from_arguments) = encryption_key_from_arguments {
1615 let decoded_key = base64_engine.decode(encryption_key_from_arguments);
1616 if let Err(e) = decoded_key {
1617 Err(anyhow!(e))
1618 } else {
1619 Ok(decoded_key.unwrap())
1620 }
1621 } else {
1622 let mut encryption_key_path = get_config_dir().unwrap();
1623 encryption_key_path.push(ENCRYPTION_KEY_FILE_NAME);
1624 if !encryption_key_path.exists() {
1625 return Err(anyhow!("Encryption key file not found!! Please generate a new one by using the -g flag or move it to the path: {:?}", encryption_key_path));
1626 }
1627 let encoded_key = std::fs::read_to_string(&encryption_key_path);
1628 if let Err(e) = encoded_key {
1629 Err(anyhow!(e))
1630 } else {
1631 let key = encoded_key.unwrap();
1632 let decoded_key = base64_engine.decode(key);
1633 if let Err(e) = decoded_key {
1634 Err(anyhow!(e))
1635 } else {
1636 Ok(decoded_key.unwrap())
1637 }
1638 }
1639 }
1640}
1641
1642pub fn generate_new_encryption_key() -> Vec<u8> {
1643 Aes256Gcm::generate_key(&mut OsRng).to_vec()
1644}
1645
1646pub async fn get_all_save_ids_for_user(user_id: String, access_token: &str) -> Result<Vec<usize>> {
1647 let client = reqwest::Client::new();
1648 let response = client
1649 .get(format!(
1650 "{}/rest/v1/user_data?user_id=eq.{}&select=save_id",
1651 SUPABASE_URL, user_id
1652 ))
1653 .header("apikey", SUPABASE_ANON_KEY)
1654 .header("Content-Type", "application/json")
1655 .header("Authorization", format!("Bearer {}", access_token))
1656 .header("Range", "0-9")
1657 .send()
1658 .await;
1659 if response.is_err() {
1660 debug!("Error getting save ids: {:?}", response.err());
1661 return Err(anyhow!("Error getting save ids".to_string()));
1662 }
1663 let response = response.unwrap();
1664 let status = response.status();
1665 if status == StatusCode::OK {
1666 let body = response.json::<serde_json::Value>().await;
1667 match body {
1668 Ok(save_instances) => {
1669 let mut save_ids: Vec<usize> = Vec::new();
1670 let save_instances_as_array = save_instances.as_array();
1671 if save_instances_as_array.is_none() {
1672 return Err(anyhow!("Error getting save ids".to_string()));
1673 }
1674 let save_instances_as_array = save_instances_as_array.unwrap();
1675 for save_instance in save_instances_as_array {
1676 let save_id = save_instance.get("save_id");
1677 if save_id.is_none() {
1678 return Err(anyhow!("Error getting save ids".to_string()));
1679 }
1680 let save_id = save_id.unwrap().as_u64();
1681 if save_id.is_none() {
1682 return Err(anyhow!("Error getting save ids".to_string()));
1683 }
1684 debug!("Found save_id: {:?}", save_id.unwrap() as usize);
1685 save_ids.push(save_id.unwrap() as usize);
1686 }
1687 debug!("Save instances: {:?}", save_instances);
1688 Ok(save_ids)
1689 }
1690 Err(e) => {
1691 debug!("Error getting save ids: {:?}", e);
1692 Err(anyhow!("Error getting save ids".to_string()))
1693 }
1694 }
1695 } else {
1696 debug!("Status: {:?}", status);
1697 debug!("Error getting save ids: {:?}", response.text().await);
1698 Err(anyhow!("Error getting save ids".to_string()))
1699 }
1700}
1701
1702pub async fn get_all_save_ids_and_creation_dates_for_user(
1703 user_id: String,
1704 access_token: &str,
1705 cli_mode: bool,
1706) -> Result<Vec<(usize, String, usize)>> {
1707 let client = reqwest::Client::new();
1708 let response = client
1709 .get(format!(
1710 "{}/rest/v1/user_data?user_id=eq.{}&select=save_id,created_at,id",
1711 SUPABASE_URL, user_id
1712 ))
1713 .header("apikey", SUPABASE_ANON_KEY)
1714 .header("Content-Type", "application/json")
1715 .header("Authorization", format!("Bearer {}", access_token))
1716 .send()
1717 .await;
1718 if let Err(e) = response {
1719 debug!("Error getting save ids and created_at: {:?}", e);
1720 return Err(anyhow!("Error getting save ids".to_string()));
1721 }
1722 let response = response.unwrap();
1723 let status = response.status();
1724 if status == StatusCode::OK {
1725 let body = response.json::<serde_json::Value>().await;
1726 match body {
1727 Ok(save_instances) => {
1728 let mut save_ids_and_creation_dates: Vec<(usize, String, usize)> = Vec::new();
1729 let save_instances_as_array = save_instances.as_array();
1730 if save_instances_as_array.is_none() {
1731 return Err(anyhow!("Error getting save ids".to_string()));
1732 }
1733 let save_instances_as_array = save_instances_as_array.unwrap().to_owned();
1734 for save_instance in save_instances_as_array {
1735 let save_id = save_instance.get("save_id");
1736 if save_id.is_none() {
1737 return Err(anyhow!("Error getting save ids".to_string()));
1738 }
1739 let save_id = save_id.unwrap().as_u64();
1740 if save_id.is_none() {
1741 return Err(anyhow!("Error getting save ids".to_string()));
1742 }
1743 let save_id = save_id.unwrap() as usize;
1744 let created_at = save_instance.get("created_at");
1745 if created_at.is_none() {
1746 return Err(anyhow!("Error getting save ids".to_string()));
1747 }
1748 let created_at = created_at.unwrap().as_str();
1749 if created_at.is_none() {
1750 return Err(anyhow!("Error getting save ids".to_string()));
1751 }
1752 let created_at = created_at.unwrap().to_owned();
1753 let id = save_instance.get("id");
1754 if id.is_none() {
1755 return Err(anyhow!("Error getting save ids".to_string()));
1756 }
1757 let id = id.unwrap().as_u64();
1758 if id.is_none() {
1759 return Err(anyhow!("Error getting save ids".to_string()));
1760 }
1761 let id = id.unwrap() as usize;
1762 if cli_mode {
1763 print_debug(&format!("save_id: {:?}", save_id));
1764 print_debug(&format!("created_at: {:?}", created_at));
1765 print_debug(&format!("id: {:?}", id));
1766 } else {
1767 debug!("save_id: {:?}", save_id);
1768 debug!("created_at: {:?}", created_at);
1769 debug!("id: {:?}", id);
1770 }
1771 save_ids_and_creation_dates.push((save_id, created_at, id));
1772 }
1773 if cli_mode {
1774 print_debug(&format!("save_instances: {:?}", save_instances));
1775 } else {
1776 debug!("save_instances: {:?}", save_instances);
1777 }
1778 Ok(save_ids_and_creation_dates)
1779 }
1780 Err(e) => {
1781 if cli_mode {
1782 print_debug(&format!("Error getting save ids: {:?}", e));
1783 } else {
1784 debug!("Error getting save ids: {:?}", e);
1785 }
1786 Err(anyhow!("Error getting save ids".to_string()))
1787 }
1788 }
1789 } else {
1790 if cli_mode {
1791 print_debug(&format!("Status: {:?}", status));
1792 print_debug(&format!(
1793 "Error getting save ids: {:?}",
1794 response.text().await
1795 ));
1796 } else {
1797 debug!("Status: {:?}", status);
1798 debug!("Error getting save ids: {:?}", response.text().await);
1799 }
1800 Err(anyhow!("Error getting save ids".to_string()))
1801 }
1802}
1803
1804pub async fn delete_a_save_from_database(
1805 access_token: &str,
1806 cli_mode: bool,
1807 save_id: u64,
1808 save_number: Option<usize>,
1809) -> Result<String> {
1810 let client = reqwest::Client::new();
1811 let response = client
1812 .delete(format!(
1813 "{}/rest/v1/user_data?id=eq.{}",
1814 SUPABASE_URL, save_id
1815 ))
1816 .header("apikey", SUPABASE_ANON_KEY)
1817 .header("Content-Type", "application/json")
1818 .header("Authorization", format!("Bearer {}", access_token))
1819 .send()
1820 .await?;
1821 let status = response.status();
1822 if status == StatusCode::NO_CONTENT {
1823 if cli_mode {
1824 if save_number.is_some() {
1825 print_info(&format!("👍 Cloud save {} deleted", save_number.unwrap()));
1826 } else {
1827 print_info("👍 Cloud save deleted");
1828 }
1829 Ok("👍 Cloud save deleted".to_string())
1830 } else if save_number.is_some() {
1831 info!("👍 Cloud save {} deleted", save_number.unwrap());
1832 Ok(format!("👍 Cloud save {} deleted", save_number.unwrap()).to_string())
1833 } else {
1834 info!("👍 Cloud save deleted");
1835 Ok("👍 Cloud save deleted".to_string())
1836 }
1837 } else {
1838 let body = response.json::<serde_json::Value>().await;
1839 if cli_mode {
1840 print_error("Error deleting cloud save");
1841 print_debug(&format!(
1842 "status code {}, response body: {:?}",
1843 status, body
1844 ));
1845 } else {
1846 error!("Error deleting cloud save");
1847 debug!("status code {}, response body: {:?}", status, body);
1848 }
1849 Err(anyhow!("Error deleting cloud save"))
1850 }
1851}
1852
1853pub async fn get_user_id_from_database(access_token: &str, cli_mode: bool) -> Result<String> {
1854 let user_data_client = reqwest::Client::new();
1855 let user_data_response = user_data_client
1856 .get(format!("{}/auth/v1/user", SUPABASE_URL))
1857 .header("apikey", SUPABASE_ANON_KEY)
1858 .header("Content-Type", "application/json")
1859 .header("Authorization", format!("Bearer {}", access_token))
1860 .send()
1861 .await?;
1862 let user_data_status = user_data_response.status();
1863 let user_data_body = user_data_response.json::<serde_json::Value>().await;
1864 if user_data_status != StatusCode::OK {
1865 if cli_mode {
1866 print_error("Error retrieving user data");
1867 print_debug(&format!(
1868 "status code {}, response body: {:?}",
1869 user_data_status, user_data_body
1870 ));
1871 } else {
1872 error!("Error retrieving user data");
1873 debug!(
1874 "status code {}, response body: {:?}",
1875 user_data_status, user_data_body
1876 );
1877 }
1878 return Err(anyhow!("Error retrieving user data"));
1879 }
1880 let user_data_body = user_data_body.unwrap();
1881 let user_id = user_data_body.get("id");
1882 if user_id.is_none() {
1883 if cli_mode {
1884 print_error("Error retrieving user data");
1885 print_debug(&format!(
1886 "status code {}, response body: {:?}, could not find id",
1887 user_data_status, user_data_body
1888 ));
1889 } else {
1890 error!("Error retrieving user data");
1891 debug!(
1892 "status code {}, response body: {:?}, could not find id",
1893 user_data_status, user_data_body
1894 );
1895 }
1896 return Err(anyhow!("Error retrieving user data"));
1897 }
1898 let user_id = user_id.unwrap().as_str();
1899 if cli_mode {
1900 print_debug(&format!("user_id: {:?}", user_id));
1901 } else {
1902 debug!("user_id: {:?}", user_id);
1903 }
1904 Ok(user_id.unwrap().to_string())
1905}
1906
1907pub async fn login_for_user(
1908 email_id: &str,
1909 password: &str,
1910 cli_mode: bool,
1911) -> Result<(String, String, String), String> {
1912 let request_body = json!(
1913 {
1914 "email": email_id,
1915 "password": password
1916 }
1917 );
1918 let client = reqwest::Client::new();
1919 let response = client
1920 .post(format!(
1921 "{}/auth/v1/token?grant_type=password",
1922 SUPABASE_URL
1923 ))
1924 .header("apikey", SUPABASE_ANON_KEY)
1925 .header("Content-Type", "application/json")
1926 .body(request_body.to_string())
1927 .send()
1928 .await;
1929 if let Err(e) = response {
1930 if cli_mode {
1931 print_debug(&format!("Error logging in: {}", e));
1932 print_error("Error logging in, Something went wrong, please try again later");
1933 } else {
1934 debug!("Error logging in: {}", e);
1935 error!("Error logging in, Something went wrong, please try again later");
1936 }
1937 return Err("Error logging in, Something went wrong, please try again later".to_string());
1938 }
1939 let response = response.unwrap();
1940 let status = response.status();
1941 let body = response.json::<serde_json::Value>().await;
1942 if status == StatusCode::OK {
1943 match body {
1944 Ok(body) => {
1945 let access_token = body.get("access_token");
1946 let refresh_token = body.get("refresh_token");
1947 let access_token_result = match access_token {
1948 Some(access_token) => {
1949 let access_token = access_token.as_str().unwrap();
1950 if cli_mode {
1951 print_info("🚀 Login successful");
1952 print_debug(&format!("Access token: {}", access_token));
1953 } else {
1954 info!("🚀 Login successful");
1955 debug!("Access token: {}", access_token);
1956 }
1957 let user_id = get_user_id_from_database(access_token, cli_mode)
1958 .await
1959 .unwrap_or_else(|_| "Error getting user id".to_string());
1960 Ok((access_token.to_string(), user_id))
1961 }
1962 None => {
1963 if cli_mode {
1964 print_error("Error logging in");
1965 print_debug(&format!(
1966 "status code {}, response body: {:?}, could not find access token",
1967 status, body
1968 ));
1969 } else {
1970 error!("Error logging in, If this is your first login attempt after signup please login again, if it is not please contact the developer");
1971 debug!(
1972 "status code {}, response body: {:?}, could not find access token",
1973 status, body
1974 );
1975 }
1976 Err("Error logging in, If this is your first login attempt after signup please login again, if it is not please contact the developer".to_string())
1977 }
1978 };
1979 let refresh_token_result = match refresh_token {
1980 Some(refresh_token) => {
1981 let refresh_token = refresh_token.as_str().unwrap();
1982 if cli_mode {
1983 print_debug(&format!("Refresh token: {}", refresh_token));
1984 } else {
1985 debug!("Refresh token: {}", refresh_token);
1986 }
1987 Ok(refresh_token.to_string())
1988 }
1989 None => {
1990 if cli_mode {
1991 print_error("Error logging in");
1992 print_debug(&format!(
1993 "status code {}, response body: {:?}, could not find refresh token",
1994 status, body
1995 ));
1996 } else {
1997 error!("Error logging in, If this is your first login attempt after signup please login again, if it is not please contact the developer");
1998 debug!(
1999 "status code {}, response body: {:?}, could not find refresh token",
2000 status, body
2001 );
2002 }
2003 Err("Error logging in, If this is your first login attempt after signup please login again, if it is not please contact the developer".to_string())
2004 }
2005 };
2006
2007 if access_token_result.is_err() || refresh_token_result.is_err() {
2008 Err("Error logging in, If this is your first login attempt after signup please login again, if it is not please contact the developer".to_string())
2009 } else {
2010 let access_token_result = access_token_result.unwrap();
2011 let refresh_token_result = refresh_token_result.unwrap();
2012 Ok((
2013 access_token_result.0,
2014 access_token_result.1,
2015 refresh_token_result,
2016 ))
2017 }
2018 }
2019 Err(e) => Err(format!("Error logging in: {}", e)),
2020 }
2021 } else if status == StatusCode::TOO_MANY_REQUESTS {
2022 if cli_mode {
2023 print_error("Too many requests, please try again later. Due to the free nature of supabase i am limited to only 4 signup requests per hour. Sorry! 😢");
2024 print_debug(&format!(
2025 "status code {}, response body: {:?}",
2026 status, body
2027 ));
2028 } else {
2029 error!("Too many requests, please try again later. Due to the free nature of supabase i am limited to only 4 signup requests per hour. Sorry! 😢");
2030 debug!("status code {}, response body: {:?}", status, body);
2031 }
2032 Err("Too many requests, please try again later. Due to the free nature of supabase i am limited to only 4 signup requests per hour. Sorry! 😢".to_string())
2033 } else {
2034 match body {
2035 Ok(body) => {
2036 let error_description = body.get("error_description");
2037 match error_description {
2038 Some(error_description) => {
2039 let error_description = error_description.to_string();
2040 if cli_mode {
2041 print_error(&error_description);
2042 print_debug(&format!(
2043 "status code {}, response body: {:?}",
2044 status, body
2045 ));
2046 } else {
2047 error!("{}", error_description);
2048 debug!("status code {}, response body: {:?}", status, body);
2049 }
2050 Err(format!("Error logging in: {}", error_description))
2051 }
2052 None => {
2053 if cli_mode {
2054 print_error("Error logging in");
2055 print_debug(&format!(
2056 "status code {}, response body: {:?}",
2057 status, body
2058 ));
2059 } else {
2060 error!("Error logging in");
2061 debug!("status code {}, response body: {:?}", status, body);
2062 }
2063 Err("Error logging in".to_string())
2064 }
2065 }
2066 }
2067 Err(e) => {
2068 if cli_mode {
2069 print_error(&format!("Error logging in: {}", e));
2070 } else {
2071 error!("Error logging in: {}", e);
2072 }
2073 Err(format!("Error logging in: {}", e))
2074 }
2075 }
2076 }
2077}
2078
2079async fn save_refresh_token_to_disk(
2080 refresh_token: &str,
2081 email_id: &str,
2082 encryption_key_from_arguments: Option<String>,
2083) -> Result<()> {
2084 let base64_engine = base64::engine::general_purpose::URL_SAFE_NO_PAD;
2085 let refresh_token_path = get_config_dir();
2086 if refresh_token_path.is_err() {
2087 return Err(anyhow!("Error getting config directory"));
2088 }
2089 let mut refresh_token_path = refresh_token_path.unwrap();
2090 refresh_token_path.push(REFRESH_TOKEN_FILE_NAME);
2091 if refresh_token_path.exists() {
2092 let delete_file_status = std::fs::remove_file(&refresh_token_path);
2093 if delete_file_status.is_err() {
2094 return Err(anyhow!("Error deleting refresh token file"));
2095 }
2096 }
2097 let encryption_key = get_user_encryption_key(encryption_key_from_arguments);
2098 if let Err(e) = encryption_key {
2099 return Err(anyhow!(e));
2100 }
2101 let encryption_key = encryption_key.unwrap();
2102 let key = Key::<Aes256Gcm>::from_slice(&encryption_key);
2103 let cipher = Aes256Gcm::new(key);
2104 let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
2105 let encrypted_refresh_token = cipher.encrypt(&nonce, refresh_token.as_bytes());
2106 if encrypted_refresh_token.is_err() {
2107 return Err(anyhow!("Error encrypting refresh token"));
2108 }
2109 let encrypted_refresh_token = encrypted_refresh_token.unwrap();
2110 let nonce = nonce.to_vec();
2111 let nonce = base64_engine.encode(nonce);
2112 let encrypted_refresh_token = base64_engine.encode(encrypted_refresh_token);
2113 let encoded_email_id = base64_engine.encode(email_id.as_bytes());
2114 let refresh_token_data = format!(
2115 "{}{}{}{}{}",
2116 nonce,
2117 REFRESH_TOKEN_SEPARATOR,
2118 encrypted_refresh_token,
2119 REFRESH_TOKEN_SEPARATOR,
2120 encoded_email_id
2121 );
2122 let file_creation_status = std::fs::write(&refresh_token_path, refresh_token_data);
2123 if file_creation_status.is_err() {
2124 return Err(anyhow!("Error creating refresh token file"));
2125 }
2126 Ok(())
2127}
2128
2129fn get_refresh_token_from_disk(
2130 encryption_key_from_arguments: Option<String>,
2131) -> Result<(String, String)> {
2132 let base64_engine = base64::engine::general_purpose::URL_SAFE_NO_PAD;
2133 let refresh_token_path = get_config_dir();
2134 if refresh_token_path.is_err() {
2135 return Err(anyhow!("Error getting config directory"));
2136 }
2137 let mut refresh_token_path = refresh_token_path.unwrap();
2138 refresh_token_path.push(REFRESH_TOKEN_FILE_NAME);
2139 let refresh_token_data = std::fs::read_to_string(&refresh_token_path);
2140 if refresh_token_data.is_err() {
2141 return Err(anyhow!("Error reading refresh token file"));
2142 }
2143 let refresh_token_data = refresh_token_data.unwrap();
2144 let refresh_token_data = refresh_token_data
2145 .split(REFRESH_TOKEN_SEPARATOR)
2146 .collect::<Vec<&str>>();
2147 if refresh_token_data.len() != 3 {
2148 return Err(anyhow!("Error reading refresh token file"));
2149 }
2150 let nonce = refresh_token_data[0];
2151 let nonce = base64_engine.decode(nonce);
2152 if nonce.is_err() {
2153 return Err(anyhow!("Error reading refresh token file"));
2154 }
2155 let nonce = nonce.unwrap();
2156 let nonce = GenericArray::from_slice(&nonce);
2157 let encrypted_refresh_token = refresh_token_data[1];
2158 let encrypted_refresh_token = base64_engine.decode(encrypted_refresh_token);
2159 if encrypted_refresh_token.is_err() {
2160 return Err(anyhow!("Error reading refresh token file"));
2161 }
2162 let encrypted_refresh_token = encrypted_refresh_token.unwrap();
2163 let email_id = refresh_token_data[2];
2164 let email_id = base64_engine.decode(email_id);
2165 if email_id.is_err() {
2166 return Err(anyhow!("Error reading refresh token file"));
2167 }
2168 let email_id = email_id.unwrap();
2169 let email_id = String::from_utf8(email_id);
2170 if email_id.is_err() {
2171 return Err(anyhow!("Error reading refresh token file"));
2172 }
2173 let email_id = email_id.unwrap();
2174 let encryption_key = get_user_encryption_key(encryption_key_from_arguments);
2175 if let Err(e) = encryption_key {
2176 return Err(anyhow!(e));
2177 }
2178 let encryption_key = encryption_key.unwrap();
2179 let key = Key::<Aes256Gcm>::from_slice(&encryption_key);
2180 let cipher = Aes256Gcm::new(key);
2181 let decrypted_refresh_token = cipher.decrypt(nonce, encrypted_refresh_token.as_slice());
2182 if decrypted_refresh_token.is_err() {
2183 return Err(anyhow!("Error decrypting refresh token"));
2184 }
2185 let decrypted_refresh_token = decrypted_refresh_token.unwrap();
2186 let decrypted_refresh_token = String::from_utf8(decrypted_refresh_token);
2187 if decrypted_refresh_token.is_err() {
2188 return Err(anyhow!(
2189 "Error converting decrypted refresh token to string"
2190 ));
2191 }
2192 let decrypted_refresh_token = decrypted_refresh_token.unwrap();
2193 Ok((decrypted_refresh_token, email_id))
2194}
2195
2196async fn delete_refresh_token_from_disk() -> Result<()> {
2197 let refresh_token_path = get_config_dir();
2198 if refresh_token_path.is_err() {
2199 return Err(anyhow!("Error getting config directory"));
2200 }
2201 let mut refresh_token_path = refresh_token_path.unwrap();
2202 refresh_token_path.push(REFRESH_TOKEN_FILE_NAME);
2203 if !refresh_token_path.exists() {
2204 return Ok(());
2205 }
2206 let delete_file_status = std::fs::remove_file(&refresh_token_path);
2207 if delete_file_status.is_err() {
2208 return Err(anyhow!("Error deleting refresh token file"));
2209 }
2210 Ok(())
2211}
2212
2213async fn refresh_access_token(refresh_token: &str) -> Result<(String, String, String), String> {
2214 let request_body = json!(
2215 {
2216 "grant_type": "refresh_token",
2217 "refresh_token": refresh_token
2218 }
2219 );
2220 let client = reqwest::Client::new();
2221 let response = client
2222 .post(format!(
2223 "{}/auth/v1/token?grant_type=refresh_token",
2224 SUPABASE_URL
2225 ))
2226 .header("apikey", SUPABASE_ANON_KEY)
2227 .header("Content-Type", "application/json")
2228 .body(request_body.to_string())
2229 .send()
2230 .await;
2231 if let Err(e) = response {
2232 debug!("Error logging in: {}", e);
2233 error!("Error logging in, Something went wrong, please try again later");
2234 return Err("Error logging in, Something went wrong, please try again later".to_string());
2235 }
2236 let response = response.unwrap();
2237 let status = response.status();
2238 let body = response.json::<serde_json::Value>().await;
2239 if status == StatusCode::OK {
2240 match body {
2241 Ok(body) => {
2242 let access_token = body.get("access_token");
2243 let refresh_token = body.get("refresh_token");
2244 let access_token_result = match access_token {
2245 Some(access_token) => {
2246 let access_token = access_token.as_str().unwrap();
2247 info!("🚀 Login successful");
2248 debug!("Access token: {}", access_token);
2249 let user_id = get_user_id_from_database(access_token, false)
2250 .await
2251 .unwrap_or_else(|_| "Error getting user id".to_string());
2252 Ok((access_token.to_string(), user_id))
2253 }
2254 None => {
2255 error!("Error logging in, If this is your first login attempt after signup please login again, if it is not please contact the developer");
2256 debug!(
2257 "status code {}, response body: {:?}, could not find access token",
2258 status, body
2259 );
2260 Err("Error logging in, If this is your first login attempt after signup please login again, if it is not please contact the developer".to_string())
2261 }
2262 };
2263 let refresh_token_result = match refresh_token {
2264 Some(refresh_token) => {
2265 let refresh_token = refresh_token.as_str().unwrap();
2266 debug!("Refresh token: {}", refresh_token);
2267 Ok(refresh_token.to_string())
2268 }
2269 None => {
2270 error!("Error logging in, If this is your first login attempt after signup please login again, if it is not please contact the developer");
2271 debug!(
2272 "status code {}, response body: {:?}, could not find refresh token",
2273 status, body
2274 );
2275 Err("Error logging in, If this is your first login attempt after signup please login again, if it is not please contact the developer".to_string())
2276 }
2277 };
2278
2279 if access_token_result.is_err() || refresh_token_result.is_err() {
2280 Err("Error logging in, If this is your first login attempt after signup please login again, if it is not please contact the developer".to_string())
2281 } else {
2282 let access_token_result = access_token_result.unwrap();
2283 let refresh_token_result = refresh_token_result.unwrap();
2284 Ok((
2285 access_token_result.0,
2286 access_token_result.1,
2287 refresh_token_result,
2288 ))
2289 }
2290 }
2291 Err(e) => Err(format!("Error logging in: {}", e)),
2292 }
2293 } else if status == StatusCode::TOO_MANY_REQUESTS {
2294 error!("Too many requests, please try again later. Due to the free nature of supabase i am limited to only 4 signup requests per hour. Sorry! 😢");
2295 debug!("status code {}, response body: {:?}", status, body);
2296 Err("Too many requests, please try again later. Due to the free nature of supabase i am limited to only 4 signup requests per hour. Sorry! 😢".to_string())
2297 } else {
2298 match body {
2299 Ok(body) => {
2300 let error_description = body.get("error_description");
2301 match error_description {
2302 Some(error_description) => {
2303 let error_description = error_description.to_string();
2304 error!("{}", error_description);
2305 debug!("status code {}, response body: {:?}", status, body);
2306 Err(format!("Error logging in: {}", error_description))
2307 }
2308 None => {
2309 error!("Error logging in");
2310 debug!("status code {}, response body: {:?}", status, body);
2311 Err("Error logging in".to_string())
2312 }
2313 }
2314 }
2315 Err(e) => {
2316 error!("Error logging in: {}", e);
2317 Err(format!("Error logging in: {}", e))
2318 }
2319 }
2320 }
2321}
2322
2323async fn test_refresh_token_on_disk(
2324 encryption_key_from_arguments: Option<String>,
2325) -> Result<UserLoginData> {
2326 let (refresh_token, email_id) =
2327 get_refresh_token_from_disk(encryption_key_from_arguments.clone())?;
2328 debug!("refresh_token: {:?}", refresh_token);
2329 let status = refresh_access_token(&refresh_token).await;
2330 if status.is_err() {
2331 return Err(anyhow!(status.err().unwrap()));
2332 }
2333 let status = status.unwrap();
2334 let access_token = status.0;
2335 let user_id = status.1;
2336 let refresh_token = status.2;
2337 let save_status =
2338 save_refresh_token_to_disk(&refresh_token, &email_id, encryption_key_from_arguments).await;
2339 if save_status.is_err() {
2340 error!("Error saving refresh token to disk");
2341 }
2342 let user_data = UserLoginData {
2343 auth_token: Some(access_token),
2344 email_id: Some(email_id),
2345 refresh_token: Some(refresh_token),
2346 user_id: Some(user_id),
2347 };
2348 Ok(user_data)
2349}