diff --git a/web/Cargo.toml b/web/Cargo.toml index 8e9fc5bf..da48813d 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -37,6 +37,7 @@ tracing = "0.1.41" tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } tracing-panic = "0.1.2" rust_xlsxwriter = "0.87.0" +regex = "1.11.1" [build-dependencies] built = "0.7.4" diff --git a/web/src/endpoints/user/post_edit.rs b/web/src/endpoints/user/post_edit.rs index e38d5212..1c786cd2 100644 --- a/web/src/endpoints/user/post_edit.rs +++ b/web/src/endpoints/user/post_edit.rs @@ -1,11 +1,10 @@ use actix_web::{http::header::LOCATION, web, HttpResponse, Responder}; -use garde::Validate; use sqlx::PgPool; use crate::{ endpoints::{user::NewOrEditUserForm, IdPath}, - models::{Area, Function, Role, User, UserChangeset}, - utils::ApplicationError, + models::{Function, Role, User, UserChangeset}, + utils::{validation::AsyncValidate, ApplicationError}, }; #[actix_web::post("/users/edit/{id}")] @@ -34,9 +33,6 @@ pub async fn post_edit( } let area_id = form.area.unwrap_or(user_in_db.area_id); - if Area::read_by_id(pool.get_ref(), area_id).await?.is_none() { - return Err(ApplicationError::Unauthorized); - } let mut functions = Vec::with_capacity(3); @@ -67,7 +63,7 @@ pub async fn post_edit( } } - if let Err(e) = changeset.validate() { + if let Err(e) = changeset.validate_with_pool(pool.get_ref()).await { return Ok(HttpResponse::UnprocessableEntity().body(e.to_string())); }; @@ -214,6 +210,6 @@ mod tests { }; let response = test_post(&context.db_pool, app, &config, form).await; - assert_eq!(StatusCode::BAD_REQUEST, response.status()); + assert_eq!(StatusCode::UNPROCESSABLE_ENTITY, response.status()); } } diff --git a/web/src/endpoints/user/post_new.rs b/web/src/endpoints/user/post_new.rs index dc0a9992..726f50d8 100644 --- a/web/src/endpoints/user/post_new.rs +++ b/web/src/endpoints/user/post_new.rs @@ -1,12 +1,11 @@ use actix_web::{http::header::LOCATION, web, HttpResponse, Responder}; -use garde::Validate; use sqlx::PgPool; use crate::{ endpoints::user::NewOrEditUserForm, mail::Mailer, models::{Function, Registration, Role, User, UserChangeset}, - utils::ApplicationError, + utils::{validation::AsyncValidate, ApplicationError}, }; #[actix_web::post("/users/new")] @@ -58,7 +57,7 @@ pub async fn post_new( .body("email: an user already exists with the same email")); } - if let Err(e) = changeset.validate() { + if let Err(e) = changeset.validate_with_pool(pool.get_ref()).await { return Ok(HttpResponse::UnprocessableEntity().body(e.to_string())); }; diff --git a/web/src/models/user_changeset.rs b/web/src/models/user_changeset.rs index 413858e4..fdcf3731 100644 --- a/web/src/models/user_changeset.rs +++ b/web/src/models/user_changeset.rs @@ -1,24 +1,42 @@ #[cfg(test)] use fake::{faker::internet::en::SafeEmail, faker::name::en::Name, Dummy}; -use garde::Validate; +use sqlx::PgPool; -use super::{Function, Role}; +use crate::utils::validation::{email_is_valid, AsyncValidate, AsyncValidateError}; -#[derive(Debug, Validate)] +use super::{Area, Function, Role}; + +#[derive(Debug)] #[cfg_attr(test, derive(Dummy))] -#[garde(allow_unvalidated)] pub struct UserChangeset { #[cfg_attr(test, dummy(faker = "Name()"))] pub name: String, - #[garde(email)] #[cfg_attr(test, dummy(faker = "SafeEmail()"))] pub email: String, #[cfg_attr(test, dummy(expr = "Role::Staff"))] pub role: Role, #[cfg_attr(test, dummy(expr = "vec![Function::Posten]"))] pub functions: Vec, - /// check before: must exist and user can create other user for this area #[cfg_attr(test, dummy(expr = "1"))] pub area_id: i32, } + +impl AsyncValidate for UserChangeset { + async fn validate_with_pool(&self, pool: &sqlx::PgPool) -> Result<(), AsyncValidateError> { + email_is_valid(&self.email)?; + area_exists(pool, self.area_id).await?; + + Ok(()) + } +} + +async fn area_exists(pool: &PgPool, id: i32) -> Result<(), AsyncValidateError> { + if Area::read_by_id(pool, id).await?.is_none() { + return Err(AsyncValidateError::new( + "Angegebener Bereich für Nutzer existiert nicht!", + )); + } + + Ok(()) +} diff --git a/web/src/utils/mod.rs b/web/src/utils/mod.rs index 435e3a25..a54fc755 100644 --- a/web/src/utils/mod.rs +++ b/web/src/utils/mod.rs @@ -1,3 +1,4 @@ +mod app_customization; mod application_error; pub mod auth; mod date_time_format; @@ -5,16 +6,16 @@ pub mod event_planning_template; pub mod manage_commands; pub mod password_change; mod template_response_trait; -mod app_customization; pub mod token_generation; +pub mod validation; #[cfg(test)] pub mod test_helper; +pub use app_customization::Customization; pub use application_error::ApplicationError; pub use date_time_format::DateTimeFormat; pub use template_response_trait::TemplateResponse; -pub use app_customization::Customization; use chrono::{NaiveDate, Utc}; diff --git a/web/src/utils/validation/email.rs b/web/src/utils/validation/email.rs new file mode 100644 index 00000000..68240732 --- /dev/null +++ b/web/src/utils/validation/email.rs @@ -0,0 +1,58 @@ +// great inspiration taken from https://github.com/jprochazk/garde/blob/main/garde/src/rules/email.rs +use regex::Regex; + +use super::AsyncValidateError; + +pub fn email_is_valid(email: &str) -> Result<(), AsyncValidateError> { + if email.is_empty() { + return Err(AsyncValidateError::new("E-Mail ist leer!")); + } + + let (user, domain) = email + .split_once('@') + .ok_or(AsyncValidateError::new("E-Mail enthält kein '@'!"))?; + + if user.len() > 64 { + return Err(AsyncValidateError::new("Nutzerteil der E-Mail zu lang!")); + } + + let user_re = Regex::new(r"(?i-u)^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+\z").unwrap(); + + if !user_re.is_match(user) { + return Err(AsyncValidateError::new( + "Nutzerteil der E-Mail enthält unerlaubte Zeichen.", + )); + } + + if domain.len() > 255 { + return Err(AsyncValidateError::new( + "Domainteil der E-Mail ist zu lang.", + )); + } + + let domain_re = Regex::new( + r"(?i-u)^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$", + ) + .unwrap(); + + if !domain_re.is_match(domain) { + return Err(AsyncValidateError::new( + "Domainteil der E-Mail enthält unerlaubte Zeichen!", + )); + } + + Ok(()) +} + +#[test] +pub fn email_validation_works_correctly() { + assert!(email_is_valid("abc@example.com").is_ok()); + assert!(email_is_valid("admin.new@example-domain.de").is_ok()); + assert!(email_is_valid("admin!new@sub.web.www.example-domain.de").is_ok()); + + assert!(email_is_valid("admin.domain.de").is_err()); + assert!(email_is_valid("admin@web@domain.de").is_err()); + assert!(email_is_valid("@domain.de").is_err()); + assert!(email_is_valid("user@").is_err()); + assert!(email_is_valid("").is_err()); +} diff --git a/web/src/utils/validation/error.rs b/web/src/utils/validation/error.rs new file mode 100644 index 00000000..43f2ad19 --- /dev/null +++ b/web/src/utils/validation/error.rs @@ -0,0 +1,29 @@ +use tracing::error; + +#[derive(Debug)] +pub struct AsyncValidateError { + message: String, +} + +impl std::fmt::Display for AsyncValidateError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.message) + } +} + +impl std::error::Error for AsyncValidateError {} + +impl AsyncValidateError { + pub fn new(message: &str) -> Self { + AsyncValidateError { + message: message.to_string(), + } + } +} + +impl From for AsyncValidateError { + fn from(value: sqlx::Error) -> Self { + error!(error = %value, "database error while validation input"); + AsyncValidateError::new("Datenbankfehler beim Validieren!") + } +} diff --git a/web/src/utils/validation/mod.rs b/web/src/utils/validation/mod.rs new file mode 100644 index 00000000..a688580e --- /dev/null +++ b/web/src/utils/validation/mod.rs @@ -0,0 +1,7 @@ +mod email; +mod error; +mod r#trait; + +pub use email::email_is_valid; +pub use error::AsyncValidateError; +pub use r#trait::AsyncValidate; diff --git a/web/src/utils/validation/trait.rs b/web/src/utils/validation/trait.rs new file mode 100644 index 00000000..f7bc555b --- /dev/null +++ b/web/src/utils/validation/trait.rs @@ -0,0 +1,7 @@ +use sqlx::PgPool; + +use super::AsyncValidateError; + +pub trait AsyncValidate { + async fn validate_with_pool(&self, pool: &PgPool) -> Result<(), AsyncValidateError>; +}