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