refactor: Introduce utility functions for exception handling and database operations; update auth and playlist services to use new exception methods
All checks were successful
Backend CI / test (push) Successful in 3m58s

This commit is contained in:
JSC
2025-07-31 13:28:06 +02:00
parent f24698e3ff
commit b8346ab667
9 changed files with 679 additions and 122 deletions

140
app/utils/validation.py Normal file
View File

@@ -0,0 +1,140 @@
"""Common validation utility functions."""
import re
from pathlib import Path
from typing import Any, Optional
# Password validation constants
MIN_PASSWORD_LENGTH = 8
def validate_email(email: str) -> bool:
"""Validate email address format.
Args:
email: Email address to validate
Returns:
True if email format is valid, False otherwise
"""
pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
return bool(re.match(pattern, email))
def validate_password_strength(password: str) -> tuple[bool, str | None]:
"""Validate password meets security requirements.
Args:
password: Password to validate
Returns:
Tuple of (is_valid, error_message)
"""
if len(password) < MIN_PASSWORD_LENGTH:
msg = f"Password must be at least {MIN_PASSWORD_LENGTH} characters long"
return False, msg
if not re.search(r"[A-Z]", password):
return False, "Password must contain at least one uppercase letter"
if not re.search(r"[a-z]", password):
return False, "Password must contain at least one lowercase letter"
if not re.search(r"\d", password):
return False, "Password must contain at least one number"
return True, None
def validate_filename(
filename: str, allowed_extensions: list[str] | None = None
) -> bool:
"""Validate filename format and extension.
Args:
filename: Filename to validate
allowed_extensions: List of allowed file extensions (with dots)
Returns:
True if filename is valid, False otherwise
"""
if not filename or filename.startswith(".") or "/" in filename or "\\" in filename:
return False
if allowed_extensions:
file_path = Path(filename)
return file_path.suffix.lower() in [ext.lower() for ext in allowed_extensions]
return True
def validate_audio_filename(filename: str) -> bool:
"""Validate audio filename has allowed extension.
Args:
filename: Audio filename to validate
Returns:
True if filename has valid audio extension, False otherwise
"""
audio_extensions = [".mp3", ".wav", ".flac", ".ogg", ".m4a", ".aac", ".wma"]
return validate_filename(filename, audio_extensions)
def sanitize_filename(filename: str) -> str:
"""Sanitize filename by removing/replacing invalid characters.
Args:
filename: Filename to sanitize
Returns:
Sanitized filename safe for filesystem
"""
# Remove or replace problematic characters
sanitized = re.sub(r'[<>:"/\\|?*]', "_", filename)
# Remove leading/trailing whitespace and dots
sanitized = sanitized.strip(" .")
# Ensure not empty
if not sanitized:
sanitized = "untitled"
return sanitized
def validate_url(url: str) -> bool:
"""Validate URL format.
Args:
url: URL to validate
Returns:
True if URL format is valid, False otherwise
"""
pattern = r"^https?://[^\s/$.?#].[^\s]*$"
return bool(re.match(pattern, url))
def validate_positive_integer(value: Any, field_name: str = "value") -> int:
"""Validate and convert value to positive integer.
Args:
value: Value to validate and convert
field_name: Name of field for error messages
Returns:
Validated positive integer
Raises:
ValueError: If value is not a positive integer
"""
try:
int_value = int(value)
if int_value <= 0:
msg = f"{field_name} must be a positive integer"
raise ValueError(msg)
return int_value
except (TypeError, ValueError) as e:
msg = f"{field_name} must be a positive integer"
raise ValueError(msg) from e