1 use crate::{captcha_as_wav_base64, Perform};
2 use actix_web::web::Data;
4 use captcha::{gen, Difficulty};
6 use lemmy_api_common::{
8 check_registration_application,
9 get_local_user_view_from_jwt,
11 password_length_check,
14 send_email_verification_success,
15 send_password_reset_email,
16 send_verification_email,
19 activities::block::SiteOrCommunity,
20 protocol::activities::block::{block_user::BlockUser, undo_block_user::UndoBlockUser},
22 use lemmy_db_schema::{
23 diesel_option_overwrite,
24 diesel_option_overwrite_to_url,
25 from_opt_str_to_opt_enum,
29 email_verification::EmailVerification,
30 local_user::{LocalUser, LocalUserForm},
32 password_reset_request::*,
34 person_block::{PersonBlock, PersonBlockForm},
36 private_message::PrivateMessage,
39 traits::{Blockable, Crud},
43 comment_report_view::CommentReportView,
44 comment_view::{CommentQueryBuilder, CommentView},
45 local_user_view::LocalUserView,
46 post_report_view::PostReportView,
47 private_message_view::PrivateMessageView,
49 use lemmy_db_views_actor::{
50 person_mention_view::{PersonMentionQueryBuilder, PersonMentionView},
51 person_view::PersonViewSafe,
55 utils::{is_valid_display_name, is_valid_matrix_id, naive_from_unix},
59 use lemmy_websocket::{
60 messages::{CaptchaItem, SendAllMessage},
65 #[async_trait::async_trait(?Send)]
66 impl Perform for Login {
67 type Response = LoginResponse;
69 #[tracing::instrument(skip(context, _websocket_id))]
72 context: &Data<LemmyContext>,
73 _websocket_id: Option<ConnectionId>,
74 ) -> Result<LoginResponse, LemmyError> {
75 let data: &Login = self;
77 // Fetch that username / email
78 let username_or_email = data.username_or_email.clone();
79 let local_user_view = blocking(context.pool(), move |conn| {
80 LocalUserView::find_by_email_or_name(conn, &username_or_email)
83 .map_err(LemmyError::from)
84 .map_err(|e| e.with_message("couldnt_find_that_username_or_email"))?;
86 // Verify the password
87 let valid: bool = verify(
89 &local_user_view.local_user.password_encrypted,
93 return Err(LemmyError::from_message("password_incorrect"));
96 let site = blocking(context.pool(), Site::read_local_site).await??;
97 if site.require_email_verification && !local_user_view.local_user.email_verified {
98 return Err(LemmyError::from_message("email_not_verified"));
101 check_registration_application(&site, &local_user_view, context.pool()).await?;
107 local_user_view.local_user.id.0,
108 &context.secret().jwt_secret,
109 &context.settings().hostname,
113 verify_email_sent: false,
114 registration_created: false,
119 #[async_trait::async_trait(?Send)]
120 impl Perform for GetCaptcha {
121 type Response = GetCaptchaResponse;
123 #[tracing::instrument(skip(context, _websocket_id))]
126 context: &Data<LemmyContext>,
127 _websocket_id: Option<ConnectionId>,
128 ) -> Result<Self::Response, LemmyError> {
129 let captcha_settings = context.settings().captcha;
131 if !captcha_settings.enabled {
132 return Ok(GetCaptchaResponse { ok: None });
135 let captcha = match captcha_settings.difficulty.as_str() {
136 "easy" => gen(Difficulty::Easy),
137 "medium" => gen(Difficulty::Medium),
138 "hard" => gen(Difficulty::Hard),
139 _ => gen(Difficulty::Medium),
142 let answer = captcha.chars_as_string();
144 let png = captcha.as_base64().expect("failed to generate captcha");
146 let uuid = uuid::Uuid::new_v4().to_string();
148 let wav = captcha_as_wav_base64(&captcha);
150 let captcha_item = CaptchaItem {
152 uuid: uuid.to_owned(),
153 expires: naive_now() + Duration::minutes(10), // expires in 10 minutes
156 // Stores the captcha item on the queue
157 context.chat_server().do_send(captcha_item);
159 Ok(GetCaptchaResponse {
160 ok: Some(CaptchaResponse { png, wav, uuid }),
165 #[async_trait::async_trait(?Send)]
166 impl Perform for SaveUserSettings {
167 type Response = LoginResponse;
169 #[tracing::instrument(skip(context, _websocket_id))]
172 context: &Data<LemmyContext>,
173 _websocket_id: Option<ConnectionId>,
174 ) -> Result<LoginResponse, LemmyError> {
175 let data: &SaveUserSettings = self;
176 let local_user_view =
177 get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
179 let avatar = diesel_option_overwrite_to_url(&data.avatar)?;
180 let banner = diesel_option_overwrite_to_url(&data.banner)?;
181 let bio = diesel_option_overwrite(&data.bio);
182 let display_name = diesel_option_overwrite(&data.display_name);
183 let matrix_user_id = diesel_option_overwrite(&data.matrix_user_id);
184 let bot_account = data.bot_account;
185 let email_deref = data.email.as_deref().map(|e| e.to_owned());
186 let email = diesel_option_overwrite(&email_deref);
188 if let Some(Some(email)) = &email {
189 let previous_email = local_user_view.local_user.email.unwrap_or_default();
190 // Only send the verification email if there was an email change
191 if previous_email.ne(email) {
192 send_verification_email(
193 local_user_view.local_user.id,
195 &local_user_view.person.name,
203 // When the site requires email, make sure email is not Some(None). IE, an overwrite to a None value
204 if let Some(email) = &email {
205 let site_fut = blocking(context.pool(), Site::read_local_site);
206 if email.is_none() && site_fut.await??.require_email_verification {
207 return Err(LemmyError::from_message("email_required"));
211 if let Some(Some(bio)) = &bio {
212 if bio.chars().count() > 300 {
213 return Err(LemmyError::from_message("bio_length_overflow"));
217 if let Some(Some(display_name)) = &display_name {
218 if !is_valid_display_name(
220 context.settings().actor_name_max_length,
222 return Err(LemmyError::from_message("invalid_username"));
226 if let Some(Some(matrix_user_id)) = &matrix_user_id {
227 if !is_valid_matrix_id(matrix_user_id) {
228 return Err(LemmyError::from_message("invalid_matrix_id"));
232 let local_user_id = local_user_view.local_user.id;
233 let person_id = local_user_view.person.id;
234 let default_listing_type = data.default_listing_type;
235 let default_sort_type = data.default_sort_type;
236 let password_encrypted = local_user_view.local_user.password_encrypted;
237 let public_key = local_user_view.person.public_key;
239 let person_form = PersonForm {
240 name: local_user_view.person.name,
246 updated: Some(naive_now()),
255 last_refreshed_at: None,
256 shared_inbox_url: None,
262 blocking(context.pool(), move |conn| {
263 Person::update(conn, person_id, &person_form)
266 .map_err(LemmyError::from)
267 .map_err(|e| e.with_message("user_already_exists"))?;
269 let local_user_form = LocalUserForm {
270 person_id: Some(person_id),
272 password_encrypted: Some(password_encrypted),
273 show_nsfw: data.show_nsfw,
274 show_bot_accounts: data.show_bot_accounts,
275 show_scores: data.show_scores,
276 theme: data.theme.to_owned(),
278 default_listing_type,
279 lang: data.lang.to_owned(),
280 show_avatars: data.show_avatars,
281 show_read_posts: data.show_read_posts,
282 show_new_post_notifs: data.show_new_post_notifs,
283 send_notifications_to_email: data.send_notifications_to_email,
284 email_verified: None,
285 accepted_application: None,
288 let local_user_res = blocking(context.pool(), move |conn| {
289 LocalUser::update(conn, local_user_id, &local_user_form)
292 let updated_local_user = match local_user_res {
295 let err_type = if e.to_string()
296 == "duplicate key value violates unique constraint \"local_user_email_key\""
298 "email_already_exists"
300 "user_already_exists"
303 return Err(LemmyError::from(e).with_message(err_type));
311 updated_local_user.id.0,
312 &context.secret().jwt_secret,
313 &context.settings().hostname,
317 verify_email_sent: false,
318 registration_created: false,
323 #[async_trait::async_trait(?Send)]
324 impl Perform for ChangePassword {
325 type Response = LoginResponse;
327 #[tracing::instrument(skip(self, context, _websocket_id))]
330 context: &Data<LemmyContext>,
331 _websocket_id: Option<ConnectionId>,
332 ) -> Result<LoginResponse, LemmyError> {
333 let data: &ChangePassword = self;
334 let local_user_view =
335 get_local_user_view_from_jwt(data.auth.as_ref(), context.pool(), context.secret()).await?;
337 password_length_check(&data.new_password)?;
339 // Make sure passwords match
340 if data.new_password != data.new_password_verify {
341 return Err(LemmyError::from_message("passwords_dont_match"));
344 // Check the old password
345 let valid: bool = verify(
347 &local_user_view.local_user.password_encrypted,
351 return Err(LemmyError::from_message("password_incorrect"));
354 let local_user_id = local_user_view.local_user.id;
355 let new_password = data.new_password.to_owned();
356 let updated_local_user = blocking(context.pool(), move |conn| {
357 LocalUser::update_password(conn, local_user_id, &new_password)
365 updated_local_user.id.0,
366 &context.secret().jwt_secret,
367 &context.settings().hostname,
371 verify_email_sent: false,
372 registration_created: false,
377 #[async_trait::async_trait(?Send)]
378 impl Perform for AddAdmin {
379 type Response = AddAdminResponse;
381 #[tracing::instrument(skip(context, websocket_id))]
384 context: &Data<LemmyContext>,
385 websocket_id: Option<ConnectionId>,
386 ) -> Result<AddAdminResponse, LemmyError> {
387 let data: &AddAdmin = self;
388 let local_user_view =
389 get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
391 // Make sure user is an admin
392 is_admin(&local_user_view)?;
394 let added = data.added;
395 let added_person_id = data.person_id;
396 let added_admin = blocking(context.pool(), move |conn| {
397 Person::add_admin(conn, added_person_id, added)
400 .map_err(LemmyError::from)
401 .map_err(|e| e.with_message("couldnt_update_user"))?;
404 let form = ModAddForm {
405 mod_person_id: local_user_view.person.id,
406 other_person_id: added_admin.id,
407 removed: Some(!data.added),
410 blocking(context.pool(), move |conn| ModAdd::create(conn, &form)).await??;
412 let admins = blocking(context.pool(), PersonViewSafe::admins).await??;
414 let res = AddAdminResponse { admins };
416 context.chat_server().do_send(SendAllMessage {
417 op: UserOperation::AddAdmin,
418 response: res.clone(),
426 #[async_trait::async_trait(?Send)]
427 impl Perform for BanPerson {
428 type Response = BanPersonResponse;
430 #[tracing::instrument(skip(context, websocket_id))]
433 context: &Data<LemmyContext>,
434 websocket_id: Option<ConnectionId>,
435 ) -> Result<BanPersonResponse, LemmyError> {
436 let data: &BanPerson = self;
437 let local_user_view =
438 get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
440 // Make sure user is an admin
441 is_admin(&local_user_view)?;
444 let banned_person_id = data.person_id;
445 let expires = data.expires.map(naive_from_unix);
447 let ban_person = move |conn: &'_ _| Person::ban_person(conn, banned_person_id, ban, expires);
448 let person = blocking(context.pool(), ban_person)
450 .map_err(LemmyError::from)
451 .map_err(|e| e.with_message("couldnt_update_user"))?;
453 // Remove their data if that's desired
454 let remove_data = data.remove_data.unwrap_or(false);
456 remove_user_data(person.id, context.pool()).await?;
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),
468 blocking(context.pool(), move |conn| ModBan::create(conn, &form)).await??;
470 let person_id = data.person_id;
471 let person_view = blocking(context.pool(), move |conn| {
472 PersonViewSafe::read(conn, person_id)
476 let site = SiteOrCommunity::Site(
477 blocking(context.pool(), Site::read_local_site)
481 // if the action affects a local user, federate to other instances
487 &local_user_view.person.into(),
498 &local_user_view.person.into(),
506 let res = BanPersonResponse {
511 context.chat_server().do_send(SendAllMessage {
512 op: UserOperation::BanPerson,
513 response: res.clone(),
521 #[async_trait::async_trait(?Send)]
522 impl Perform for GetBannedPersons {
523 type Response = BannedPersonsResponse;
527 context: &Data<LemmyContext>,
528 _websocket_id: Option<ConnectionId>,
529 ) -> Result<Self::Response, LemmyError> {
530 let data: &GetBannedPersons = self;
531 let local_user_view =
532 get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
534 // Make sure user is an admin
535 is_admin(&local_user_view)?;
537 let banned = blocking(context.pool(), PersonViewSafe::banned).await??;
539 let res = Self::Response { banned };
545 #[async_trait::async_trait(?Send)]
546 impl Perform for BlockPerson {
547 type Response = BlockPersonResponse;
549 #[tracing::instrument(skip(context, _websocket_id))]
552 context: &Data<LemmyContext>,
553 _websocket_id: Option<ConnectionId>,
554 ) -> Result<BlockPersonResponse, LemmyError> {
555 let data: &BlockPerson = self;
556 let local_user_view =
557 get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
559 let target_id = data.person_id;
560 let person_id = local_user_view.person.id;
562 // Don't let a person block themselves
563 if target_id == person_id {
564 return Err(LemmyError::from_message("cant_block_yourself"));
567 let person_block_form = PersonBlockForm {
573 let block = move |conn: &'_ _| PersonBlock::block(conn, &person_block_form);
574 blocking(context.pool(), block)
576 .map_err(LemmyError::from)
577 .map_err(|e| e.with_message("person_block_already_exists"))?;
579 let unblock = move |conn: &'_ _| PersonBlock::unblock(conn, &person_block_form);
580 blocking(context.pool(), unblock)
582 .map_err(LemmyError::from)
583 .map_err(|e| e.with_message("person_block_already_exists"))?;
586 // TODO does any federated stuff need to be done here?
588 let person_view = blocking(context.pool(), move |conn| {
589 PersonViewSafe::read(conn, target_id)
593 let res = BlockPersonResponse {
602 #[async_trait::async_trait(?Send)]
603 impl Perform for GetReplies {
604 type Response = GetRepliesResponse;
606 #[tracing::instrument(skip(context, _websocket_id))]
609 context: &Data<LemmyContext>,
610 _websocket_id: Option<ConnectionId>,
611 ) -> Result<GetRepliesResponse, LemmyError> {
612 let data: &GetReplies = self;
613 let local_user_view =
614 get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
616 let sort: Option<SortType> = from_opt_str_to_opt_enum(&data.sort);
618 let page = data.page;
619 let limit = data.limit;
620 let unread_only = data.unread_only;
621 let person_id = local_user_view.person.id;
622 let show_bot_accounts = local_user_view.local_user.show_bot_accounts;
624 let replies = blocking(context.pool(), move |conn| {
625 CommentQueryBuilder::create(conn)
627 .unread_only(unread_only)
628 .recipient_id(person_id)
629 .show_bot_accounts(show_bot_accounts)
630 .my_person_id(person_id)
637 Ok(GetRepliesResponse { replies })
641 #[async_trait::async_trait(?Send)]
642 impl Perform for GetPersonMentions {
643 type Response = GetPersonMentionsResponse;
645 #[tracing::instrument(skip(context, _websocket_id))]
648 context: &Data<LemmyContext>,
649 _websocket_id: Option<ConnectionId>,
650 ) -> Result<GetPersonMentionsResponse, LemmyError> {
651 let data: &GetPersonMentions = self;
652 let local_user_view =
653 get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
655 let sort: Option<SortType> = from_opt_str_to_opt_enum(&data.sort);
657 let page = data.page;
658 let limit = data.limit;
659 let unread_only = data.unread_only;
660 let person_id = local_user_view.person.id;
661 let mentions = blocking(context.pool(), move |conn| {
662 PersonMentionQueryBuilder::create(conn)
663 .recipient_id(person_id)
664 .my_person_id(person_id)
666 .unread_only(unread_only)
673 Ok(GetPersonMentionsResponse { mentions })
677 #[async_trait::async_trait(?Send)]
678 impl Perform for MarkPersonMentionAsRead {
679 type Response = PersonMentionResponse;
681 #[tracing::instrument(skip(context, _websocket_id))]
684 context: &Data<LemmyContext>,
685 _websocket_id: Option<ConnectionId>,
686 ) -> Result<PersonMentionResponse, LemmyError> {
687 let data: &MarkPersonMentionAsRead = self;
688 let local_user_view =
689 get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
691 let person_mention_id = data.person_mention_id;
692 let read_person_mention = blocking(context.pool(), move |conn| {
693 PersonMention::read(conn, person_mention_id)
697 if local_user_view.person.id != read_person_mention.recipient_id {
698 return Err(LemmyError::from_message("couldnt_update_comment"));
701 let person_mention_id = read_person_mention.id;
702 let read = data.read;
704 move |conn: &'_ _| PersonMention::update_read(conn, person_mention_id, read);
705 blocking(context.pool(), update_mention)
707 .map_err(LemmyError::from)
708 .map_err(|e| e.with_message("couldnt_update_comment"))?;
710 let person_mention_id = read_person_mention.id;
711 let person_id = local_user_view.person.id;
712 let person_mention_view = blocking(context.pool(), move |conn| {
713 PersonMentionView::read(conn, person_mention_id, Some(person_id))
717 Ok(PersonMentionResponse {
723 #[async_trait::async_trait(?Send)]
724 impl Perform for MarkAllAsRead {
725 type Response = GetRepliesResponse;
727 #[tracing::instrument(skip(context, _websocket_id))]
730 context: &Data<LemmyContext>,
731 _websocket_id: Option<ConnectionId>,
732 ) -> Result<GetRepliesResponse, LemmyError> {
733 let data: &MarkAllAsRead = self;
734 let local_user_view =
735 get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
737 let person_id = local_user_view.person.id;
738 let replies = blocking(context.pool(), move |conn| {
739 CommentQueryBuilder::create(conn)
740 .my_person_id(person_id)
741 .recipient_id(person_id)
749 // TODO: this should probably be a bulk operation
750 // Not easy to do as a bulk operation,
751 // because recipient_id isn't in the comment table
752 for comment_view in &replies {
753 let reply_id = comment_view.comment.id;
754 let mark_as_read = move |conn: &'_ _| Comment::update_read(conn, reply_id, true);
755 blocking(context.pool(), mark_as_read)
757 .map_err(LemmyError::from)
758 .map_err(|e| e.with_message("couldnt_update_comment"))?;
761 // Mark all user mentions as read
762 let update_person_mentions =
763 move |conn: &'_ _| PersonMention::mark_all_as_read(conn, person_id);
764 blocking(context.pool(), update_person_mentions)
766 .map_err(LemmyError::from)
767 .map_err(|e| e.with_message("couldnt_update_comment"))?;
769 // Mark all private_messages as read
770 let update_pm = move |conn: &'_ _| PrivateMessage::mark_all_as_read(conn, person_id);
771 blocking(context.pool(), update_pm)
773 .map_err(LemmyError::from)
774 .map_err(|e| e.with_message("couldnt_update_private_message"))?;
776 Ok(GetRepliesResponse { replies: vec![] })
780 #[async_trait::async_trait(?Send)]
781 impl Perform for PasswordReset {
782 type Response = PasswordResetResponse;
784 #[tracing::instrument(skip(self, context, _websocket_id))]
787 context: &Data<LemmyContext>,
788 _websocket_id: Option<ConnectionId>,
789 ) -> Result<PasswordResetResponse, LemmyError> {
790 let data: &PasswordReset = self;
793 let email = data.email.clone();
794 let local_user_view = blocking(context.pool(), move |conn| {
795 LocalUserView::find_by_email(conn, &email)
798 .map_err(LemmyError::from)
799 .map_err(|e| e.with_message("couldnt_find_that_username_or_email"))?;
801 // Email the pure token to the user.
802 send_password_reset_email(&local_user_view, context.pool(), &context.settings()).await?;
803 Ok(PasswordResetResponse {})
807 #[async_trait::async_trait(?Send)]
808 impl Perform for PasswordChange {
809 type Response = LoginResponse;
811 #[tracing::instrument(skip(self, context, _websocket_id))]
814 context: &Data<LemmyContext>,
815 _websocket_id: Option<ConnectionId>,
816 ) -> Result<LoginResponse, LemmyError> {
817 let data: &PasswordChange = self;
819 // Fetch the user_id from the token
820 let token = data.token.clone();
821 let local_user_id = blocking(context.pool(), move |conn| {
822 PasswordResetRequest::read_from_token(conn, &token).map(|p| p.local_user_id)
826 password_length_check(&data.password)?;
828 // Make sure passwords match
829 if data.password != data.password_verify {
830 return Err(LemmyError::from_message("passwords_dont_match"));
833 // Update the user with the new password
834 let password = data.password.clone();
835 let updated_local_user = blocking(context.pool(), move |conn| {
836 LocalUser::update_password(conn, local_user_id, &password)
839 .map_err(LemmyError::from)
840 .map_err(|e| e.with_message("couldnt_update_user"))?;
846 updated_local_user.id.0,
847 &context.secret().jwt_secret,
848 &context.settings().hostname,
852 verify_email_sent: false,
853 registration_created: false,
858 #[async_trait::async_trait(?Send)]
859 impl Perform for GetReportCount {
860 type Response = GetReportCountResponse;
862 #[tracing::instrument(skip(context, _websocket_id))]
865 context: &Data<LemmyContext>,
866 _websocket_id: Option<ConnectionId>,
867 ) -> Result<GetReportCountResponse, LemmyError> {
868 let data: &GetReportCount = self;
869 let local_user_view =
870 get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
872 let person_id = local_user_view.person.id;
873 let admin = local_user_view.person.admin;
874 let community_id = data.community_id;
876 let comment_reports = blocking(context.pool(), move |conn| {
877 CommentReportView::get_report_count(conn, person_id, admin, community_id)
881 let post_reports = blocking(context.pool(), move |conn| {
882 PostReportView::get_report_count(conn, person_id, admin, community_id)
886 let res = GetReportCountResponse {
896 #[async_trait::async_trait(?Send)]
897 impl Perform for GetUnreadCount {
898 type Response = GetUnreadCountResponse;
900 #[tracing::instrument(skip(context, _websocket_id))]
903 context: &Data<LemmyContext>,
904 _websocket_id: Option<ConnectionId>,
905 ) -> Result<Self::Response, LemmyError> {
907 let local_user_view =
908 get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
910 let person_id = local_user_view.person.id;
912 let replies = blocking(context.pool(), move |conn| {
913 CommentView::get_unread_replies(conn, person_id)
917 let mentions = blocking(context.pool(), move |conn| {
918 PersonMentionView::get_unread_mentions(conn, person_id)
922 let private_messages = blocking(context.pool(), move |conn| {
923 PrivateMessageView::get_unread_messages(conn, person_id)
927 let res = Self::Response {
937 #[async_trait::async_trait(?Send)]
938 impl Perform for VerifyEmail {
939 type Response = VerifyEmailResponse;
943 context: &Data<LemmyContext>,
944 _websocket_id: Option<usize>,
945 ) -> Result<Self::Response, LemmyError> {
946 let token = self.token.clone();
947 let verification = blocking(context.pool(), move |conn| {
948 EmailVerification::read_for_token(conn, &token)
951 .map_err(LemmyError::from)
952 .map_err(|e| e.with_message("token_not_found"))?;
954 let form = LocalUserForm {
955 // necessary in case this is a new signup
956 email_verified: Some(true),
957 // necessary in case email of an existing user was changed
958 email: Some(Some(verification.email)),
959 ..LocalUserForm::default()
961 let local_user_id = verification.local_user_id;
962 blocking(context.pool(), move |conn| {
963 LocalUser::update(conn, local_user_id, &form)
967 let local_user_view = blocking(context.pool(), move |conn| {
968 LocalUserView::read(conn, local_user_id)
972 send_email_verification_success(&local_user_view, &context.settings())?;
974 blocking(context.pool(), move |conn| {
975 EmailVerification::delete_old_tokens_for_local_user(conn, local_user_id)
979 Ok(VerifyEmailResponse {})