//! Input validation utilities for FC use std::sync::LazyLock; use regex::Regex; /// Username validation: 3-32 chars, alphanumeric + underscore + hyphen static USERNAME_REGEX: LazyLock = LazyLock::new(|| { Regex::new(r"^[a-zA-Z0-9_-]{3,32}$").expect("Invalid username regex pattern") }); /// Email validation (basic RFC 5322 compliant) static EMAIL_REGEX: LazyLock = LazyLock::new(|| { Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$") .expect("Invalid email regex pattern") }); /// Validation errors #[derive(Debug, Clone)] pub struct ValidationError { pub field: String, pub message: String, } impl std::fmt::Display for ValidationError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}: {}", self.field, self.message) } } impl std::error::Error for ValidationError {} /// Validate username format /// Requirements: /// - 3-32 characters /// - Alphanumeric, underscore, hyphen only pub fn validate_username(username: &str) -> Result<(), ValidationError> { if username.is_empty() { return Err(ValidationError { field: "username".to_string(), message: "Username is required".to_string(), }); } if !USERNAME_REGEX.is_match(username) { return Err(ValidationError { field: "username".to_string(), message: "Username must be 3-32 characters and contain only letters, \ numbers, underscores, and hyphens" .to_string(), }); } Ok(()) } /// Validate email format pub fn validate_email(email: &str) -> Result<(), ValidationError> { if email.is_empty() { return Err(ValidationError { field: "email".to_string(), message: "Email is required".to_string(), }); } if !EMAIL_REGEX.is_match(email) { return Err(ValidationError { field: "email".to_string(), message: "Invalid email format".to_string(), }); } Ok(()) } /// Validate password strength /// Requirements: /// - At least 12 characters /// - At least one uppercase letter /// - At least one lowercase letter /// - At least one number /// - At least one special character pub fn validate_password(password: &str) -> Result<(), ValidationError> { if password.len() < 12 { return Err(ValidationError { field: "password".to_string(), message: "Password must be at least 12 characters".to_string(), }); } let has_upper = password.chars().any(|c| c.is_ascii_uppercase()); let has_lower = password.chars().any(|c| c.is_ascii_lowercase()); let has_digit = password.chars().any(|c| c.is_ascii_digit()); let has_special = password.chars().any(|c| !c.is_ascii_alphanumeric()); if !has_upper { return Err(ValidationError { field: "password".to_string(), message: "Password must contain at least one uppercase letter" .to_string(), }); } if !has_lower { return Err(ValidationError { field: "password".to_string(), message: "Password must contain at least one lowercase letter" .to_string(), }); } if !has_digit { return Err(ValidationError { field: "password".to_string(), message: "Password must contain at least one number".to_string(), }); } if !has_special { return Err(ValidationError { field: "password".to_string(), message: "Password must contain at least one special character" .to_string(), }); } Ok(()) } /// Validate role against allowed roles pub fn validate_role( role: &str, allowed: &[&str], ) -> Result<(), ValidationError> { if role.is_empty() { return Err(ValidationError { field: "role".to_string(), message: "Role is required".to_string(), }); } if !allowed.contains(&role) { return Err(ValidationError { field: "role".to_string(), message: format!("Invalid role. Must be one of: {}", allowed.join(", ")), }); } Ok(()) } /// Validate full name (optional field) /// - Max 255 characters /// - Must not contain control characters pub fn validate_full_name(name: &str) -> Result<(), ValidationError> { if name.len() > 255 { return Err(ValidationError { field: "full_name".to_string(), message: "Full name must be 255 characters or less".to_string(), }); } if name.chars().any(char::is_control) { return Err(ValidationError { field: "full_name".to_string(), message: "Full name cannot contain control characters".to_string(), }); } Ok(()) } /// Validate job name /// Requirements: /// - 1-255 characters /// - Alphanumeric + common path characters pub fn validate_job_name(name: &str) -> Result<(), ValidationError> { if name.is_empty() { return Err(ValidationError { field: "job_name".to_string(), message: "Job name is required".to_string(), }); } if name.len() > 255 { return Err(ValidationError { field: "job_name".to_string(), message: "Job name must be 255 characters or less".to_string(), }); } // Allow alphanumeric, hyphen, underscore, dot, and path separators let valid_chars: std::collections::HashSet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-./" .chars() .collect(); if !name.chars().all(|c| valid_chars.contains(&c)) { return Err(ValidationError { field: "job_name".to_string(), message: "Job name contains invalid characters".to_string(), }); } Ok(()) }