""" Shared validators for Pydantic schemas. This module provides reusable validation functions to ensure consistency across all schemas and avoid code duplication. """ import re # Common weak passwords that should be rejected COMMON_PASSWORDS: set[str] = { "password", "password1", "password123", "password1234", "admin", "admin123", "admin1234", "welcome", "welcome1", "welcome123", "qwerty", "qwerty123", "12345678", "123456789", "1234567890", "letmein", "letmein1", "letmein123", "monkey123", "dragon123", "passw0rd", "p@ssw0rd", "p@ssword", } def validate_password_strength(password: str) -> str: """ Validate password strength with enterprise-grade requirements. Requirements: - Minimum 12 characters (increased from 8 for better security) - At least one lowercase letter - At least one uppercase letter - At least one digit - At least one special character - Not in common password list Args: password: The password to validate Returns: The validated password Raises: ValueError: If password doesn't meet requirements Examples: >>> validate_password_strength("MySecureP@ss123") # Valid >>> validate_password_strength("password1") # Invalid - too weak """ # Check minimum length if len(password) < 12: raise ValueError("Password must be at least 12 characters long") # Check against common passwords (case-insensitive) if password.lower() in COMMON_PASSWORDS: raise ValueError("Password is too common. Please choose a stronger password") # Check for required character types checks = [ (any(c.islower() for c in password), "at least one lowercase letter"), (any(c.isupper() for c in password), "at least one uppercase letter"), (any(c.isdigit() for c in password), "at least one digit"), ( any(c in "!@#$%^&*()_+-=[]{}|;:,.<>?~`" for c in password), "at least one special character (!@#$%^&*()_+-=[]{}|;:,.<>?~`)", ), ] failed = [msg for check, msg in checks if not check] if failed: raise ValueError(f"Password must contain {', '.join(failed)}") return password def validate_phone_number(phone: str | None) -> str | None: """ Validate phone number format. Accepts international format with + prefix or local format with 0 prefix. Removes formatting characters (spaces, hyphens, parentheses). Args: phone: Phone number to validate (can be None) Returns: Cleaned phone number or None Raises: ValueError: If phone number format is invalid Examples: >>> validate_phone_number("+1 (555) 123-4567") # Valid >>> validate_phone_number("0412 345 678") # Valid >>> validate_phone_number("invalid") # Invalid """ if phone is None: return None # Check for empty strings if not phone or phone.strip() == "": raise ValueError("Phone number cannot be empty") # Remove all spaces and formatting characters cleaned = re.sub(r"[\s\-\(\)]", "", phone) # Basic pattern: # Must start with + or 0 # After + must have at least 8 digits # After 0 must have at least 8 digits # Maximum total length of 15 digits (international standard) # Only allowed characters are + at start and digits pattern = r"^(?:\+[0-9]{8,14}|0[0-9]{8,14})$" if not re.match(pattern, cleaned): raise ValueError("Phone number must start with + or 0 followed by 8-14 digits") # Additional validation to catch specific invalid cases # NOTE: These checks are defensive code - the regex pattern above already catches these cases if cleaned.count("+") > 1: # pragma: no cover raise ValueError("Phone number can only contain one + symbol at the start") # Check for any non-digit characters (except the leading +) if not all(c.isdigit() for c in cleaned[1:]): # pragma: no cover raise ValueError("Phone number can only contain digits after the prefix") return cleaned def validate_email_format(email: str) -> str: """ Additional email validation beyond Pydantic's EmailStr. This can be extended for custom email validation rules. Args: email: Email address to validate Returns: Validated email address Raises: ValueError: If email format is invalid """ # Pydantic's EmailStr already does comprehensive validation # This function is here for custom rules if needed # Example: Reject disposable email domains (optional) # disposable_domains = {'tempmail.com', '10minutemail.com', 'guerrillamail.com'} # domain = email.split('@')[1].lower() # if domain in disposable_domains: # raise ValueError('Disposable email addresses are not allowed') return email.lower() # Normalize to lowercase def validate_slug(slug: str) -> str: """ Validate URL slug format. Slugs must: - Be 2-50 characters long - Contain only lowercase letters, numbers, and hyphens - Not start or end with a hyphen - Not contain consecutive hyphens Args: slug: URL slug to validate Returns: Validated slug Raises: ValueError: If slug format is invalid """ if not slug or len(slug) < 2: raise ValueError("Slug must be at least 2 characters long") if len(slug) > 50: raise ValueError("Slug must be at most 50 characters long") # Check format if not re.match(r"^[a-z0-9]+(?:-[a-z0-9]+)*$", slug): raise ValueError( "Slug can only contain lowercase letters, numbers, and hyphens. " "It cannot start or end with a hyphen, and cannot contain consecutive hyphens" ) return slug