]> Untitled Git - lemmy.git/blob - crates/api/src/local_user.rs
727dd1a5c14c15671d5eb9a838ec17aca06b6c05
[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
195     let person_form = PersonForm {
196       name: local_user_view.person.name,
197       avatar,
198       banner,
199       inbox_url: None,
200       display_name,
201       published: None,
202       updated: Some(naive_now()),
203       banned: None,
204       deleted: None,
205       actor_id: None,
206       bio,
207       local: None,
208       admin: None,
209       private_key: None,
210       public_key: None,
211       last_refreshed_at: None,
212       shared_inbox_url: None,
213       matrix_user_id,
214       bot_account,
215     };
216
217     blocking(context.pool(), move |conn| {
218       Person::update(conn, person_id, &person_form)
219     })
220     .await?
221     .map_err(|e| ApiError::err("user_already_exists", e))?;
222
223     let local_user_form = LocalUserForm {
224       person_id,
225       email,
226       password_encrypted,
227       show_nsfw: data.show_nsfw,
228       show_bot_accounts: data.show_bot_accounts,
229       show_scores: data.show_scores,
230       theme: data.theme.to_owned(),
231       default_sort_type,
232       default_listing_type,
233       lang: data.lang.to_owned(),
234       show_avatars: data.show_avatars,
235       show_read_posts: data.show_read_posts,
236       show_new_post_notifs: data.show_new_post_notifs,
237       send_notifications_to_email: data.send_notifications_to_email,
238     };
239
240     let local_user_res = blocking(context.pool(), move |conn| {
241       LocalUser::update(conn, local_user_id, &local_user_form)
242     })
243     .await?;
244     let updated_local_user = match local_user_res {
245       Ok(u) => u,
246       Err(e) => {
247         let err_type = if e.to_string()
248           == "duplicate key value violates unique constraint \"local_user_email_key\""
249         {
250           "email_already_exists"
251         } else {
252           "user_already_exists"
253         };
254
255         return Err(ApiError::err(err_type, e).into());
256       }
257     };
258
259     // Return the jwt
260     Ok(LoginResponse {
261       jwt: Claims::jwt(
262         updated_local_user.id.0,
263         &context.secret().jwt_secret,
264         &context.settings().hostname,
265       )?,
266     })
267   }
268 }
269
270 #[async_trait::async_trait(?Send)]
271 impl Perform for ChangePassword {
272   type Response = LoginResponse;
273
274   async fn perform(
275     &self,
276     context: &Data<LemmyContext>,
277     _websocket_id: Option<ConnectionId>,
278   ) -> Result<LoginResponse, LemmyError> {
279     let data: &ChangePassword = self;
280     let local_user_view =
281       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
282
283     password_length_check(&data.new_password)?;
284
285     // Make sure passwords match
286     if data.new_password != data.new_password_verify {
287       return Err(ApiError::err_plain("passwords_dont_match").into());
288     }
289
290     // Check the old password
291     let valid: bool = verify(
292       &data.old_password,
293       &local_user_view.local_user.password_encrypted,
294     )
295     .unwrap_or(false);
296     if !valid {
297       return Err(ApiError::err_plain("password_incorrect").into());
298     }
299
300     let local_user_id = local_user_view.local_user.id;
301     let new_password = data.new_password.to_owned();
302     let updated_local_user = blocking(context.pool(), move |conn| {
303       LocalUser::update_password(conn, local_user_id, &new_password)
304     })
305     .await??;
306
307     // Return the jwt
308     Ok(LoginResponse {
309       jwt: Claims::jwt(
310         updated_local_user.id.0,
311         &context.secret().jwt_secret,
312         &context.settings().hostname,
313       )?,
314     })
315   }
316 }
317
318 #[async_trait::async_trait(?Send)]
319 impl Perform for AddAdmin {
320   type Response = AddAdminResponse;
321
322   async fn perform(
323     &self,
324     context: &Data<LemmyContext>,
325     websocket_id: Option<ConnectionId>,
326   ) -> Result<AddAdminResponse, LemmyError> {
327     let data: &AddAdmin = self;
328     let local_user_view =
329       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
330
331     // Make sure user is an admin
332     is_admin(&local_user_view)?;
333
334     let added = data.added;
335     let added_person_id = data.person_id;
336     let added_admin = blocking(context.pool(), move |conn| {
337       Person::add_admin(conn, added_person_id, added)
338     })
339     .await?
340     .map_err(|e| ApiError::err("couldnt_update_user", e))?;
341
342     // Mod tables
343     let form = ModAddForm {
344       mod_person_id: local_user_view.person.id,
345       other_person_id: added_admin.id,
346       removed: Some(!data.added),
347     };
348
349     blocking(context.pool(), move |conn| ModAdd::create(conn, &form)).await??;
350
351     let site_creator_id = blocking(context.pool(), move |conn| {
352       Site::read(conn, 1).map(|s| s.creator_id)
353     })
354     .await??;
355
356     let mut admins = blocking(context.pool(), PersonViewSafe::admins).await??;
357     let creator_index = admins
358       .iter()
359       .position(|r| r.person.id == site_creator_id)
360       .context(location_info!())?;
361     let creator_person = admins.remove(creator_index);
362     admins.insert(0, creator_person);
363
364     let res = AddAdminResponse { admins };
365
366     context.chat_server().do_send(SendAllMessage {
367       op: UserOperation::AddAdmin,
368       response: res.clone(),
369       websocket_id,
370     });
371
372     Ok(res)
373   }
374 }
375
376 #[async_trait::async_trait(?Send)]
377 impl Perform for BanPerson {
378   type Response = BanPersonResponse;
379
380   async fn perform(
381     &self,
382     context: &Data<LemmyContext>,
383     websocket_id: Option<ConnectionId>,
384   ) -> Result<BanPersonResponse, LemmyError> {
385     let data: &BanPerson = self;
386     let local_user_view =
387       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
388
389     // Make sure user is an admin
390     is_admin(&local_user_view)?;
391
392     let ban = data.ban;
393     let banned_person_id = data.person_id;
394     let ban_person = move |conn: &'_ _| Person::ban_person(conn, banned_person_id, ban);
395     blocking(context.pool(), ban_person)
396       .await?
397       .map_err(|e| ApiError::err("couldnt_update_user", e))?;
398
399     // Remove their data if that's desired
400     if data.remove_data.unwrap_or(false) {
401       // Posts
402       blocking(context.pool(), move |conn: &'_ _| {
403         Post::update_removed_for_creator(conn, banned_person_id, None, true)
404       })
405       .await??;
406
407       // Communities
408       // Remove all communities where they're the top mod
409       // for now, remove the communities manually
410       let first_mod_communities = blocking(context.pool(), move |conn: &'_ _| {
411         CommunityModeratorView::get_community_first_mods(conn)
412       })
413       .await??;
414
415       // Filter to only this banned users top communities
416       let banned_user_first_communities: Vec<CommunityModeratorView> = first_mod_communities
417         .into_iter()
418         .filter(|fmc| fmc.moderator.id == banned_person_id)
419         .collect();
420
421       for first_mod_community in banned_user_first_communities {
422         blocking(context.pool(), move |conn: &'_ _| {
423           Community::update_removed(conn, first_mod_community.community.id, true)
424         })
425         .await??;
426       }
427
428       // Comments
429       blocking(context.pool(), move |conn: &'_ _| {
430         Comment::update_removed_for_creator(conn, banned_person_id, true)
431       })
432       .await??;
433     }
434
435     // Mod tables
436     let expires = data.expires.map(naive_from_unix);
437
438     let form = ModBanForm {
439       mod_person_id: local_user_view.person.id,
440       other_person_id: data.person_id,
441       reason: data.reason.to_owned(),
442       banned: Some(data.ban),
443       expires,
444     };
445
446     blocking(context.pool(), move |conn| ModBan::create(conn, &form)).await??;
447
448     let person_id = data.person_id;
449     let person_view = blocking(context.pool(), move |conn| {
450       PersonViewSafe::read(conn, person_id)
451     })
452     .await??;
453
454     let res = BanPersonResponse {
455       person_view,
456       banned: data.ban,
457     };
458
459     context.chat_server().do_send(SendAllMessage {
460       op: UserOperation::BanPerson,
461       response: res.clone(),
462       websocket_id,
463     });
464
465     Ok(res)
466   }
467 }
468
469 #[async_trait::async_trait(?Send)]
470 impl Perform for BlockPerson {
471   type Response = BlockPersonResponse;
472
473   async fn perform(
474     &self,
475     context: &Data<LemmyContext>,
476     _websocket_id: Option<ConnectionId>,
477   ) -> Result<BlockPersonResponse, LemmyError> {
478     let data: &BlockPerson = self;
479     let local_user_view =
480       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
481
482     let target_id = data.person_id;
483     let person_id = local_user_view.person.id;
484
485     // Don't let a person block themselves
486     if target_id == person_id {
487       return Err(ApiError::err_plain("cant_block_yourself").into());
488     }
489
490     let person_block_form = PersonBlockForm {
491       person_id,
492       target_id,
493     };
494
495     if data.block {
496       let block = move |conn: &'_ _| PersonBlock::block(conn, &person_block_form);
497       blocking(context.pool(), block)
498         .await?
499         .map_err(|e| ApiError::err("person_block_already_exists", e))?;
500     } else {
501       let unblock = move |conn: &'_ _| PersonBlock::unblock(conn, &person_block_form);
502       blocking(context.pool(), unblock)
503         .await?
504         .map_err(|e| ApiError::err("person_block_already_exists", e))?;
505     }
506
507     // TODO does any federated stuff need to be done here?
508
509     let person_view = blocking(context.pool(), move |conn| {
510       PersonViewSafe::read(conn, target_id)
511     })
512     .await??;
513
514     let res = BlockPersonResponse {
515       person_view,
516       blocked: data.block,
517     };
518
519     Ok(res)
520   }
521 }
522
523 #[async_trait::async_trait(?Send)]
524 impl Perform for GetReplies {
525   type Response = GetRepliesResponse;
526
527   async fn perform(
528     &self,
529     context: &Data<LemmyContext>,
530     _websocket_id: Option<ConnectionId>,
531   ) -> Result<GetRepliesResponse, LemmyError> {
532     let data: &GetReplies = self;
533     let local_user_view =
534       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
535
536     let sort: Option<SortType> = from_opt_str_to_opt_enum(&data.sort);
537
538     let page = data.page;
539     let limit = data.limit;
540     let unread_only = data.unread_only;
541     let person_id = local_user_view.person.id;
542     let show_bot_accounts = local_user_view.local_user.show_bot_accounts;
543
544     let replies = blocking(context.pool(), move |conn| {
545       CommentQueryBuilder::create(conn)
546         .sort(sort)
547         .unread_only(unread_only)
548         .recipient_id(person_id)
549         .show_bot_accounts(show_bot_accounts)
550         .my_person_id(person_id)
551         .page(page)
552         .limit(limit)
553         .list()
554     })
555     .await??;
556
557     Ok(GetRepliesResponse { replies })
558   }
559 }
560
561 #[async_trait::async_trait(?Send)]
562 impl Perform for GetPersonMentions {
563   type Response = GetPersonMentionsResponse;
564
565   async fn perform(
566     &self,
567     context: &Data<LemmyContext>,
568     _websocket_id: Option<ConnectionId>,
569   ) -> Result<GetPersonMentionsResponse, LemmyError> {
570     let data: &GetPersonMentions = self;
571     let local_user_view =
572       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
573
574     let sort: Option<SortType> = from_opt_str_to_opt_enum(&data.sort);
575
576     let page = data.page;
577     let limit = data.limit;
578     let unread_only = data.unread_only;
579     let person_id = local_user_view.person.id;
580     let mentions = blocking(context.pool(), move |conn| {
581       PersonMentionQueryBuilder::create(conn)
582         .recipient_id(person_id)
583         .my_person_id(person_id)
584         .sort(sort)
585         .unread_only(unread_only)
586         .page(page)
587         .limit(limit)
588         .list()
589     })
590     .await??;
591
592     Ok(GetPersonMentionsResponse { mentions })
593   }
594 }
595
596 #[async_trait::async_trait(?Send)]
597 impl Perform for MarkPersonMentionAsRead {
598   type Response = PersonMentionResponse;
599
600   async fn perform(
601     &self,
602     context: &Data<LemmyContext>,
603     _websocket_id: Option<ConnectionId>,
604   ) -> Result<PersonMentionResponse, LemmyError> {
605     let data: &MarkPersonMentionAsRead = self;
606     let local_user_view =
607       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
608
609     let person_mention_id = data.person_mention_id;
610     let read_person_mention = blocking(context.pool(), move |conn| {
611       PersonMention::read(conn, person_mention_id)
612     })
613     .await??;
614
615     if local_user_view.person.id != read_person_mention.recipient_id {
616       return Err(ApiError::err_plain("couldnt_update_comment").into());
617     }
618
619     let person_mention_id = read_person_mention.id;
620     let read = data.read;
621     let update_mention =
622       move |conn: &'_ _| PersonMention::update_read(conn, person_mention_id, read);
623     blocking(context.pool(), update_mention)
624       .await?
625       .map_err(|e| ApiError::err("couldnt_update_comment", e))?;
626
627     let person_mention_id = read_person_mention.id;
628     let person_id = local_user_view.person.id;
629     let person_mention_view = blocking(context.pool(), move |conn| {
630       PersonMentionView::read(conn, person_mention_id, Some(person_id))
631     })
632     .await??;
633
634     Ok(PersonMentionResponse {
635       person_mention_view,
636     })
637   }
638 }
639
640 #[async_trait::async_trait(?Send)]
641 impl Perform for MarkAllAsRead {
642   type Response = GetRepliesResponse;
643
644   async fn perform(
645     &self,
646     context: &Data<LemmyContext>,
647     _websocket_id: Option<ConnectionId>,
648   ) -> Result<GetRepliesResponse, LemmyError> {
649     let data: &MarkAllAsRead = self;
650     let local_user_view =
651       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
652
653     let person_id = local_user_view.person.id;
654     let replies = blocking(context.pool(), move |conn| {
655       CommentQueryBuilder::create(conn)
656         .my_person_id(person_id)
657         .recipient_id(person_id)
658         .unread_only(true)
659         .page(1)
660         .limit(999)
661         .list()
662     })
663     .await??;
664
665     // TODO: this should probably be a bulk operation
666     // Not easy to do as a bulk operation,
667     // because recipient_id isn't in the comment table
668     for comment_view in &replies {
669       let reply_id = comment_view.comment.id;
670       let mark_as_read = move |conn: &'_ _| Comment::update_read(conn, reply_id, true);
671       blocking(context.pool(), mark_as_read)
672         .await?
673         .map_err(|e| ApiError::err("couldnt_update_comment", e))?;
674     }
675
676     // Mark all user mentions as read
677     let update_person_mentions =
678       move |conn: &'_ _| PersonMention::mark_all_as_read(conn, person_id);
679     blocking(context.pool(), update_person_mentions)
680       .await?
681       .map_err(|e| ApiError::err("couldnt_update_comment", e))?;
682
683     // Mark all private_messages as read
684     let update_pm = move |conn: &'_ _| PrivateMessage::mark_all_as_read(conn, person_id);
685     blocking(context.pool(), update_pm)
686       .await?
687       .map_err(|e| ApiError::err("couldnt_update_private_message", e))?;
688
689     Ok(GetRepliesResponse { replies: vec![] })
690   }
691 }
692
693 #[async_trait::async_trait(?Send)]
694 impl Perform for PasswordReset {
695   type Response = PasswordResetResponse;
696
697   async fn perform(
698     &self,
699     context: &Data<LemmyContext>,
700     _websocket_id: Option<ConnectionId>,
701   ) -> Result<PasswordResetResponse, LemmyError> {
702     let data: &PasswordReset = self;
703
704     // Fetch that email
705     let email = data.email.clone();
706     let local_user_view = blocking(context.pool(), move |conn| {
707       LocalUserView::find_by_email(conn, &email)
708     })
709     .await?
710     .map_err(|e| ApiError::err("couldnt_find_that_username_or_email", e))?;
711
712     // Generate a random token
713     let token = generate_random_string();
714
715     // Insert the row
716     let token2 = token.clone();
717     let local_user_id = local_user_view.local_user.id;
718     blocking(context.pool(), move |conn| {
719       PasswordResetRequest::create_token(conn, local_user_id, &token2)
720     })
721     .await??;
722
723     // Email the pure token to the user.
724     // TODO no i18n support here.
725     let email = &local_user_view.local_user.email.expect("email");
726     let subject = &format!("Password reset for {}", local_user_view.person.name);
727     let protocol_and_hostname = &context.settings().get_protocol_and_hostname();
728     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);
729     send_email(
730       subject,
731       email,
732       &local_user_view.person.name,
733       html,
734       &context.settings(),
735     )
736     .map_err(|e| ApiError::err("email_send_failed", e))?;
737
738     Ok(PasswordResetResponse {})
739   }
740 }
741
742 #[async_trait::async_trait(?Send)]
743 impl Perform for PasswordChange {
744   type Response = LoginResponse;
745
746   async fn perform(
747     &self,
748     context: &Data<LemmyContext>,
749     _websocket_id: Option<ConnectionId>,
750   ) -> Result<LoginResponse, LemmyError> {
751     let data: &PasswordChange = self;
752
753     // Fetch the user_id from the token
754     let token = data.token.clone();
755     let local_user_id = blocking(context.pool(), move |conn| {
756       PasswordResetRequest::read_from_token(conn, &token).map(|p| p.local_user_id)
757     })
758     .await??;
759
760     password_length_check(&data.password)?;
761
762     // Make sure passwords match
763     if data.password != data.password_verify {
764       return Err(ApiError::err_plain("passwords_dont_match").into());
765     }
766
767     // Update the user with the new password
768     let password = data.password.clone();
769     let updated_local_user = blocking(context.pool(), move |conn| {
770       LocalUser::update_password(conn, local_user_id, &password)
771     })
772     .await?
773     .map_err(|e| ApiError::err("couldnt_update_user", e))?;
774
775     // Return the jwt
776     Ok(LoginResponse {
777       jwt: Claims::jwt(
778         updated_local_user.id.0,
779         &context.secret().jwt_secret,
780         &context.settings().hostname,
781       )?,
782     })
783   }
784 }
785
786 #[async_trait::async_trait(?Send)]
787 impl Perform for GetReportCount {
788   type Response = GetReportCountResponse;
789
790   async fn perform(
791     &self,
792     context: &Data<LemmyContext>,
793     _websocket_id: Option<ConnectionId>,
794   ) -> Result<GetReportCountResponse, LemmyError> {
795     let data: &GetReportCount = self;
796     let local_user_view =
797       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
798
799     let person_id = local_user_view.person.id;
800     let admin = local_user_view.person.admin;
801     let community_id = data.community_id;
802
803     let comment_reports = blocking(context.pool(), move |conn| {
804       CommentReportView::get_report_count(conn, person_id, admin, community_id)
805     })
806     .await??;
807
808     let post_reports = blocking(context.pool(), move |conn| {
809       PostReportView::get_report_count(conn, person_id, admin, community_id)
810     })
811     .await??;
812
813     let res = GetReportCountResponse {
814       community_id,
815       comment_reports,
816       post_reports,
817     };
818
819     Ok(res)
820   }
821 }
822
823 #[async_trait::async_trait(?Send)]
824 impl Perform for GetUnreadCount {
825   type Response = GetUnreadCountResponse;
826
827   async fn perform(
828     &self,
829     context: &Data<LemmyContext>,
830     _websocket_id: Option<ConnectionId>,
831   ) -> Result<Self::Response, LemmyError> {
832     let data = self;
833     let local_user_view =
834       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
835
836     let person_id = local_user_view.person.id;
837
838     let replies = blocking(context.pool(), move |conn| {
839       CommentView::get_unread_replies(conn, person_id)
840     })
841     .await??;
842
843     let mentions = blocking(context.pool(), move |conn| {
844       PersonMentionView::get_unread_mentions(conn, person_id)
845     })
846     .await??;
847
848     let private_messages = blocking(context.pool(), move |conn| {
849       PrivateMessageView::get_unread_messages(conn, person_id)
850     })
851     .await??;
852
853     let res = Self::Response {
854       replies,
855       mentions,
856       private_messages,
857     };
858
859     Ok(res)
860   }
861 }