Skip to main content

doido_auth/
validations.rs

1//! `validatable` module — email format + password length checks on registration
2//! (the Devise `validatable` analogue). Gated at runtime by `auth.modules`.
3
4use crate::config::AuthModule;
5use crate::error::AuthError;
6use crate::state::try_global;
7
8/// Validate an email/password pair for registration when the `validatable`
9/// module is enabled. A no-op when auth state isn't initialised (unit tests) or
10/// the module is disabled, so callers can invoke it unconditionally.
11pub fn validate_registration(email: &str, password: &str) -> Result<(), AuthError> {
12    let state = match try_global() {
13        Some(state) => state,
14        None => return Ok(()),
15    };
16    if !state.config.has_module(AuthModule::Validatable) {
17        return Ok(());
18    }
19    validate_email(email)?;
20    validate_password_length(password, state.config.password_length)?;
21    Ok(())
22}
23
24/// A conservative email format check (`local@domain`, a dot in the domain, no
25/// whitespace). Returns a [`AuthError::Validation`] on failure.
26pub fn validate_email(email: &str) -> Result<(), AuthError> {
27    if is_valid_email(email) {
28        Ok(())
29    } else {
30        Err(AuthError::Validation("email is invalid".into()))
31    }
32}
33
34/// Enforce a minimum password length.
35pub fn validate_password_length(password: &str, min: usize) -> Result<(), AuthError> {
36    if password.chars().count() >= min {
37        Ok(())
38    } else {
39        Err(AuthError::Validation(format!(
40            "password is too short (minimum is {min} characters)"
41        )))
42    }
43}
44
45fn is_valid_email(email: &str) -> bool {
46    let email = email.trim();
47    if email.is_empty() || email.chars().any(char::is_whitespace) {
48        return false;
49    }
50    match email.split_once('@') {
51        Some((local, domain)) => {
52            !local.is_empty()
53                && domain.contains('.')
54                && !domain.starts_with('.')
55                && !domain.ends_with('.')
56        }
57        None => false,
58    }
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64
65    #[test]
66    fn accepts_reasonable_emails() {
67        for e in ["a@b.com", "user.name@example.co", "x@sub.domain.io"] {
68            assert!(validate_email(e).is_ok(), "{e} should be valid");
69        }
70    }
71
72    #[test]
73    fn rejects_bad_emails() {
74        for e in ["", "no-at", "a@b", "@b.com", "a b@c.com", "a@.com", "a@b."] {
75            assert!(validate_email(e).is_err(), "{e} should be invalid");
76        }
77    }
78
79    #[test]
80    fn enforces_minimum_password_length() {
81        assert!(validate_password_length("secret", 6).is_ok());
82        assert!(validate_password_length("short", 6).is_err());
83    }
84}