mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-01-25 22:19:53 -06:00
- Normalize line endings from CRLF to LF across all files to match .editorconfig - Standardize quote style from single quotes to double quotes - Normalize whitespace and formatting throughout codebase - Apply consistent code style across 372 files including: * Application code (models, routes, services, utils) * Test files * Configuration files * CI/CD workflows This ensures consistency with the project's .editorconfig settings and improves code maintainability.
220 lines
5.5 KiB
Python
220 lines
5.5 KiB
Python
"""
|
|
Input validation utilities.
|
|
Provides consistent validation across the application.
|
|
"""
|
|
|
|
from typing import Any, Dict, Optional, List
|
|
from datetime import datetime, date
|
|
from decimal import Decimal, InvalidOperation
|
|
from flask import request
|
|
from marshmallow import ValidationError
|
|
|
|
|
|
def validate_required(data: Dict[str, Any], fields: List[str]) -> Dict[str, Any]:
|
|
"""
|
|
Validate that required fields are present.
|
|
|
|
Args:
|
|
data: Dictionary to validate
|
|
fields: List of required field names
|
|
|
|
Returns:
|
|
dict with 'valid' (bool) and 'errors' (list) keys
|
|
|
|
Raises:
|
|
ValidationError if validation fails
|
|
"""
|
|
errors = []
|
|
for field in fields:
|
|
if field not in data or data[field] is None:
|
|
errors.append(f"{field} is required")
|
|
|
|
if errors:
|
|
raise ValidationError(errors)
|
|
|
|
return {"valid": True, "errors": []}
|
|
|
|
|
|
def validate_date_range(start_date: Any, end_date: Any) -> bool:
|
|
"""
|
|
Validate that end_date is after start_date.
|
|
|
|
Args:
|
|
start_date: Start date (datetime, date, or string)
|
|
end_date: End date (datetime, date, or string)
|
|
|
|
Returns:
|
|
True if valid
|
|
|
|
Raises:
|
|
ValidationError if invalid
|
|
"""
|
|
if isinstance(start_date, str):
|
|
start_date = datetime.fromisoformat(start_date.replace("Z", "+00:00"))
|
|
if isinstance(end_date, str):
|
|
end_date = datetime.fromisoformat(end_date.replace("Z", "+00:00"))
|
|
|
|
if isinstance(start_date, datetime):
|
|
start_date = start_date.date()
|
|
if isinstance(end_date, datetime):
|
|
end_date = end_date.date()
|
|
|
|
if end_date <= start_date:
|
|
raise ValidationError("end_date must be after start_date")
|
|
|
|
return True
|
|
|
|
|
|
def validate_decimal(value: Any, min_value: Optional[Decimal] = None, max_value: Optional[Decimal] = None) -> Decimal:
|
|
"""
|
|
Validate and convert a value to Decimal.
|
|
|
|
Args:
|
|
value: Value to validate
|
|
min_value: Minimum allowed value
|
|
max_value: Maximum allowed value
|
|
|
|
Returns:
|
|
Decimal value
|
|
|
|
Raises:
|
|
ValidationError if invalid
|
|
"""
|
|
try:
|
|
decimal_value = Decimal(str(value))
|
|
except (ValueError, InvalidOperation, TypeError):
|
|
raise ValidationError(f"Invalid decimal value: {value}")
|
|
|
|
if min_value is not None and decimal_value < min_value:
|
|
raise ValidationError(f"Value must be at least {min_value}")
|
|
|
|
if max_value is not None and decimal_value > max_value:
|
|
raise ValidationError(f"Value must be at most {max_value}")
|
|
|
|
return decimal_value
|
|
|
|
|
|
def validate_integer(value: Any, min_value: Optional[int] = None, max_value: Optional[int] = None) -> int:
|
|
"""
|
|
Validate and convert a value to integer.
|
|
|
|
Args:
|
|
value: Value to validate
|
|
min_value: Minimum allowed value
|
|
max_value: Maximum allowed value
|
|
|
|
Returns:
|
|
Integer value
|
|
|
|
Raises:
|
|
ValidationError if invalid
|
|
"""
|
|
try:
|
|
int_value = int(value)
|
|
except (ValueError, TypeError):
|
|
raise ValidationError(f"Invalid integer value: {value}")
|
|
|
|
if min_value is not None and int_value < min_value:
|
|
raise ValidationError(f"Value must be at least {min_value}")
|
|
|
|
if max_value is not None and int_value > max_value:
|
|
raise ValidationError(f"Value must be at most {max_value}")
|
|
|
|
return int_value
|
|
|
|
|
|
def validate_string(value: Any, min_length: Optional[int] = None, max_length: Optional[int] = None) -> str:
|
|
"""
|
|
Validate and convert a value to string.
|
|
|
|
Args:
|
|
value: Value to validate
|
|
min_length: Minimum string length
|
|
max_length: Maximum string length
|
|
|
|
Returns:
|
|
String value
|
|
|
|
Raises:
|
|
ValidationError if invalid
|
|
"""
|
|
if value is None:
|
|
raise ValidationError("String value cannot be None")
|
|
|
|
str_value = str(value).strip()
|
|
|
|
if min_length is not None and len(str_value) < min_length:
|
|
raise ValidationError(f"String must be at least {min_length} characters")
|
|
|
|
if max_length is not None and len(str_value) > max_length:
|
|
raise ValidationError(f"String must be at most {max_length} characters")
|
|
|
|
return str_value
|
|
|
|
|
|
def validate_email(email: str) -> str:
|
|
"""
|
|
Validate email address format.
|
|
|
|
Args:
|
|
email: Email address to validate
|
|
|
|
Returns:
|
|
Validated email address
|
|
|
|
Raises:
|
|
ValidationError if invalid
|
|
"""
|
|
import re
|
|
|
|
email = email.strip().lower()
|
|
pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
|
|
|
|
if not re.match(pattern, email):
|
|
raise ValidationError(f"Invalid email address: {email}")
|
|
|
|
return email
|
|
|
|
|
|
def validate_json_request() -> Dict[str, Any]:
|
|
"""
|
|
Validate that request contains valid JSON.
|
|
|
|
Returns:
|
|
Parsed JSON data
|
|
|
|
Raises:
|
|
ValidationError if invalid
|
|
"""
|
|
if not request.is_json:
|
|
raise ValidationError("Request must contain JSON data")
|
|
|
|
data = request.get_json()
|
|
if data is None:
|
|
raise ValidationError("Request JSON is empty")
|
|
|
|
return data
|
|
|
|
|
|
def sanitize_input(value: str, max_length: Optional[int] = None) -> str:
|
|
"""
|
|
Sanitize user input by removing dangerous characters.
|
|
|
|
Args:
|
|
value: Input string
|
|
max_length: Maximum length to truncate to
|
|
|
|
Returns:
|
|
Sanitized string
|
|
"""
|
|
import bleach
|
|
|
|
# Remove HTML tags and dangerous characters
|
|
sanitized = bleach.clean(value, tags=[], strip=True)
|
|
|
|
# Truncate if needed
|
|
if max_length and len(sanitized) > max_length:
|
|
sanitized = sanitized[:max_length]
|
|
|
|
return sanitized
|