Skip to main content

authx_core/
validation.rs

1use email_address::EmailAddress;
2
3use crate::error::{AuthError, Result};
4
5/// Validate that `email` is a well-formed RFC 5322 address.
6pub fn validate_email(email: &str) -> Result<()> {
7    if EmailAddress::is_valid(email) {
8        Ok(())
9    } else {
10        Err(AuthError::Internal(format!(
11            "invalid email address: {email}"
12        )))
13    }
14}
15
16/// Validate that `slug` is a valid org slug: lowercase alphanumeric and hyphens,
17/// 2–63 characters, must not start or end with a hyphen.
18pub fn validate_slug(slug: &str) -> Result<()> {
19    let ok = !slug.is_empty()
20        && slug.len() >= 2
21        && slug.len() <= 63
22        && slug
23            .chars()
24            .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
25        && !slug.starts_with('-')
26        && !slug.ends_with('-');
27    if ok {
28        Ok(())
29    } else {
30        Err(AuthError::Internal(format!(
31            "invalid org slug '{slug}': must be 2–63 lowercase alphanumeric/hyphen characters, not starting or ending with a hyphen"
32        )))
33    }
34}
35
36/// Validate password meets minimum security requirements:
37/// - At least `min_len` characters
38/// - At least one uppercase letter
39/// - At least one digit
40/// - At least one special character
41pub fn validate_password(password: &str, min_len: usize) -> Result<()> {
42    if password.len() < min_len {
43        return Err(AuthError::WeakPassword);
44    }
45    if !password.chars().any(|c| c.is_ascii_uppercase()) {
46        return Err(AuthError::WeakPassword);
47    }
48    if !password.chars().any(|c| c.is_ascii_digit()) {
49        return Err(AuthError::WeakPassword);
50    }
51    if !password
52        .chars()
53        .any(|c| !c.is_alphanumeric() && c.is_ascii())
54    {
55        return Err(AuthError::WeakPassword);
56    }
57    Ok(())
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63
64    #[test]
65    fn valid_slug_passes() {
66        assert!(validate_slug("my-org").is_ok());
67        assert!(validate_slug("acme").is_ok());
68        assert!(validate_slug("org-123").is_ok());
69    }
70
71    #[test]
72    fn invalid_slug_rejected() {
73        assert!(validate_slug("").is_err());
74        assert!(validate_slug("a").is_err()); // too short
75        assert!(validate_slug("-leading").is_err());
76        assert!(validate_slug("trailing-").is_err());
77        assert!(validate_slug("has space").is_err());
78        assert!(validate_slug("UPPER").is_err());
79        assert!(validate_slug(&"a".repeat(64)).is_err()); // too long
80    }
81
82    #[test]
83    fn valid_email_passes() {
84        assert!(validate_email("user@example.com").is_ok());
85        assert!(validate_email("user+tag@sub.domain.io").is_ok());
86    }
87
88    #[test]
89    fn invalid_email_rejected() {
90        assert!(validate_email("notanemail").is_err());
91        assert!(validate_email("@nodomain").is_err());
92        assert!(validate_email("missing@").is_err());
93        assert!(validate_email("").is_err());
94    }
95
96    #[test]
97    fn strong_password_passes() {
98        assert!(validate_password("Secure@123", 8).is_ok());
99        assert!(validate_password("Tr0ub4dor&3", 8).is_ok());
100    }
101
102    #[test]
103    fn weak_passwords_rejected() {
104        assert!(validate_password("short", 8).is_err());
105        assert!(validate_password("alllowercase1!", 8).is_err()); // no uppercase
106        assert!(validate_password("NoDigitsHere!", 8).is_err()); // no digit
107        assert!(validate_password("NoSpecial123", 8).is_err()); // no special char
108    }
109}