diff options
author | Colin Reeder <vpzomtrrfrt@gmail.com> | 2020-10-25 22:28:47 -0600 |
---|---|---|
committer | Colin Reeder <vpzomtrrfrt@gmail.com> | 2020-10-25 22:28:47 -0600 |
commit | 73b7d5fba0bd9729bbbf7be135fde5d2cbda1d3b (patch) | |
tree | bfe451fa8a169e21f02f891df1eb1f366108047c | |
parent | eaf6306865c3d2d719630aaafbd4cfdaa7760788 (diff) |
Implement user suspension (#113)
-rw-r--r-- | migrations/20201026032058_user-suspends/down.sql | 3 | ||||
-rw-r--r-- | migrations/20201026032058_user-suspends/up.sql | 3 | ||||
-rw-r--r-- | res/lang/en.ftl | 1 | ||||
-rw-r--r-- | src/main.rs | 10 | ||||
-rw-r--r-- | src/routes/api/mod.rs | 18 | ||||
-rw-r--r-- | src/routes/api/users.rs | 72 |
6 files changed, 89 insertions, 18 deletions
diff --git a/migrations/20201026032058_user-suspends/down.sql b/migrations/20201026032058_user-suspends/down.sql new file mode 100644 index 0000000..d0b4c4c --- /dev/null +++ b/migrations/20201026032058_user-suspends/down.sql @@ -0,0 +1,3 @@ +BEGIN; + ALTER TABLE person DROP COLUMN suspended; +COMMIT; diff --git a/migrations/20201026032058_user-suspends/up.sql b/migrations/20201026032058_user-suspends/up.sql new file mode 100644 index 0000000..8af6ecd --- /dev/null +++ b/migrations/20201026032058_user-suspends/up.sql @@ -0,0 +1,3 @@ +BEGIN; + ALTER TABLE person ADD COLUMN suspended BOOLEAN NOT NULL DEFAULT (FALSE); +COMMIT; diff --git a/res/lang/en.ftl b/res/lang/en.ftl index 55f3f14..27483af 100644 --- a/res/lang/en.ftl +++ b/res/lang/en.ftl @@ -30,3 +30,4 @@ root = lotide is running. Note that lotide itself does not include a frontend, a user_email_invalid = Specified email address is invalid user_name_disallowed_chars = Username contains disallowed characters user_no_avatar = That user does not have an avatar +user_suspended_error = This account has been suspended diff --git a/src/main.rs b/src/main.rs index afd7101..45e304f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -653,6 +653,16 @@ pub async fn is_site_admin(db: &tokio_postgres::Client, user: UserLocalID) -> Re }) } +pub async fn is_local_user(db: &tokio_postgres::Client, user: UserLocalID) -> Result<bool, Error> { + let row = db + .query_opt("SELECT local FROM person WHERE id=$1", &[&user]) + .await?; + Ok(match row { + None => false, + Some(row) => row.get(0), + }) +} + pub fn spawn_task<F: std::future::Future<Output = Result<(), Error>> + Send + 'static>(task: F) { use futures::future::TryFutureExt; tokio::spawn(task.map_err(|err| { diff --git a/src/routes/api/mod.rs b/src/routes/api/mod.rs index d62123d..14c7f71 100644 --- a/src/routes/api/mod.rs +++ b/src/routes/api/mod.rs @@ -342,7 +342,7 @@ async fn route_unstable_logins_create( let row = db .query_opt( - "SELECT id, username, passhash, is_site_admin, EXISTS(SELECT 1 FROM notification WHERE to_user = person.id AND created_at > person.last_checked_notifications) FROM person WHERE LOWER(username)=LOWER($1) AND local", + "SELECT id, username, passhash, is_site_admin, suspended, EXISTS(SELECT 1 FROM notification WHERE to_user = person.id AND created_at > person.last_checked_notifications) FROM person WHERE LOWER(username)=LOWER($1) AND local", &[&body.username], ) .await? @@ -371,6 +371,13 @@ async fn route_unstable_logins_create( .await??; if correct { + if row.get(4) { + return Err(crate::Error::UserError(crate::simple_response( + hyper::StatusCode::FORBIDDEN, + lang.tr("user_suspended_error", None).into_owned(), + ))); + } + let token = insert_token(id, &db).await?; crate::json_response( @@ -378,7 +385,7 @@ async fn route_unstable_logins_create( id, username, is_site_admin: row.get(3), - has_unread_notifications: row.get(4), + has_unread_notifications: row.get(5), }}), ) } else { @@ -526,12 +533,7 @@ async fn route_unstable_instance_patch( let user = crate::require_login(&req_parts, &db).await?; - let is_site_admin: bool = { - let row = db - .query_one("SELECT is_site_admin FROM person WHERE id=$1", &[&user]) - .await?; - row.get(0) - }; + let is_site_admin = crate::is_site_admin(&db, user).await?; if is_site_admin { if let Some(description) = body.description { diff --git a/src/routes/api/users.rs b/src/routes/api/users.rs index d4a5950..d701963 100644 --- a/src/routes/api/users.rs +++ b/src/routes/api/users.rs @@ -8,9 +8,32 @@ use serde_derive::{Deserialize, Serialize}; use std::borrow::Cow; use std::sync::Arc; -struct MeOrAdminResult { +struct MeOrLocalAndAdminResult { pub login_user: UserLocalID, pub target_user: UserLocalID, + is_admin: Option<bool>, +} + +impl MeOrLocalAndAdminResult { + pub async fn require_admin( + &self, + db: &tokio_postgres::Client, + lang: &crate::Translator, + ) -> Result<(), crate::Error> { + let is_admin = match self.is_admin { + Some(value) => value, + None => crate::is_site_admin(db, self.login_user).await?, + }; + + if is_admin { + Ok(()) + } else { + Err(crate::Error::UserError(crate::simple_response( + hyper::StatusCode::FORBIDDEN, + lang.tr("not_admin", None).into_owned(), + ))) + } + } } #[derive(Clone, Copy, PartialEq, Debug)] @@ -59,22 +82,32 @@ impl UserIDOrMe { } } - pub async fn require_me_or_admin( + pub async fn require_me_or_local_and_admin( self, req: &hyper::Request<hyper::Body>, db: &tokio_postgres::Client, - ) -> Result<MeOrAdminResult, crate::Error> { + ) -> Result<MeOrLocalAndAdminResult, crate::Error> { let login_user = crate::require_login(req, db).await?; match self { - UserIDOrMe::Me => Ok(MeOrAdminResult { + UserIDOrMe::Me => Ok(MeOrLocalAndAdminResult { login_user, target_user: login_user, + is_admin: None, }), UserIDOrMe::User(target_user) => { - if target_user == login_user || crate::is_site_admin(db, login_user).await? { - Ok(MeOrAdminResult { + if target_user == login_user { + Ok(MeOrLocalAndAdminResult { login_user, target_user, + is_admin: None, + }) + } else if crate::is_site_admin(db, login_user).await? + && crate::is_local_user(db, target_user).await? + { + Ok(MeOrLocalAndAdminResult { + login_user, + target_user, + is_admin: Some(true), }) } else { Err(crate::Error::UserError(crate::simple_response( @@ -111,6 +144,8 @@ struct RespUserInfo<'a> { description: &'a str, #[serde(skip_serializing_if = "Option::is_none")] + suspended: Option<bool>, + #[serde(skip_serializing_if = "Option::is_none")] your_note: Option<Option<JustContentText<'a>>>, } @@ -210,9 +245,9 @@ async fn route_unstable_users_patch( req: hyper::Request<hyper::Body>, ) -> Result<hyper::Response<hyper::Body>, crate::Error> { let lang = crate::get_lang_for_req(&req); - let db = ctx.db_pool.get().await?; + let mut db = ctx.db_pool.get().await?; - let me_or_admin = params.0.require_me_or_admin(&req, &db).await?; + let me_or_admin = params.0.require_me_or_local_and_admin(&req, &db).await?; let user_id = me_or_admin.target_user; #[derive(Deserialize)] @@ -221,6 +256,7 @@ async fn route_unstable_users_patch( email_address: Option<Cow<'a, str>>, password: Option<String>, avatar: Option<Cow<'a, str>>, + suspended: Option<bool>, } let body = hyper::body::to_bytes(req.into_body()).await?; @@ -260,6 +296,11 @@ async fn route_unstable_users_patch( changes.push(("avatar", avatar)); } + if let Some(suspended) = &body.suspended { + me_or_admin.require_admin(&db, &lang).await?; + + changes.push(("suspended", suspended)); + } if !changes.is_empty() { use std::fmt::Write; @@ -286,7 +327,17 @@ async fn route_unstable_users_patch( let sql: &str = &sql; - db.execute(sql, &values).await?; + let trans = db.transaction().await?; + trans.execute(sql, &values).await?; + if body.suspended == Some(true) { + // just suspended, need to clear out current logins + + trans + .execute("DELETE FROM login WHERE person=$1", &[&user_id]) + .await?; + } + + trans.commit().await?; } Ok(crate::empty_response()) @@ -565,7 +616,7 @@ async fn route_unstable_users_get( let row = db .query_opt( - "SELECT username, local, ap_id, description, avatar FROM person WHERE id=$1", + "SELECT username, local, ap_id, description, avatar, suspended FROM person WHERE id=$1", &[&user_id], ) .await?; @@ -595,6 +646,7 @@ async fn route_unstable_users_get( let info = RespUserInfo { base: info, description: row.get(3), + suspended: if local { Some(row.get(5)) } else { None }, your_note, }; |