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