use chrono::{Datelike, NaiveDate, Utc}; use crate::errors::{AuthError, AuthResult}; pub const MAX_USERNAME_LEN: usize = 64; pub const MAX_DISPLAY_NAME_LEN: usize = 64; pub const MIN_EMAIL_LEN: usize = 3; pub const MAX_EMAIL_LEN: usize = 254; pub fn validate_email(raw: &str) -> AuthResult { let email = raw.trim().to_lowercase(); if email.len() < MIN_EMAIL_LEN || email.len() > MAX_EMAIL_LEN { return Err(AuthError::InvalidEmail); } if email.chars().any(char::is_whitespace) { return Err(AuthError::InvalidEmail); } let mut parts = email.split('@'); let (Some(local), Some(domain), None) = (parts.next(), parts.next(), parts.next()) else { return Err(AuthError::InvalidEmail); }; if !valid_local_part(local) || !valid_domain(domain) { return Err(AuthError::InvalidEmail); } Ok(email) } /// 本地部分:不允许空、首尾点与连续点,字符限于 RFC 5321 的常用集合。 fn valid_local_part(local: &str) -> bool { const MAX_LOCAL_LEN: usize = 64; if local.is_empty() || local.len() > MAX_LOCAL_LEN { return false; } if local.starts_with('.') || local.ends_with('.') || local.contains("..") { return false; } local.chars().all(|c| { c.is_ascii_alphanumeric() || matches!( c, '.' | '!' | '#' | '$' | '%' | '&' | '\'' | '*' | '+' | '-' | '/' | '=' | '?' | '^' | '_' | '`' | '{' | '|' | '}' | '~' ) }) } /// 域名:至少两段、每段非空且不以连字符开头/结尾,顶级域纯字母且至少两位。 fn valid_domain(domain: &str) -> bool { const MAX_LABEL_LEN: usize = 63; let labels: Vec<&str> = domain.split('.').collect(); if labels.len() < 2 { return false; } for label in &labels { if label.is_empty() || label.len() > MAX_LABEL_LEN { return false; } if label.starts_with('-') || label.ends_with('-') { return false; } if !label.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') { return false; } } let Some(tld) = labels.last() else { return false; }; tld.len() >= 2 && tld.chars().all(|c| c.is_ascii_alphabetic()) } pub fn validate_username(name: &str) -> AuthResult { let name = name.trim(); if name.is_empty() || name.len() > MAX_USERNAME_LEN { return Err(AuthError::InvalidUsername); } if !name .chars() .all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '.' | '-')) { return Err(AuthError::InvalidUsername); } Ok(name.to_string()) } pub fn validate_display_name(name: &str) -> AuthResult { let name = name.trim(); if name.is_empty() || name.chars().count() > MAX_DISPLAY_NAME_LEN { return Err(AuthError::InvalidDisplayname); } Ok(name.to_string()) } pub fn validate_age(date_of_birth: Option, min_age: u8) -> AuthResult { let Some(birth) = date_of_birth else { return Err(AuthError::InvalidBirthDate); }; let today = Utc::now().date_naive(); if birth > today { return Err(AuthError::InvalidBirthDate); } let mut age = today.year() - birth.year(); if (today.month(), today.day()) < (birth.month(), birth.day()) { age -= 1; } if age < min_age as i32 { return Err(AuthError::UnderMinimumAge { min_age: min_age as u32, }); } Ok(birth) }