fc-common: validation and roles modules
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: Idc4d0743153c77b4dd915a95a603680f6a6a6964
This commit is contained in:
parent
8d07063d3f
commit
a3155f54e8
7 changed files with 324 additions and 0 deletions
206
crates/common/src/validation.rs
Normal file
206
crates/common/src/validation.rs
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
//! Input validation utilities for FC
|
||||
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use regex::Regex;
|
||||
|
||||
/// Username validation: 3-32 chars, alphanumeric + underscore + hyphen
|
||||
static USERNAME_REGEX: LazyLock<Regex> = 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<Regex> = 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(|c| c.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<char> =
|
||||
"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(())
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue