summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorColin Reeder <vpzomtrrfrt@gmail.com>2020-10-25 22:28:47 -0600
committerColin Reeder <vpzomtrrfrt@gmail.com>2020-10-25 22:28:47 -0600
commit73b7d5fba0bd9729bbbf7be135fde5d2cbda1d3b (patch)
treebfe451fa8a169e21f02f891df1eb1f366108047c
parenteaf6306865c3d2d719630aaafbd4cfdaa7760788 (diff)
Implement user suspension (#113)
-rw-r--r--migrations/20201026032058_user-suspends/down.sql3
-rw-r--r--migrations/20201026032058_user-suspends/up.sql3
-rw-r--r--res/lang/en.ftl1
-rw-r--r--src/main.rs10
-rw-r--r--src/routes/api/mod.rs18
-rw-r--r--src/routes/api/users.rs72
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,
};