]> Untitled Git - lemmy.git/blobdiff - crates/api/src/local_user.rs
Implement instance actor (#1798)
[lemmy.git] / crates / api / src / local_user.rs
index a0711d690698b7bd92e0a30646af939b4ee41206..b4e2bfb190b87246c2e4750b2f5d5a7d17c8b671 100644 (file)
@@ -1,15 +1,23 @@
 use crate::{captcha_as_wav_base64, Perform};
 use actix_web::web::Data;
-use anyhow::Context;
 use bcrypt::verify;
 use captcha::{gen, Difficulty};
 use chrono::Duration;
 use lemmy_api_common::{
   blocking,
+  check_registration_application,
   get_local_user_view_from_jwt,
   is_admin,
   password_length_check,
   person::*,
+  remove_user_data,
+  send_email_verification_success,
+  send_password_reset_email,
+  send_verification_email,
+};
+use lemmy_apub::{
+  activities::block::SiteOrCommunity,
+  protocol::activities::block::{block_user::BlockUser, undo_block_user::UndoBlockUser},
 };
 use lemmy_db_schema::{
   diesel_option_overwrite,
@@ -18,14 +26,13 @@ use lemmy_db_schema::{
   naive_now,
   source::{
     comment::Comment,
-    community::Community,
+    email_verification::EmailVerification,
     local_user::{LocalUser, LocalUserForm},
     moderator::*,
     password_reset_request::*,
     person::*,
     person_block::{PersonBlock, PersonBlockForm},
     person_mention::*,
-    post::Post,
     private_message::PrivateMessage,
     site::*,
   },
@@ -40,18 +47,14 @@ use lemmy_db_views::{
   private_message_view::PrivateMessageView,
 };
 use lemmy_db_views_actor::{
-  community_moderator_view::CommunityModeratorView,
   person_mention_view::{PersonMentionQueryBuilder, PersonMentionView},
   person_view::PersonViewSafe,
 };
 use lemmy_utils::{
   claims::Claims,
-  email::send_email,
-  location_info,
-  utils::{generate_random_string, is_valid_display_name, is_valid_matrix_id, naive_from_unix},
+  utils::{is_valid_display_name, is_valid_matrix_id, naive_from_unix},
   ConnectionId,
   LemmyError,
-  Sensitive,
 };
 use lemmy_websocket::{
   messages::{CaptchaItem, SendAllMessage},
@@ -90,14 +93,25 @@ impl Perform for Login {
       return Err(LemmyError::from_message("password_incorrect"));
     }
 
+    let site = blocking(context.pool(), Site::read_local_site).await??;
+    if site.require_email_verification && !local_user_view.local_user.email_verified {
+      return Err(LemmyError::from_message("email_not_verified"));
+    }
+
+    check_registration_application(&site, &local_user_view, context.pool()).await?;
+
     // Return the jwt
     Ok(LoginResponse {
-      jwt: Claims::jwt(
-        local_user_view.local_user.id.0,
-        &context.secret().jwt_secret,
-        &context.settings().hostname,
-      )?
-      .into(),
+      jwt: Some(
+        Claims::jwt(
+          local_user_view.local_user.id.0,
+          &context.secret().jwt_secret,
+          &context.settings().hostname,
+        )?
+        .into(),
+      ),
+      verify_email_sent: false,
+      registration_created: false,
     })
   }
 }
@@ -164,11 +178,35 @@ impl Perform for SaveUserSettings {
 
     let avatar = diesel_option_overwrite_to_url(&data.avatar)?;
     let banner = diesel_option_overwrite_to_url(&data.banner)?;
-    let email = diesel_option_overwrite(&data.email.clone().map(Sensitive::into_inner));
     let bio = diesel_option_overwrite(&data.bio);
     let display_name = diesel_option_overwrite(&data.display_name);
     let matrix_user_id = diesel_option_overwrite(&data.matrix_user_id);
     let bot_account = data.bot_account;
+    let email_deref = data.email.as_deref().map(|e| e.to_owned());
+    let email = diesel_option_overwrite(&email_deref);
+
+    if let Some(Some(email)) = &email {
+      let previous_email = local_user_view.local_user.email.unwrap_or_default();
+      // Only send the verification email if there was an email change
+      if previous_email.ne(email) {
+        send_verification_email(
+          local_user_view.local_user.id,
+          email,
+          &local_user_view.person.name,
+          context.pool(),
+          &context.settings(),
+        )
+        .await?;
+      }
+    }
+
+    // When the site requires email, make sure email is not Some(None). IE, an overwrite to a None value
+    if let Some(email) = &email {
+      let site_fut = blocking(context.pool(), Site::read_local_site);
+      if email.is_none() && site_fut.await??.require_email_verification {
+        return Err(LemmyError::from_message("email_required"));
+      }
+    }
 
     if let Some(Some(bio)) = &bio {
       if bio.chars().count() > 300 {
@@ -218,6 +256,7 @@ impl Perform for SaveUserSettings {
       shared_inbox_url: None,
       matrix_user_id,
       bot_account,
+      ban_expires: None,
     };
 
     blocking(context.pool(), move |conn| {
@@ -228,9 +267,9 @@ impl Perform for SaveUserSettings {
     .map_err(|e| e.with_message("user_already_exists"))?;
 
     let local_user_form = LocalUserForm {
-      person_id,
+      person_id: Some(person_id),
       email,
-      password_encrypted,
+      password_encrypted: Some(password_encrypted),
       show_nsfw: data.show_nsfw,
       show_bot_accounts: data.show_bot_accounts,
       show_scores: data.show_scores,
@@ -242,6 +281,8 @@ impl Perform for SaveUserSettings {
       show_read_posts: data.show_read_posts,
       show_new_post_notifs: data.show_new_post_notifs,
       send_notifications_to_email: data.send_notifications_to_email,
+      email_verified: None,
+      accepted_application: None,
     };
 
     let local_user_res = blocking(context.pool(), move |conn| {
@@ -265,12 +306,16 @@ impl Perform for SaveUserSettings {
 
     // Return the jwt
     Ok(LoginResponse {
-      jwt: Claims::jwt(
-        updated_local_user.id.0,
-        &context.secret().jwt_secret,
-        &context.settings().hostname,
-      )?
-      .into(),
+      jwt: Some(
+        Claims::jwt(
+          updated_local_user.id.0,
+          &context.secret().jwt_secret,
+          &context.settings().hostname,
+        )?
+        .into(),
+      ),
+      verify_email_sent: false,
+      registration_created: false,
     })
   }
 }
@@ -315,12 +360,16 @@ impl Perform for ChangePassword {
 
     // Return the jwt
     Ok(LoginResponse {
-      jwt: Claims::jwt(
-        updated_local_user.id.0,
-        &context.secret().jwt_secret,
-        &context.settings().hostname,
-      )?
-      .into(),
+      jwt: Some(
+        Claims::jwt(
+          updated_local_user.id.0,
+          &context.secret().jwt_secret,
+          &context.settings().hostname,
+        )?
+        .into(),
+      ),
+      verify_email_sent: false,
+      registration_created: false,
     })
   }
 }
@@ -360,18 +409,7 @@ impl Perform for AddAdmin {
 
     blocking(context.pool(), move |conn| ModAdd::create(conn, &form)).await??;
 
-    let site_creator_id = blocking(context.pool(), move |conn| {
-      Site::read(conn, 1).map(|s| s.creator_id)
-    })
-    .await??;
-
-    let mut admins = blocking(context.pool(), PersonViewSafe::admins).await??;
-    let creator_index = admins
-      .iter()
-      .position(|r| r.person.id == site_creator_id)
-      .context(location_info!())?;
-    let creator_person = admins.remove(creator_index);
-    admins.insert(0, creator_person);
+    let admins = blocking(context.pool(), PersonViewSafe::admins).await??;
 
     let res = AddAdminResponse { admins };
 
@@ -404,51 +442,21 @@ impl Perform for BanPerson {
 
     let ban = data.ban;
     let banned_person_id = data.person_id;
-    let ban_person = move |conn: &'_ _| Person::ban_person(conn, banned_person_id, ban);
-    blocking(context.pool(), ban_person)
+    let expires = data.expires.map(naive_from_unix);
+
+    let ban_person = move |conn: &'_ _| Person::ban_person(conn, banned_person_id, ban, expires);
+    let person = blocking(context.pool(), ban_person)
       .await?
       .map_err(LemmyError::from)
       .map_err(|e| e.with_message("couldnt_update_user"))?;
 
     // Remove their data if that's desired
-    if data.remove_data.unwrap_or(false) {
-      // Posts
-      blocking(context.pool(), move |conn: &'_ _| {
-        Post::update_removed_for_creator(conn, banned_person_id, None, true)
-      })
-      .await??;
-
-      // Communities
-      // Remove all communities where they're the top mod
-      // for now, remove the communities manually
-      let first_mod_communities = blocking(context.pool(), move |conn: &'_ _| {
-        CommunityModeratorView::get_community_first_mods(conn)
-      })
-      .await??;
-
-      // Filter to only this banned users top communities
-      let banned_user_first_communities: Vec<CommunityModeratorView> = first_mod_communities
-        .into_iter()
-        .filter(|fmc| fmc.moderator.id == banned_person_id)
-        .collect();
-
-      for first_mod_community in banned_user_first_communities {
-        blocking(context.pool(), move |conn: &'_ _| {
-          Community::update_removed(conn, first_mod_community.community.id, true)
-        })
-        .await??;
-      }
-
-      // Comments
-      blocking(context.pool(), move |conn: &'_ _| {
-        Comment::update_removed_for_creator(conn, banned_person_id, true)
-      })
-      .await??;
+    let remove_data = data.remove_data.unwrap_or(false);
+    if remove_data {
+      remove_user_data(person.id, context.pool()).await?;
     }
 
     // Mod tables
-    let expires = data.expires.map(naive_from_unix);
-
     let form = ModBanForm {
       mod_person_id: local_user_view.person.id,
       other_person_id: data.person_id,
@@ -465,6 +473,36 @@ impl Perform for BanPerson {
     })
     .await??;
 
+    let site = SiteOrCommunity::Site(
+      blocking(context.pool(), Site::read_local_site)
+        .await??
+        .into(),
+    );
+    // if the action affects a local user, federate to other instances
+    if person.local {
+      if ban {
+        BlockUser::send(
+          &site,
+          &person.into(),
+          &local_user_view.person.into(),
+          remove_data,
+          data.reason.clone(),
+          expires,
+          context,
+        )
+        .await?;
+      } else {
+        UndoBlockUser::send(
+          &site,
+          &person.into(),
+          &local_user_view.person.into(),
+          data.reason.clone(),
+          context,
+        )
+        .await?;
+      }
+    }
+
     let res = BanPersonResponse {
       person_view,
       banned: data.ban,
@@ -480,6 +518,30 @@ impl Perform for BanPerson {
   }
 }
 
+#[async_trait::async_trait(?Send)]
+impl Perform for GetBannedPersons {
+  type Response = BannedPersonsResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<Self::Response, LemmyError> {
+    let data: &GetBannedPersons = self;
+    let local_user_view =
+      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
+
+    // Make sure user is an admin
+    is_admin(&local_user_view)?;
+
+    let banned = blocking(context.pool(), PersonViewSafe::banned).await??;
+
+    let res = Self::Response { banned };
+
+    Ok(res)
+  }
+}
+
 #[async_trait::async_trait(?Send)]
 impl Perform for BlockPerson {
   type Response = BlockPersonResponse;
@@ -736,34 +798,8 @@ impl Perform for PasswordReset {
     .map_err(LemmyError::from)
     .map_err(|e| e.with_message("couldnt_find_that_username_or_email"))?;
 
-    // Generate a random token
-    let token = generate_random_string();
-
-    // Insert the row
-    let token2 = token.clone();
-    let local_user_id = local_user_view.local_user.id;
-    blocking(context.pool(), move |conn| {
-      PasswordResetRequest::create_token(conn, local_user_id, &token2)
-    })
-    .await??;
-
     // Email the pure token to the user.
-    // TODO no i18n support here.
-    let email = &local_user_view.local_user.email.expect("email");
-    let subject = &format!("Password reset for {}", local_user_view.person.name);
-    let protocol_and_hostname = &context.settings().get_protocol_and_hostname();
-    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);
-    send_email(
-      subject,
-      email,
-      &local_user_view.person.name,
-      html,
-      &context.settings(),
-    )
-    .map_err(|e| anyhow::anyhow!("{}", e))
-    .map_err(LemmyError::from)
-    .map_err(|e| e.with_message("email_send_failed"))?;
-
+    send_password_reset_email(&local_user_view, context.pool(), &context.settings()).await?;
     Ok(PasswordResetResponse {})
   }
 }
@@ -805,12 +841,16 @@ impl Perform for PasswordChange {
 
     // Return the jwt
     Ok(LoginResponse {
-      jwt: Claims::jwt(
-        updated_local_user.id.0,
-        &context.secret().jwt_secret,
-        &context.settings().hostname,
-      )?
-      .into(),
+      jwt: Some(
+        Claims::jwt(
+          updated_local_user.id.0,
+          &context.secret().jwt_secret,
+          &context.settings().hostname,
+        )?
+        .into(),
+      ),
+      verify_email_sent: false,
+      registration_created: false,
     })
   }
 }
@@ -893,3 +933,49 @@ impl Perform for GetUnreadCount {
     Ok(res)
   }
 }
+
+#[async_trait::async_trait(?Send)]
+impl Perform for VerifyEmail {
+  type Response = VerifyEmailResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<usize>,
+  ) -> Result<Self::Response, LemmyError> {
+    let token = self.token.clone();
+    let verification = blocking(context.pool(), move |conn| {
+      EmailVerification::read_for_token(conn, &token)
+    })
+    .await?
+    .map_err(LemmyError::from)
+    .map_err(|e| e.with_message("token_not_found"))?;
+
+    let form = LocalUserForm {
+      // necessary in case this is a new signup
+      email_verified: Some(true),
+      // necessary in case email of an existing user was changed
+      email: Some(Some(verification.email)),
+      ..LocalUserForm::default()
+    };
+    let local_user_id = verification.local_user_id;
+    blocking(context.pool(), move |conn| {
+      LocalUser::update(conn, local_user_id, &form)
+    })
+    .await??;
+
+    let local_user_view = blocking(context.pool(), move |conn| {
+      LocalUserView::read(conn, local_user_id)
+    })
+    .await??;
+
+    send_email_verification_success(&local_user_view, &context.settings())?;
+
+    blocking(context.pool(), move |conn| {
+      EmailVerification::delete_old_tokens_for_local_user(conn, local_user_id)
+    })
+    .await??;
+
+    Ok(VerifyEmailResponse {})
+  }
+}