Files
fast-next-template/backend/app/schemas/validators.py
Felipe Cardoso a410586cfb Enable demo mode features, auto-fill demo credentials, and enhance branding integration
- Added `DEMO_MODE` to backend configuration with relaxed security support for specific demo accounts.
- Updated password validators to allow predefined weak passwords in demo mode.
- Auto-fill login forms with demo credentials via query parameters for improved demo accessibility.
- Introduced demo user creation logic during database initialization if `DEMO_MODE` is enabled.
- Replaced `img` tags with `next/image` for consistent and optimized visuals in branding elements.
- Refined footer, header, and layout components to incorporate improved logo handling.
2025-11-21 07:42:40 +01:00

212 lines
6.0 KiB
Python

"""
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 if we are in demo mode
from app.core.config import settings
if settings.DEMO_MODE:
# In demo mode, allow specific weak passwords for demo accounts
demo_passwords = {"Demo123!", "Admin123!"}
if password in demo_passwords:
return password
# 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