diff --git a/Cargo.toml b/Cargo.toml index b7c3dc18..e38ad786 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ tracing = ["tracinglib", "tracing-futures"] decimal = ["rust_decimal"] cbor = ["serde_cbor"] chrono-duration = ["chrono", "iso8601-duration"] +password-strength-validator = ["zxcvbn"] [dependencies] async-graphql-derive = { path = "derive", version = "3.0.19" } @@ -63,6 +64,7 @@ uuid = { version = "0.8.2", optional = true, features = ["v4", "serde"] } rust_decimal = { version = "1.14.3", optional = true } url = { version = "2.2.1", optional = true } smol_str = { version = "0.1.21", optional = true } +zxcvbn = { version = "2.1.2", optional = true } # Non-feature optional dependencies blocking = { version = "1.0.2", optional = true } diff --git a/derive/src/validators.rs b/derive/src/validators.rs index 17eb7dfe..ed50eb28 100644 --- a/derive/src/validators.rs +++ b/derive/src/validators.rs @@ -34,6 +34,8 @@ pub struct Validators { #[darling(default)] multiple_of: Option, #[darling(default)] + min_password_strength: Option, + #[darling(default)] maximum: Option, #[darling(default)] minimum: Option, @@ -93,6 +95,12 @@ impl Validators { }); } + if let Some(n) = &self.min_password_strength { + elem_validators.push(quote! { + #crate_name::validators::min_password_strength(__raw_value, #n) + }); + } + if let Some(n) = &self.maximum { elem_validators.push(quote! { #crate_name::validators::maximum(__raw_value, #n) diff --git a/src/validators/min_password_strength.rs b/src/validators/min_password_strength.rs new file mode 100644 index 00000000..510a6dfd --- /dev/null +++ b/src/validators/min_password_strength.rs @@ -0,0 +1,34 @@ +use crate::{InputType, InputValueError}; +use zxcvbn::{zxcvbn, ZxcvbnError}; + +pub fn min_password_strength + InputType>( + value: &T, + min_score: u8, +) -> Result<(), InputValueError> { + match zxcvbn(value.as_ref(), &[]) { + Ok(password_strength) => { + if password_strength.score() < min_score { + Err("password is too weak".into()) + } else { + Ok(()) + } + } + Err(ZxcvbnError::BlankPassword) => Err("password is too weak".into()), + _ => Err("error processing password strength".into()), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_min_password_strength() { + assert!(min_password_strength(&"password".to_string(), 3).is_err()); + assert!(min_password_strength(&"query".to_string(), 3).is_err()); + assert!(min_password_strength(&"P@ssword1".to_string(), 3).is_err()); + assert!(min_password_strength(&"".to_string(), 3).is_err()); + + assert!(min_password_strength(&"Some!Secure!Password".to_string(), 3).is_ok()); + } +} diff --git a/src/validators/mod.rs b/src/validators/mod.rs index 4270dd4c..f5e8537b 100644 --- a/src/validators/mod.rs +++ b/src/validators/mod.rs @@ -7,6 +7,8 @@ mod max_length; mod maximum; mod min_items; mod min_length; +#[cfg(feature = "password-strength-validator")] +mod min_password_strength; mod minimum; mod multiple_of; mod regex; @@ -23,6 +25,8 @@ pub use max_length::max_length; pub use maximum::maximum; pub use min_items::min_items; pub use min_length::min_length; +#[cfg(feature = "password-strength-validator")] +pub use min_password_strength::min_password_strength; pub use minimum::minimum; pub use multiple_of::multiple_of;