feat: custom async validation
This commit is contained in:
parent
0b4248604a
commit
9666932915
@ -37,6 +37,7 @@ tracing = "0.1.41"
|
|||||||
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
||||||
tracing-panic = "0.1.2"
|
tracing-panic = "0.1.2"
|
||||||
rust_xlsxwriter = "0.87.0"
|
rust_xlsxwriter = "0.87.0"
|
||||||
|
regex = "1.11.1"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
built = "0.7.4"
|
built = "0.7.4"
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
use actix_web::{http::header::LOCATION, web, HttpResponse, Responder};
|
use actix_web::{http::header::LOCATION, web, HttpResponse, Responder};
|
||||||
use garde::Validate;
|
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
endpoints::{user::NewOrEditUserForm, IdPath},
|
endpoints::{user::NewOrEditUserForm, IdPath},
|
||||||
models::{Area, Function, Role, User, UserChangeset},
|
models::{Function, Role, User, UserChangeset},
|
||||||
utils::ApplicationError,
|
utils::{validation::AsyncValidate, ApplicationError},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[actix_web::post("/users/edit/{id}")]
|
#[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);
|
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);
|
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()));
|
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;
|
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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
use actix_web::{http::header::LOCATION, web, HttpResponse, Responder};
|
use actix_web::{http::header::LOCATION, web, HttpResponse, Responder};
|
||||||
use garde::Validate;
|
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
endpoints::user::NewOrEditUserForm,
|
endpoints::user::NewOrEditUserForm,
|
||||||
mail::Mailer,
|
mail::Mailer,
|
||||||
models::{Function, Registration, Role, User, UserChangeset},
|
models::{Function, Registration, Role, User, UserChangeset},
|
||||||
utils::ApplicationError,
|
utils::{validation::AsyncValidate, ApplicationError},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[actix_web::post("/users/new")]
|
#[actix_web::post("/users/new")]
|
||||||
@ -58,7 +57,7 @@ pub async fn post_new(
|
|||||||
.body("email: an user already exists with the same email"));
|
.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()));
|
return Ok(HttpResponse::UnprocessableEntity().body(e.to_string()));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,24 +1,42 @@
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use fake::{faker::internet::en::SafeEmail, faker::name::en::Name, Dummy};
|
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))]
|
#[cfg_attr(test, derive(Dummy))]
|
||||||
#[garde(allow_unvalidated)]
|
|
||||||
pub struct UserChangeset {
|
pub struct UserChangeset {
|
||||||
#[cfg_attr(test, dummy(faker = "Name()"))]
|
#[cfg_attr(test, dummy(faker = "Name()"))]
|
||||||
pub name: String,
|
pub name: String,
|
||||||
#[garde(email)]
|
|
||||||
#[cfg_attr(test, dummy(faker = "SafeEmail()"))]
|
#[cfg_attr(test, dummy(faker = "SafeEmail()"))]
|
||||||
pub email: String,
|
pub email: String,
|
||||||
#[cfg_attr(test, dummy(expr = "Role::Staff"))]
|
#[cfg_attr(test, dummy(expr = "Role::Staff"))]
|
||||||
pub role: Role,
|
pub role: Role,
|
||||||
#[cfg_attr(test, dummy(expr = "vec![Function::Posten]"))]
|
#[cfg_attr(test, dummy(expr = "vec![Function::Posten]"))]
|
||||||
pub functions: Vec<Function>,
|
pub functions: Vec<Function>,
|
||||||
/// check before: must exist and user can create other user for this area
|
|
||||||
#[cfg_attr(test, dummy(expr = "1"))]
|
#[cfg_attr(test, dummy(expr = "1"))]
|
||||||
pub area_id: i32,
|
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(())
|
||||||
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
mod app_customization;
|
||||||
mod application_error;
|
mod application_error;
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
mod date_time_format;
|
mod date_time_format;
|
||||||
@ -5,16 +6,16 @@ pub mod event_planning_template;
|
|||||||
pub mod manage_commands;
|
pub mod manage_commands;
|
||||||
pub mod password_change;
|
pub mod password_change;
|
||||||
mod template_response_trait;
|
mod template_response_trait;
|
||||||
mod app_customization;
|
|
||||||
pub mod token_generation;
|
pub mod token_generation;
|
||||||
|
pub mod validation;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub mod test_helper;
|
pub mod test_helper;
|
||||||
|
|
||||||
|
pub use app_customization::Customization;
|
||||||
pub use application_error::ApplicationError;
|
pub use application_error::ApplicationError;
|
||||||
pub use date_time_format::DateTimeFormat;
|
pub use date_time_format::DateTimeFormat;
|
||||||
pub use template_response_trait::TemplateResponse;
|
pub use template_response_trait::TemplateResponse;
|
||||||
pub use app_customization::Customization;
|
|
||||||
|
|
||||||
use chrono::{NaiveDate, Utc};
|
use chrono::{NaiveDate, Utc};
|
||||||
|
|
||||||
|
58
web/src/utils/validation/email.rs
Normal file
58
web/src/utils/validation/email.rs
Normal file
@ -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());
|
||||||
|
}
|
29
web/src/utils/validation/error.rs
Normal file
29
web/src/utils/validation/error.rs
Normal file
@ -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<sqlx::Error> for AsyncValidateError {
|
||||||
|
fn from(value: sqlx::Error) -> Self {
|
||||||
|
error!(error = %value, "database error while validation input");
|
||||||
|
AsyncValidateError::new("Datenbankfehler beim Validieren!")
|
||||||
|
}
|
||||||
|
}
|
7
web/src/utils/validation/mod.rs
Normal file
7
web/src/utils/validation/mod.rs
Normal file
@ -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;
|
7
web/src/utils/validation/trait.rs
Normal file
7
web/src/utils/validation/trait.rs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
use sqlx::PgPool;
|
||||||
|
|
||||||
|
use super::AsyncValidateError;
|
||||||
|
|
||||||
|
pub trait AsyncValidate {
|
||||||
|
async fn validate_with_pool(&self, pool: &PgPool) -> Result<(), AsyncValidateError>;
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user