diff --git a/src/controllers/users_controller.rs b/src/controllers/users_controller.rs index ad66d711..ddaa2050 100644 --- a/src/controllers/users_controller.rs +++ b/src/controllers/users_controller.rs @@ -215,6 +215,22 @@ pub async fn set_admin<'r>( }) } +#[post("/users//change_state", data = "")] +pub async fn change_state<'r>( + username: String, + value: Api, + _session: AdminSession, + db: DbConn, +) -> Result> { + let mut user = User::find_by_username(username, &db).await?; + user.state = value.into_inner().state; + let user = user.update(&db).await?; + Ok(Accepter { + html: Redirect::to(uri!(show_user(user.username))), + json: Custom(Status::NoContent, ()), + }) +} + #[post("/users//approve")] pub async fn set_approved<'r>( username: String, diff --git a/src/lib.rs b/src/lib.rs index 4dbb03ae..6ce9448f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -114,6 +114,7 @@ fn assemble(rocket: Rocket) -> Rocket { users_controller::show_user, users_controller::list_users, users_controller::update_user, + users_controller::change_state, users_controller::set_admin, users_controller::set_approved, users_controller::forgot_password_get, diff --git a/src/models/user.rs b/src/models/user.rs index a1399a48..f97fdee7 100644 --- a/src/models/user.rs +++ b/src/models/user.rs @@ -1,5 +1,5 @@ use self::schema::users; -use crate::errors::{InternalError, LoginError, Result, ZauthError}; +use crate::errors::{self, InternalError, LoginError, ZauthError}; use crate::DbConn; use diesel::{self, prelude::*}; use diesel_derive_enum::DbEnum; @@ -11,11 +11,13 @@ use chrono::{NaiveDateTime, Utc}; use lettre::message::Mailbox; use pwhash::bcrypt::{self, BcryptSetup}; use regex::Regex; -use rocket::serde::Serialize; +use rocket::{serde::Serialize, FromFormField}; use std::convert::TryFrom; use validator::{Validate, ValidationError, ValidationErrors}; -#[derive(DbEnum, Debug, Serialize, Clone, PartialEq)] +#[derive( + DbEnum, Debug, Deserialize, FromFormField, Serialize, Clone, PartialEq, +)] pub enum UserState { PendingApproval, PendingMailConfirmation, @@ -163,6 +165,11 @@ pub struct ChangeAdmin { pub admin: bool, } +#[derive(FromForm, Deserialize, Debug, Clone)] +pub struct ChangeStatus { + pub state: UserState, +} + #[derive(Validate, FromForm, Deserialize, Debug, Clone)] pub struct ChangePassword { #[validate(length( @@ -173,7 +180,7 @@ pub struct ChangePassword { } impl User { - pub async fn all(db: &DbConn) -> Result> { + pub async fn all(db: &DbConn) -> errors::Result> { let all_users = db.run(move |conn| users::table.load::(conn)).await?; Ok(all_users) @@ -186,7 +193,7 @@ impl User { pub async fn find_by_username<'r>( username: String, db: &DbConn, - ) -> Result { + ) -> errors::Result { db.run(move |conn| { users::table .filter(users::username.eq(username)) @@ -196,13 +203,16 @@ impl User { .await } - pub async fn find_by_email(email: String, db: &DbConn) -> Result { + pub async fn find_by_email( + email: String, + db: &DbConn, + ) -> errors::Result { let query = users::table.filter(users::email.eq(email)); db.run(move |conn| query.first(conn).map_err(ZauthError::from)) .await } - pub async fn delete(self, db: &DbConn) -> Result<()> { + pub async fn delete(self, db: &DbConn) -> errors::Result<()> { db.run(move |conn| { diesel::delete(users::table.find(self.id)) .execute(conn) @@ -215,7 +225,7 @@ impl User { pub async fn find_by_password_token<'r>( token: String, db: &DbConn, - ) -> Result> { + ) -> errors::Result> { let token = token.to_owned(); let result = db .run(move |conn| { @@ -248,7 +258,7 @@ impl User { pub async fn find_by_email_token<'r>( token: String, db: &DbConn, - ) -> Result> { + ) -> errors::Result> { let token = token.to_owned(); let result = db .run(move |conn| { @@ -266,7 +276,7 @@ impl User { } } - pub async fn find_by_pending(db: &DbConn) -> Result> { + pub async fn find_by_pending(db: &DbConn) -> errors::Result> { let pending_users = db .run(move |conn| { users::table @@ -281,7 +291,7 @@ impl User { user: NewUser, bcrypt_cost: u32, db: &DbConn, - ) -> Result { + ) -> errors::Result { user.validate()?; let user = NewUserHashed { username: user.username, @@ -309,7 +319,7 @@ impl User { user: NewUser, conf: &Config, db: &DbConn, - ) -> Result { + ) -> errors::Result { user.validate()?; if Self::pending_count(&db).await? >= conf.maximum_pending_users { let mut err = ValidationErrors::new(); @@ -349,7 +359,7 @@ impl User { .await } - pub async fn approve(mut self, db: &DbConn) -> Result { + pub async fn approve(mut self, db: &DbConn) -> errors::Result { if self.state != UserState::PendingApproval { return Err(ZauthError::Unprocessable(String::from( "user is not in the pending approval state", @@ -359,7 +369,7 @@ impl User { self.update(&db).await } - pub async fn confirm_email(mut self, db: &DbConn) -> Result { + pub async fn confirm_email(mut self, db: &DbConn) -> errors::Result { if self.state == UserState::PendingMailConfirmation { self.state = UserState::PendingApproval; } @@ -375,7 +385,7 @@ impl User { self.update(&db).await } - pub fn change_with(&mut self, change: UserChange) -> Result<()> { + pub fn change_with(&mut self, change: UserChange) -> errors::Result<()> { if let Some(email) = change.email { self.email = email; } @@ -385,7 +395,7 @@ impl User { Ok(()) } - pub async fn update(self, db: &DbConn) -> Result { + pub async fn update(self, db: &DbConn) -> errors::Result { let id = self.id; db.run(move |conn| { conn.transaction(|| { @@ -407,7 +417,7 @@ impl User { change: ChangePassword, conf: &Config, db: &DbConn, - ) -> Result { + ) -> errors::Result { change.validate()?; self.hashed_password = hash(&change.password, conf.bcrypt_cost)?; self.password_reset_token = None; @@ -415,23 +425,26 @@ impl User { self.update(db).await } - pub async fn reload(self, db: &DbConn) -> Result { + pub async fn reload(self, db: &DbConn) -> errors::Result { Self::find(self.id, db).await } - pub async fn update_last_login(mut self, db: &DbConn) -> Result { + pub async fn update_last_login( + mut self, + db: &DbConn, + ) -> errors::Result { self.last_login = Utc::now().naive_utc(); self.update(db).await } - pub async fn find(id: i32, db: &DbConn) -> Result { + pub async fn find(id: i32, db: &DbConn) -> errors::Result { db.run(move |conn| { users::table.find(id).first(conn).map_err(ZauthError::from) }) .await } - pub async fn pending_count(db: &DbConn) -> Result { + pub async fn pending_count(db: &DbConn) -> errors::Result { let count: i64 = db .run(move |conn| { users::table @@ -447,7 +460,7 @@ impl User { Ok(count as usize) } - pub async fn last(db: &DbConn) -> Result { + pub async fn last(db: &DbConn) -> errors::Result { db.run(move |conn| { users::table .order(users::id.desc()) @@ -461,7 +474,7 @@ impl User { username: String, password: String, db: &DbConn, - ) -> Result { + ) -> errors::Result { match Self::find_by_username(username, db).await { Ok(user) if !verify(&password, &user.hashed_password) => { Err(ZauthError::LoginError(LoginError::UsernamePasswordError)) @@ -506,7 +519,7 @@ fn verify(password: &str, hash: &str) -> bool { impl TryFrom<&User> for Mailbox { type Error = ZauthError; - fn try_from(value: &User) -> Result { + fn try_from(value: &User) -> errors::Result { Ok(Mailbox::new( Some(value.username.to_string()), value.email.clone().parse().map_err(InternalError::from)?, diff --git a/templates/users/show.html b/templates/users/show.html index 44becf39..06eba1cf 100644 --- a/templates/users/show.html +++ b/templates/users/show.html @@ -11,7 +11,7 @@
{% if user.admin %} - Zeus Admin + Zeus Admin {% else %} Zeus Member {% endif %} @@ -29,27 +29,27 @@
- - -
+ + +
- - -
+ + +
- - -
+ + +
- - -
+ + +
- +
@@ -58,11 +58,27 @@ -
-
+ + + + {% if current_user.admin %} + +
+
+
+ +
+ + +
+
+ {% endif %}
{% endblock content %} diff --git a/tests/users.rs b/tests/users.rs index 4db9e49e..6b25e951 100644 --- a/tests/users.rs +++ b/tests/users.rs @@ -879,3 +879,52 @@ async fn validate_on_admin_create() { }) .await; } + +#[rocket::async_test] +async fn disable_user() { + common::as_admin(async move |http_client, db, _admin| { + let user = User::create( + NewUser { + username: String::from("somebody"), + password: String::from("once told me"), + full_name: String::from("zeus"), + email: String::from("would@be.forever"), + ssh_key: Some(String::from("ssh-rsa nananananananaaa")), + not_a_robot: true, + }, + common::BCRYPT_COST, + &db, + ) + .await + .unwrap(); + + assert_eq!( + user.state, + UserState::Active, + "user should be active before state change" + ); + + let response = http_client + .post(format!("/users/{}/change_state", user.username)) + .header(ContentType::Form) + .header(Accept::JSON) + .body("state=Disabled") + .dispatch() + .await; + + assert_eq!( + response.status(), + Status::NoContent, + "admin should be able to disable user" + ); + + let user = user.reload(&db).await.expect("reload user"); + + assert_eq!( + user.state, + UserState::Disabled, + "user should be disabled after state change" + ); + }) + .await; +}