]> Untitled Git - lemmy.git/blob - crates/api/src/local_user.rs
Making public key required. Fixes #1934
[lemmy.git] / crates / api / src / local_user.rs
1 use crate::{captcha_as_wav_base64, Perform};
2 use actix_web::web::Data;
3 use anyhow::Context;
4 use bcrypt::verify;
5 use captcha::{gen, Difficulty};
6 use chrono::Duration;
7 use lemmy_api_common::{
8   blocking,
9   get_local_user_view_from_jwt,
10   is_admin,
11   password_length_check,
12   person::*,
13 };
14 use lemmy_db_schema::{
15   diesel_option_overwrite,
16   diesel_option_overwrite_to_url,
17   from_opt_str_to_opt_enum,
18   naive_now,
19   source::{
20     comment::Comment,
21     community::Community,
22     local_user::{LocalUser, LocalUserForm},
23     moderator::*,
24     password_reset_request::*,
25     person::*,
26     person_block::{PersonBlock, PersonBlockForm},
27     person_mention::*,
28     post::Post,
29     private_message::PrivateMessage,
30     site::*,
31   },
32   traits::{Blockable, Crud},
33   SortType,
34 };
35 use lemmy_db_views::{
36   comment_report_view::CommentReportView,
37   comment_view::{CommentQueryBuilder, CommentView},
38   local_user_view::LocalUserView,
39   post_report_view::PostReportView,
40   private_message_view::PrivateMessageView,
41 };
42 use lemmy_db_views_actor::{
43   community_moderator_view::CommunityModeratorView,
44   person_mention_view::{PersonMentionQueryBuilder, PersonMentionView},
45   person_view::PersonViewSafe,
46 };
47 use lemmy_utils::{
48   claims::Claims,
49   email::send_email,
50   location_info,
51   utils::{generate_random_string, is_valid_display_name, is_valid_matrix_id, naive_from_unix},
52   ApiError,
53   ConnectionId,
54   LemmyError,
55 };
56 use lemmy_websocket::{
57   messages::{CaptchaItem, SendAllMessage},
58   LemmyContext,
59   UserOperation,
60 };
61
62 #[async_trait::async_trait(?Send)]
63 impl Perform for Login {
64   type Response = LoginResponse;
65
66   async fn perform(
67     &self,
68     context: &Data<LemmyContext>,
69     _websocket_id: Option<ConnectionId>,
70   ) -> Result<LoginResponse, LemmyError> {
71     let data: &Login = self;
72
73     // Fetch that username / email
74     let username_or_email = data.username_or_email.clone();
75     let local_user_view = blocking(context.pool(), move |conn| {
76       LocalUserView::find_by_email_or_name(conn, &username_or_email)
77     })
78     .await?
79     .map_err(|e| ApiError::err("couldnt_find_that_username_or_email", e))?;
80
81     // Verify the password
82     let valid: bool = verify(
83       &data.password,
84       &local_user_view.local_user.password_encrypted,
85     )
86     .unwrap_or(false);
87     if !valid {
88       return Err(ApiError::err_plain("password_incorrect").into());
89     }
90
91     // Return the jwt
92     Ok(LoginResponse {
93       jwt: Claims::jwt(
94         local_user_view.local_user.id.0,
95         &context.secret().jwt_secret,
96         &context.settings().hostname,
97       )?,
98     })
99   }
100 }
101
102 #[async_trait::async_trait(?Send)]
103 impl Perform for GetCaptcha {
104   type Response = GetCaptchaResponse;
105
106   async fn perform(
107     &self,
108     context: &Data<LemmyContext>,
109     _websocket_id: Option<ConnectionId>,
110   ) -> Result<Self::Response, LemmyError> {
111     let captcha_settings = context.settings().captcha;
112
113     if !captcha_settings.enabled {
114       return Ok(GetCaptchaResponse { ok: None });
115     }
116
117     let captcha = match captcha_settings.difficulty.as_str() {
118       "easy" => gen(Difficulty::Easy),
119       "medium" => gen(Difficulty::Medium),
120       "hard" => gen(Difficulty::Hard),
121       _ => gen(Difficulty::Medium),
122     };
123
124     let answer = captcha.chars_as_string();
125
126     let png = captcha.as_base64().expect("failed to generate captcha");
127
128     let uuid = uuid::Uuid::new_v4().to_string();
129
130     let wav = captcha_as_wav_base64(&captcha);
131
132     let captcha_item = CaptchaItem {
133       answer,
134       uuid: uuid.to_owned(),
135       expires: naive_now() + Duration::minutes(10), // expires in 10 minutes
136     };
137
138     // Stores the captcha item on the queue
139     context.chat_server().do_send(captcha_item);
140
141     Ok(GetCaptchaResponse {
142       ok: Some(CaptchaResponse { png, wav, uuid }),
143     })
144   }
145 }
146
147 #[async_trait::async_trait(?Send)]
148 impl Perform for SaveUserSettings {
149   type Response = LoginResponse;
150
151   async fn perform(
152     &self,
153     context: &Data<LemmyContext>,
154     _websocket_id: Option<ConnectionId>,
155   ) -> Result<LoginResponse, LemmyError> {
156     let data: &SaveUserSettings = self;
157     let local_user_view =
158       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
159
160     let avatar = diesel_option_overwrite_to_url(&data.avatar)?;
161     let banner = diesel_option_overwrite_to_url(&data.banner)?;
162     let email = diesel_option_overwrite(&data.email);
163     let bio = diesel_option_overwrite(&data.bio);
164     let display_name = diesel_option_overwrite(&data.display_name);
165     let matrix_user_id = diesel_option_overwrite(&data.matrix_user_id);
166     let bot_account = data.bot_account;
167
168     if let Some(Some(bio)) = &bio {
169       if bio.chars().count() > 300 {
170         return Err(ApiError::err_plain("bio_length_overflow").into());
171       }
172     }
173
174     if let Some(Some(display_name)) = &display_name {
175       if !is_valid_display_name(
176         display_name.trim(),
177         context.settings().actor_name_max_length,
178       ) {
179         return Err(ApiError::err_plain("invalid_username").into());
180       }
181     }
182
183     if let Some(Some(matrix_user_id)) = &matrix_user_id {
184       if !is_valid_matrix_id(matrix_user_id) {
185         return Err(ApiError::err_plain("invalid_matrix_id").into());
186       }
187     }
188
189     let local_user_id = local_user_view.local_user.id;
190     let person_id = local_user_view.person.id;
191     let default_listing_type = data.default_listing_type;
192     let default_sort_type = data.default_sort_type;
193     let password_encrypted = local_user_view.local_user.password_encrypted;
194     let public_key = local_user_view.person.public_key;
195
196     let person_form = PersonForm {
197       name: local_user_view.person.name,
198       avatar,
199       banner,
200       inbox_url: None,
201       display_name,
202       published: None,
203       updated: Some(naive_now()),
204       banned: None,
205       deleted: None,
206       actor_id: None,
207       bio,
208       local: None,
209       admin: None,
210       private_key: None,
211       public_key,
212       last_refreshed_at: None,
213       shared_inbox_url: None,
214       matrix_user_id,
215       bot_account,
216     };
217
218     blocking(context.pool(), move |conn| {
219       Person::update(conn, person_id, &person_form)
220     })
221     .await?
222     .map_err(|e| ApiError::err("user_already_exists", e))?;
223
224     let local_user_form = LocalUserForm {
225       person_id,
226       email,
227       password_encrypted,
228       show_nsfw: data.show_nsfw,
229       show_bot_accounts: data.show_bot_accounts,
230       show_scores: data.show_scores,
231       theme: data.theme.to_owned(),
232       default_sort_type,
233       default_listing_type,
234       lang: data.lang.to_owned(),
235       show_avatars: data.show_avatars,
236       show_read_posts: data.show_read_posts,
237       show_new_post_notifs: data.show_new_post_notifs,
238       send_notifications_to_email: data.send_notifications_to_email,
239     };
240
241     let local_user_res = blocking(context.pool(), move |conn| {
242       LocalUser::update(conn, local_user_id, &local_user_form)
243     })
244     .await?;
245     let updated_local_user = match local_user_res {
246       Ok(u) => u,
247       Err(e) => {
248         let err_type = if e.to_string()
249           == "duplicate key value violates unique constraint \"local_user_email_key\""
250         {
251           "email_already_exists"
252         } else {
253           "user_already_exists"
254         };
255
256         return Err(ApiError::err(err_type, e).into());
257       }
258     };
259
260     // Return the jwt
261     Ok(LoginResponse {
262       jwt: Claims::jwt(
263         updated_local_user.id.0,
264         &context.secret().jwt_secret,
265         &context.settings().hostname,
266       )?,
267     })
268   }
269 }
270
271 #[async_trait::async_trait(?Send)]
272 impl Perform for ChangePassword {
273   type Response = LoginResponse;
274
275   async fn perform(
276     &self,
277     context: &Data<LemmyContext>,
278     _websocket_id: Option<ConnectionId>,
279   ) -> Result<LoginResponse, LemmyError> {
280     let data: &ChangePassword = self;
281     let local_user_view =
282       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
283
284     password_length_check(&data.new_password)?;
285
286     // Make sure passwords match
287     if data.new_password != data.new_password_verify {
288       return Err(ApiError::err_plain("passwords_dont_match").into());
289     }
290
291     // Check the old password
292     let valid: bool = verify(
293       &data.old_password,
294       &local_user_view.local_user.password_encrypted,
295     )
296     .unwrap_or(false);
297     if !valid {
298       return Err(ApiError::err_plain("password_incorrect").into());
299     }
300
301     let local_user_id = local_user_view.local_user.id;
302     let new_password = data.new_password.to_owned();
303     let updated_local_user = blocking(context.pool(), move |conn| {
304       LocalUser::update_password(conn, local_user_id, &new_password)
305     })
306     .await??;
307
308     // Return the jwt
309     Ok(LoginResponse {
310       jwt: Claims::jwt(
311         updated_local_user.id.0,
312         &context.secret().jwt_secret,
313         &context.settings().hostname,
314       )?,
315     })
316   }
317 }
318
319 #[async_trait::async_trait(?Send)]
320 impl Perform for AddAdmin {
321   type Response = AddAdminResponse;
322
323   async fn perform(
324     &self,
325     context: &Data<LemmyContext>,
326     websocket_id: Option<ConnectionId>,
327   ) -> Result<AddAdminResponse, LemmyError> {
328     let data: &AddAdmin = self;
329     let local_user_view =
330       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
331
332     // Make sure user is an admin
333     is_admin(&local_user_view)?;
334
335     let added = data.added;
336     let added_person_id = data.person_id;
337     let added_admin = blocking(context.pool(), move |conn| {
338       Person::add_admin(conn, added_person_id, added)
339     })
340     .await?
341     .map_err(|e| ApiError::err("couldnt_update_user", e))?;
342
343     // Mod tables
344     let form = ModAddForm {
345       mod_person_id: local_user_view.person.id,
346       other_person_id: added_admin.id,
347       removed: Some(!data.added),
348     };
349
350     blocking(context.pool(), move |conn| ModAdd::create(conn, &form)).await??;
351
352     let site_creator_id = blocking(context.pool(), move |conn| {
353       Site::read(conn, 1).map(|s| s.creator_id)
354     })
355     .await??;
356
357     let mut admins = blocking(context.pool(), PersonViewSafe::admins).await??;
358     let creator_index = admins
359       .iter()
360       .position(|r| r.person.id == site_creator_id)
361       .context(location_info!())?;
362     let creator_person = admins.remove(creator_index);
363     admins.insert(0, creator_person);
364
365     let res = AddAdminResponse { admins };
366
367     context.chat_server().do_send(SendAllMessage {
368       op: UserOperation::AddAdmin,
369       response: res.clone(),
370       websocket_id,
371     });
372
373     Ok(res)
374   }
375 }
376
377 #[async_trait::async_trait(?Send)]
378 impl Perform for BanPerson {
379   type Response = BanPersonResponse;
380
381   async fn perform(
382     &self,
383     context: &Data<LemmyContext>,
384     websocket_id: Option<ConnectionId>,
385   ) -> Result<BanPersonResponse, LemmyError> {
386     let data: &BanPerson = self;
387     let local_user_view =
388       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
389
390     // Make sure user is an admin
391     is_admin(&local_user_view)?;
392
393     let ban = data.ban;
394     let banned_person_id = data.person_id;
395     let ban_person = move |conn: &'_ _| Person::ban_person(conn, banned_person_id, ban);
396     blocking(context.pool(), ban_person)
397       .await?
398       .map_err(|e| ApiError::err("couldnt_update_user", e))?;
399
400     // Remove their data if that's desired
401     if data.remove_data.unwrap_or(false) {
402       // Posts
403       blocking(context.pool(), move |conn: &'_ _| {
404         Post::update_removed_for_creator(conn, banned_person_id, None, true)
405       })
406       .await??;
407
408       // Communities
409       // Remove all communities where they're the top mod
410       // for now, remove the communities manually
411       let first_mod_communities = blocking(context.pool(), move |conn: &'_ _| {
412         CommunityModeratorView::get_community_first_mods(conn)
413       })
414       .await??;
415
416       // Filter to only this banned users top communities
417       let banned_user_first_communities: Vec<CommunityModeratorView> = first_mod_communities
418         .into_iter()
419         .filter(|fmc| fmc.moderator.id == banned_person_id)
420         .collect();
421
422       for first_mod_community in banned_user_first_communities {
423         blocking(context.pool(), move |conn: &'_ _| {
424           Community::update_removed(conn, first_mod_community.community.id, true)
425         })
426         .await??;
427       }
428
429       // Comments
430       blocking(context.pool(), move |conn: &'_ _| {
431         Comment::update_removed_for_creator(conn, banned_person_id, true)
432       })
433       .await??;
434     }
435
436     // Mod tables
437     let expires = data.expires.map(naive_from_unix);
438
439     let form = ModBanForm {
440       mod_person_id: local_user_view.person.id,
441       other_person_id: data.person_id,
442       reason: data.reason.to_owned(),
443       banned: Some(data.ban),
444       expires,
445     };
446
447     blocking(context.pool(), move |conn| ModBan::create(conn, &form)).await??;
448
449     let person_id = data.person_id;
450     let person_view = blocking(context.pool(), move |conn| {
451       PersonViewSafe::read(conn, person_id)
452     })
453     .await??;
454
455     let res = BanPersonResponse {
456       person_view,
457       banned: data.ban,
458     };
459
460     context.chat_server().do_send(SendAllMessage {
461       op: UserOperation::BanPerson,
462       response: res.clone(),
463       websocket_id,
464     });
465
466     Ok(res)
467   }
468 }
469
470 #[async_trait::async_trait(?Send)]
471 impl Perform for BlockPerson {
472   type Response = BlockPersonResponse;
473
474   async fn perform(
475     &self,
476     context: &Data<LemmyContext>,
477     _websocket_id: Option<ConnectionId>,
478   ) -> Result<BlockPersonResponse, LemmyError> {
479     let data: &BlockPerson = self;
480     let local_user_view =
481       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
482
483     let target_id = data.person_id;
484     let person_id = local_user_view.person.id;
485
486     // Don't let a person block themselves
487     if target_id == person_id {
488       return Err(ApiError::err_plain("cant_block_yourself").into());
489     }
490
491     let person_block_form = PersonBlockForm {
492       person_id,
493       target_id,
494     };
495
496     if data.block {
497       let block = move |conn: &'_ _| PersonBlock::block(conn, &person_block_form);
498       blocking(context.pool(), block)
499         .await?
500         .map_err(|e| ApiError::err("person_block_already_exists", e))?;
501     } else {
502       let unblock = move |conn: &'_ _| PersonBlock::unblock(conn, &person_block_form);
503       blocking(context.pool(), unblock)
504         .await?
505         .map_err(|e| ApiError::err("person_block_already_exists", e))?;
506     }
507
508     // TODO does any federated stuff need to be done here?
509
510     let person_view = blocking(context.pool(), move |conn| {
511       PersonViewSafe::read(conn, target_id)
512     })
513     .await??;
514
515     let res = BlockPersonResponse {
516       person_view,
517       blocked: data.block,
518     };
519
520     Ok(res)
521   }
522 }
523
524 #[async_trait::async_trait(?Send)]
525 impl Perform for GetReplies {
526   type Response = GetRepliesResponse;
527
528   async fn perform(
529     &self,
530     context: &Data<LemmyContext>,
531     _websocket_id: Option<ConnectionId>,
532   ) -> Result<GetRepliesResponse, LemmyError> {
533     let data: &GetReplies = self;
534     let local_user_view =
535       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
536
537     let sort: Option<SortType> = from_opt_str_to_opt_enum(&data.sort);
538
539     let page = data.page;
540     let limit = data.limit;
541     let unread_only = data.unread_only;
542     let person_id = local_user_view.person.id;
543     let show_bot_accounts = local_user_view.local_user.show_bot_accounts;
544
545     let replies = blocking(context.pool(), move |conn| {
546       CommentQueryBuilder::create(conn)
547         .sort(sort)
548         .unread_only(unread_only)
549         .recipient_id(person_id)
550         .show_bot_accounts(show_bot_accounts)
551         .my_person_id(person_id)
552         .page(page)
553         .limit(limit)
554         .list()
555     })
556     .await??;
557
558     Ok(GetRepliesResponse { replies })
559   }
560 }
561
562 #[async_trait::async_trait(?Send)]
563 impl Perform for GetPersonMentions {
564   type Response = GetPersonMentionsResponse;
565
566   async fn perform(
567     &self,
568     context: &Data<LemmyContext>,
569     _websocket_id: Option<ConnectionId>,
570   ) -> Result<GetPersonMentionsResponse, LemmyError> {
571     let data: &GetPersonMentions = self;
572     let local_user_view =
573       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
574
575     let sort: Option<SortType> = from_opt_str_to_opt_enum(&data.sort);
576
577     let page = data.page;
578     let limit = data.limit;
579     let unread_only = data.unread_only;
580     let person_id = local_user_view.person.id;
581     let mentions = blocking(context.pool(), move |conn| {
582       PersonMentionQueryBuilder::create(conn)
583         .recipient_id(person_id)
584         .my_person_id(person_id)
585         .sort(sort)
586         .unread_only(unread_only)
587         .page(page)
588         .limit(limit)
589         .list()
590     })
591     .await??;
592
593     Ok(GetPersonMentionsResponse { mentions })
594   }
595 }
596
597 #[async_trait::async_trait(?Send)]
598 impl Perform for MarkPersonMentionAsRead {
599   type Response = PersonMentionResponse;
600
601   async fn perform(
602     &self,
603     context: &Data<LemmyContext>,
604     _websocket_id: Option<ConnectionId>,
605   ) -> Result<PersonMentionResponse, LemmyError> {
606     let data: &MarkPersonMentionAsRead = self;
607     let local_user_view =
608       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
609
610     let person_mention_id = data.person_mention_id;
611     let read_person_mention = blocking(context.pool(), move |conn| {
612       PersonMention::read(conn, person_mention_id)
613     })
614     .await??;
615
616     if local_user_view.person.id != read_person_mention.recipient_id {
617       return Err(ApiError::err_plain("couldnt_update_comment").into());
618     }
619
620     let person_mention_id = read_person_mention.id;
621     let read = data.read;
622     let update_mention =
623       move |conn: &'_ _| PersonMention::update_read(conn, person_mention_id, read);
624     blocking(context.pool(), update_mention)
625       .await?
626       .map_err(|e| ApiError::err("couldnt_update_comment", e))?;
627
628     let person_mention_id = read_person_mention.id;
629     let person_id = local_user_view.person.id;
630     let person_mention_view = blocking(context.pool(), move |conn| {
631       PersonMentionView::read(conn, person_mention_id, Some(person_id))
632     })
633     .await??;
634
635     Ok(PersonMentionResponse {
636       person_mention_view,
637     })
638   }
639 }
640
641 #[async_trait::async_trait(?Send)]
642 impl Perform for MarkAllAsRead {
643   type Response = GetRepliesResponse;
644
645   async fn perform(
646     &self,
647     context: &Data<LemmyContext>,
648     _websocket_id: Option<ConnectionId>,
649   ) -> Result<GetRepliesResponse, LemmyError> {
650     let data: &MarkAllAsRead = self;
651     let local_user_view =
652       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
653
654     let person_id = local_user_view.person.id;
655     let replies = blocking(context.pool(), move |conn| {
656       CommentQueryBuilder::create(conn)
657         .my_person_id(person_id)
658         .recipient_id(person_id)
659         .unread_only(true)
660         .page(1)
661         .limit(999)
662         .list()
663     })
664     .await??;
665
666     // TODO: this should probably be a bulk operation
667     // Not easy to do as a bulk operation,
668     // because recipient_id isn't in the comment table
669     for comment_view in &replies {
670       let reply_id = comment_view.comment.id;
671       let mark_as_read = move |conn: &'_ _| Comment::update_read(conn, reply_id, true);
672       blocking(context.pool(), mark_as_read)
673         .await?
674         .map_err(|e| ApiError::err("couldnt_update_comment", e))?;
675     }
676
677     // Mark all user mentions as read
678     let update_person_mentions =
679       move |conn: &'_ _| PersonMention::mark_all_as_read(conn, person_id);
680     blocking(context.pool(), update_person_mentions)
681       .await?
682       .map_err(|e| ApiError::err("couldnt_update_comment", e))?;
683
684     // Mark all private_messages as read
685     let update_pm = move |conn: &'_ _| PrivateMessage::mark_all_as_read(conn, person_id);
686     blocking(context.pool(), update_pm)
687       .await?
688       .map_err(|e| ApiError::err("couldnt_update_private_message", e))?;
689
690     Ok(GetRepliesResponse { replies: vec![] })
691   }
692 }
693
694 #[async_trait::async_trait(?Send)]
695 impl Perform for PasswordReset {
696   type Response = PasswordResetResponse;
697
698   async fn perform(
699     &self,
700     context: &Data<LemmyContext>,
701     _websocket_id: Option<ConnectionId>,
702   ) -> Result<PasswordResetResponse, LemmyError> {
703     let data: &PasswordReset = self;
704
705     // Fetch that email
706     let email = data.email.clone();
707     let local_user_view = blocking(context.pool(), move |conn| {
708       LocalUserView::find_by_email(conn, &email)
709     })
710     .await?
711     .map_err(|e| ApiError::err("couldnt_find_that_username_or_email", e))?;
712
713     // Generate a random token
714     let token = generate_random_string();
715
716     // Insert the row
717     let token2 = token.clone();
718     let local_user_id = local_user_view.local_user.id;
719     blocking(context.pool(), move |conn| {
720       PasswordResetRequest::create_token(conn, local_user_id, &token2)
721     })
722     .await??;
723
724     // Email the pure token to the user.
725     // TODO no i18n support here.
726     let email = &local_user_view.local_user.email.expect("email");
727     let subject = &format!("Password reset for {}", local_user_view.person.name);
728     let protocol_and_hostname = &context.settings().get_protocol_and_hostname();
729     let html = &format!("<h1>Password Reset Request for {}</h1><br><a href={}/password_change/{}>Click here to reset your password</a>", local_user_view.person.name, protocol_and_hostname, &token);
730     send_email(
731       subject,
732       email,
733       &local_user_view.person.name,
734       html,
735       &context.settings(),
736     )
737     .map_err(|e| ApiError::err("email_send_failed", e))?;
738
739     Ok(PasswordResetResponse {})
740   }
741 }
742
743 #[async_trait::async_trait(?Send)]
744 impl Perform for PasswordChange {
745   type Response = LoginResponse;
746
747   async fn perform(
748     &self,
749     context: &Data<LemmyContext>,
750     _websocket_id: Option<ConnectionId>,
751   ) -> Result<LoginResponse, LemmyError> {
752     let data: &PasswordChange = self;
753
754     // Fetch the user_id from the token
755     let token = data.token.clone();
756     let local_user_id = blocking(context.pool(), move |conn| {
757       PasswordResetRequest::read_from_token(conn, &token).map(|p| p.local_user_id)
758     })
759     .await??;
760
761     password_length_check(&data.password)?;
762
763     // Make sure passwords match
764     if data.password != data.password_verify {
765       return Err(ApiError::err_plain("passwords_dont_match").into());
766     }
767
768     // Update the user with the new password
769     let password = data.password.clone();
770     let updated_local_user = blocking(context.pool(), move |conn| {
771       LocalUser::update_password(conn, local_user_id, &password)
772     })
773     .await?
774     .map_err(|e| ApiError::err("couldnt_update_user", e))?;
775
776     // Return the jwt
777     Ok(LoginResponse {
778       jwt: Claims::jwt(
779         updated_local_user.id.0,
780         &context.secret().jwt_secret,
781         &context.settings().hostname,
782       )?,
783     })
784   }
785 }
786
787 #[async_trait::async_trait(?Send)]
788 impl Perform for GetReportCount {
789   type Response = GetReportCountResponse;
790
791   async fn perform(
792     &self,
793     context: &Data<LemmyContext>,
794     _websocket_id: Option<ConnectionId>,
795   ) -> Result<GetReportCountResponse, LemmyError> {
796     let data: &GetReportCount = self;
797     let local_user_view =
798       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
799
800     let person_id = local_user_view.person.id;
801     let admin = local_user_view.person.admin;
802     let community_id = data.community_id;
803
804     let comment_reports = blocking(context.pool(), move |conn| {
805       CommentReportView::get_report_count(conn, person_id, admin, community_id)
806     })
807     .await??;
808
809     let post_reports = blocking(context.pool(), move |conn| {
810       PostReportView::get_report_count(conn, person_id, admin, community_id)
811     })
812     .await??;
813
814     let res = GetReportCountResponse {
815       community_id,
816       comment_reports,
817       post_reports,
818     };
819
820     Ok(res)
821   }
822 }
823
824 #[async_trait::async_trait(?Send)]
825 impl Perform for GetUnreadCount {
826   type Response = GetUnreadCountResponse;
827
828   async fn perform(
829     &self,
830     context: &Data<LemmyContext>,
831     _websocket_id: Option<ConnectionId>,
832   ) -> Result<Self::Response, LemmyError> {
833     let data = self;
834     let local_user_view =
835       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
836
837     let person_id = local_user_view.person.id;
838
839     let replies = blocking(context.pool(), move |conn| {
840       CommentView::get_unread_replies(conn, person_id)
841     })
842     .await??;
843
844     let mentions = blocking(context.pool(), move |conn| {
845       PersonMentionView::get_unread_mentions(conn, person_id)
846     })
847     .await??;
848
849     let private_messages = blocking(context.pool(), move |conn| {
850       PrivateMessageView::get_unread_messages(conn, person_id)
851     })
852     .await??;
853
854     let res = Self::Response {
855       replies,
856       mentions,
857       private_messages,
858     };
859
860     Ok(res)
861   }
862 }