diff --git a/src/admin/user/commands.rs b/src/admin/user/commands.rs index 61f10a86..ad2d1c78 100644 --- a/src/admin/user/commands.rs +++ b/src/admin/user/commands.rs @@ -238,7 +238,11 @@ pub(super) async fn suspend(&self, user_id: String) -> Result { if self.services.users.is_admin(&user_id).await { return Err!("Admin users cannot be suspended."); } - self.services.users.suspend_account(&user_id).await; + // TODO: Record the actual user that sent the suspension where possible + self.services + .users + .suspend_account(&user_id, self.services.globals.server_user.as_ref()) + .await; self.write_str(&format!("User {user_id} has been suspended.")) .await diff --git a/src/database/maps.rs b/src/database/maps.rs index 91ba2ebe..214dbf34 100644 --- a/src/database/maps.rs +++ b/src/database/maps.rs @@ -379,7 +379,7 @@ pub(super) static MAPS: &[Descriptor] = &[ ..descriptor::RANDOM }, Descriptor { - name: "userid_suspended", + name: "userid_suspension", ..descriptor::RANDOM_SMALL }, Descriptor { diff --git a/src/service/users/mod.rs b/src/service/users/mod.rs index b2a42959..d2dfccd9 100644 --- a/src/service/users/mod.rs +++ b/src/service/users/mod.rs @@ -16,10 +16,21 @@ use ruma::{ }, serde::Raw, }; +use serde::{Deserialize, Serialize}; use serde_json::json; use crate::{Dep, account_data, admin, globals, rooms}; +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UserSuspension { + /// Whether the user is currently suspended + pub suspended: bool, + /// When the user was suspended (Unix timestamp in milliseconds) + pub suspended_at: u64, + /// User ID of who suspended this user + pub suspended_by: String, +} + pub struct Service { services: Services, db: Data, @@ -52,7 +63,7 @@ struct Data { userid_lastonetimekeyupdate: Arc, userid_masterkeyid: Arc, userid_password: Arc, - userid_suspended: Arc, + userid_suspension: Arc, userid_selfsigningkeyid: Arc, userid_usersigningkeyid: Arc, useridprofilekey_value: Arc, @@ -88,7 +99,7 @@ impl crate::Service for Service { userid_lastonetimekeyupdate: args.db["userid_lastonetimekeyupdate"].clone(), userid_masterkeyid: args.db["userid_masterkeyid"].clone(), userid_password: args.db["userid_password"].clone(), - userid_suspended: args.db["userid_suspended"].clone(), + userid_suspension: args.db["userid_suspension"].clone(), userid_selfsigningkeyid: args.db["userid_selfsigningkeyid"].clone(), userid_usersigningkeyid: args.db["userid_usersigningkeyid"].clone(), useridprofilekey_value: args.db["useridprofilekey_value"].clone(), @@ -146,13 +157,20 @@ impl Service { } /// Suspend account, placing it in a read-only state - pub async fn suspend_account(&self, user_id: &UserId) { - self.db.userid_suspended.insert(user_id, "1"); + pub async fn suspend_account(&self, user_id: &UserId, suspending_user: &UserId) { + self.db.userid_suspension.raw_put( + user_id, + Json(UserSuspension { + suspended: true, + suspended_at: MilliSecondsSinceUnixEpoch::now().get().into(), + suspended_by: suspending_user.to_string(), + }), + ); } /// Unsuspend account, placing it in a read-write state pub async fn unsuspend_account(&self, user_id: &UserId) { - self.db.userid_suspended.remove(user_id); + self.db.userid_suspension.remove(user_id); } /// Check if a user has an account on this homeserver. @@ -173,23 +191,21 @@ impl Service { /// Check if account is suspended pub async fn is_suspended(&self, user_id: &UserId) -> Result { - self.db - .userid_suspended + match self + .db + .userid_suspension .get(user_id) - .map_ok_or_else( - |err| { - if err.is_not_found() { - Ok(false) - } else { - err!(Database(error!( - "Failed to check if user {user_id} is suspended: {err}" - ))); - Ok(true) - } - }, - |_| Ok(true), - ) .await + .deserialized::() + { + | Ok(s) => Ok(s.suspended), + | Err(e) => + if e.is_not_found() { + Ok(false) + } else { + Err(e) + }, + } } /// Check if account is active, infallible