Skip to content
Clone
e870254
138 lines 4.1 KB Rust
use chrono::{Datelike, NaiveDate, Utc};
use crate::server::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<String> {
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<String> {
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 derive_username(email: &str, explicit: Option<&str>) -> AuthResult<String> {
match explicit {
Some(name) => validate_username(name),
None => {
let local = email.split('@').next().unwrap_or_default();
validate_username(local.split('+').next().unwrap_or_default())
}
}
}
pub fn validate_display_name(name: &str) -> AuthResult<String> {
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<NaiveDate>, min_age: u8) -> AuthResult<NaiveDate> {
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)
}