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-panic = "0.1.2"
|
||||
rust_xlsxwriter = "0.87.0"
|
||||
regex = "1.11.1"
|
||||
|
||||
[build-dependencies]
|
||||
built = "0.7.4"
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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()));
|
||||
};
|
||||
|
||||
|
@ -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<Function>,
|
||||
/// 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(())
|
||||
}
|
||||
|
@ -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};
|
||||
|
||||
|
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