use actix_web::{http::header::LOCATION, web, HttpResponse, Responder}; use brass_db::{ models::{Function, Role, User, UserChangeset}, validation::{AsyncValidate, DbContext}, }; use sqlx::PgPool; use crate::{ endpoints::{user::NewOrEditUserForm, IdPath}, utils::ApplicationError, }; #[actix_web::post("/users/edit/{id}")] pub async fn post_edit( user: web::ReqData, pool: web::Data, path: web::Path, form: web::Form, ) -> Result { if user.role != Role::AreaManager && user.role != Role::Admin { return Err(ApplicationError::Unauthorized); } if user.id == path.id { return Ok(HttpResponse::BadRequest().finish()); } let Some(user_in_db) = User::read_by_id(pool.get_ref(), path.id).await? else { return Ok(HttpResponse::NotFound().finish()); }; let role = form.role.try_into()?; if user.role == Role::AreaManager && (user.area_id != user_in_db.area_id || role == Role::Admin) { return Err(ApplicationError::Unauthorized); } let area_id = form.area.unwrap_or(user_in_db.area_id); let mut functions = Vec::with_capacity(3); if form.is_posten.unwrap_or(false) { functions.push(Function::Posten); } if form.is_wachhabender.unwrap_or(false) { functions.push(Function::Wachhabender); } if form.is_fuehrungsassistent.unwrap_or(false) { functions.push(Function::Fuehrungsassistent); } let changeset = UserChangeset { name: form.name.clone(), email: form.email.to_lowercase(), role, functions, area_id, }; if let Some(existing_id) = User::exists(pool.get_ref(), &changeset.email).await? { if existing_id != user_in_db.id { return Ok(HttpResponse::UnprocessableEntity() .body("email: an user already exists with the same email")); } } let context = DbContext::new(pool.get_ref()); if let Err(e) = changeset.validate_with_context(&context).await { return Ok(HttpResponse::UnprocessableEntity().body(e.to_string())); }; User::update(pool.get_ref(), user_in_db.id, changeset).await?; Ok(HttpResponse::Found() .insert_header((LOCATION, "/users")) .insert_header(("HX-LOCATION", "/users")) .finish()) } #[cfg(test)] mod tests { use crate::{endpoints::user::NewOrEditUserForm, utils::test_helper::*}; use brass_db::models::*; use brass_macros::db_test; use fake::{ faker::{internet::en::SafeEmail, name::en::Name}, Fake, Faker, }; use tracing::debug; #[db_test] async fn works_when_user_is_admin(context: &DbTestContext) { User::create(&context.db_pool, &Faker.fake()).await.unwrap(); Area::create(&context.db_pool, "Süd").await.unwrap(); let app = context.app().await; let config = RequestConfig { uri: "/users/edit/1".to_string(), role: Role::Admin, function: vec![Function::Posten], user_area: 1, }; let new_name: String = Name().fake(); let new_mail: String = SafeEmail().fake(); let form = NewOrEditUserForm { name: new_name.clone(), email: new_mail.clone(), role: Role::AreaManager as u8, is_posten: None, is_wachhabender: None, is_fuehrungsassistent: Some(true), area: Some(2), }; let response = test_post(&context.db_pool, app, &config, Some(form)).await; assert_eq!(StatusCode::FOUND, response.status()); let updated_user = User::read_by_id(&context.db_pool, 1) .await .unwrap() .unwrap(); assert_eq!(new_name, updated_user.name); assert_eq!(new_mail, updated_user.email); assert_eq!(Role::AreaManager, updated_user.role); assert!(updated_user.function.is_fuehrungsassistent()); assert_eq!(2, updated_user.area_id); } #[db_test] async fn cant_edit_oneself(context: &DbTestContext) { let app = context.app().await; let config = RequestConfig { uri: "/users/edit/1".to_string(), role: Role::Admin, function: vec![Function::Posten], user_area: 1, }; let form = NewOrEditUserForm { name: "".to_string(), email: "".to_string(), role: Role::AreaManager as u8, is_posten: None, is_wachhabender: None, is_fuehrungsassistent: Some(true), area: Some(1), }; let response = test_post(&context.db_pool, app, &config, Some(form)).await; assert_eq!(StatusCode::BAD_REQUEST, response.status()); } #[db_test] async fn email_gets_cast_to_lowercase(context: &DbTestContext) { User::create(&context.db_pool, &Faker.fake()).await.unwrap(); let app = context.app().await; let config = RequestConfig::new("/users/edit/1").with_role(Role::Admin); let new_name: String = Name().fake(); let new_mail: String = String::from("NONLowercaseEMAIL@example.com"); let form = NewOrEditUserForm { name: new_name.clone(), email: new_mail.clone(), role: Role::AreaManager as u8, is_posten: None, is_wachhabender: None, is_fuehrungsassistent: Some(true), area: Some(1), }; let response = test_post(&context.db_pool, app, &config, Some(form)).await; let (status, body) = response.into_status_and_body().await; debug!(body); assert_eq!(StatusCode::FOUND, status); let updated_user = User::read_by_id(&context.db_pool, 1) .await .unwrap() .unwrap(); assert_eq!(new_mail.to_lowercase(), updated_user.email); } #[db_test] async fn fails_when_email_already_present(context: &DbTestContext) { User::create(&context.db_pool, &Faker.fake()).await.unwrap(); User::create(&context.db_pool, &Faker.fake()).await.unwrap(); Area::create(&context.db_pool, "Süd").await.unwrap(); let app = context.app().await; let config = RequestConfig::new("/users/edit/1").with_role(Role::Admin); let second_user = User::read_by_id(&context.db_pool, 2) .await .unwrap() .unwrap(); let new_name: String = Name().fake(); let new_mail: String = second_user.email; let form = NewOrEditUserForm { name: new_name.clone(), email: new_mail.clone(), role: Role::AreaManager as u8, is_posten: None, is_wachhabender: None, is_fuehrungsassistent: Some(true), area: Some(2), }; let response = test_post(&context.db_pool, app, &config, Some(form)).await; assert_eq!(StatusCode::UNPROCESSABLE_ENTITY, response.status()); } }