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