Initial commit - Church Music Database
This commit is contained in:
149
legacy-site/backend/validators.py
Normal file
149
legacy-site/backend/validators.py
Normal file
@@ -0,0 +1,149 @@
|
||||
"""
|
||||
Input validation schemas using basic validation
|
||||
For production, consider migrating to Pydantic for comprehensive validation
|
||||
"""
|
||||
|
||||
import re
|
||||
import bleach
|
||||
from functools import wraps
|
||||
from flask import request, jsonify
|
||||
|
||||
class ValidationError(Exception):
|
||||
"""Custom validation error"""
|
||||
pass
|
||||
|
||||
def sanitize_html(text):
|
||||
"""
|
||||
Sanitize HTML to prevent XSS attacks
|
||||
Allows only safe tags and attributes
|
||||
"""
|
||||
if not text:
|
||||
return text
|
||||
|
||||
# Allow minimal formatting tags
|
||||
allowed_tags = ['p', 'br', 'strong', 'em', 'u', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'ul', 'ol', 'pre', 'code']
|
||||
allowed_attributes = {}
|
||||
|
||||
return bleach.clean(
|
||||
text,
|
||||
tags=allowed_tags,
|
||||
attributes=allowed_attributes,
|
||||
strip=True
|
||||
)
|
||||
|
||||
def validate_string(value, field_name, min_length=None, max_length=None, pattern=None, required=True):
|
||||
"""Validate string field"""
|
||||
if value is None or value == '':
|
||||
if required:
|
||||
raise ValidationError(f"{field_name} is required")
|
||||
return True
|
||||
|
||||
if not isinstance(value, str):
|
||||
raise ValidationError(f"{field_name} must be a string")
|
||||
|
||||
if min_length and len(value) < min_length:
|
||||
raise ValidationError(f"{field_name} must be at least {min_length} characters")
|
||||
|
||||
if max_length and len(value) > max_length:
|
||||
raise ValidationError(f"{field_name} must not exceed {max_length} characters")
|
||||
|
||||
if pattern and not re.match(pattern, value):
|
||||
raise ValidationError(f"{field_name} has invalid format")
|
||||
|
||||
return True
|
||||
|
||||
def validate_email(email, required=False):
|
||||
"""Validate email format"""
|
||||
if not email and not required:
|
||||
return True
|
||||
|
||||
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
||||
if not re.match(pattern, email):
|
||||
raise ValidationError("Invalid email format")
|
||||
return True
|
||||
|
||||
def sanitize_filename(filename):
|
||||
"""Sanitize filename to prevent path traversal"""
|
||||
# Remove any path separators
|
||||
filename = filename.replace('..', '').replace('/', '').replace('\\', '')
|
||||
# Allow only alphanumeric, dash, underscore, and dot
|
||||
filename = re.sub(r'[^a-zA-Z0-9._-]', '_', filename)
|
||||
return filename[:255] # Limit length
|
||||
|
||||
def validate_uuid(value):
|
||||
"""Validate UUID format"""
|
||||
uuid_pattern = r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
|
||||
if not re.match(uuid_pattern, str(value).lower()):
|
||||
raise ValidationError("Invalid UUID format")
|
||||
return True
|
||||
|
||||
# Request validation schemas
|
||||
PROFILE_SCHEMA = {
|
||||
'name': {'type': 'string', 'required': True, 'min_length': 1, 'max_length': 255},
|
||||
'email': {'type': 'email', 'required': False},
|
||||
'contact_number': {'type': 'string', 'required': False, 'max_length': 50},
|
||||
'default_key': {'type': 'string', 'required': False, 'max_length': 10},
|
||||
'notes': {'type': 'string', 'required': False, 'max_length': 5000}
|
||||
}
|
||||
|
||||
SONG_SCHEMA = {
|
||||
'title': {'type': 'string', 'required': True, 'min_length': 1, 'max_length': 500},
|
||||
'artist': {'type': 'string', 'required': False, 'max_length': 500},
|
||||
'band': {'type': 'string', 'required': False, 'max_length': 500},
|
||||
'singer': {'type': 'string', 'required': False, 'max_length': 500},
|
||||
'lyrics': {'type': 'string', 'required': False, 'max_length': 50000},
|
||||
'chords': {'type': 'string', 'required': False, 'max_length': 50000}
|
||||
}
|
||||
|
||||
PLAN_SCHEMA = {
|
||||
'name': {'type': 'string', 'required': True, 'min_length': 1, 'max_length': 500},
|
||||
'date': {'type': 'string', 'required': False}, # Will validate format separately
|
||||
'notes': {'type': 'string', 'required': False, 'max_length': 5000}
|
||||
}
|
||||
|
||||
def validate_request_data(schema):
|
||||
"""
|
||||
Decorator to validate request JSON data against schema
|
||||
|
||||
Usage:
|
||||
@app.route('/api/profiles', methods=['POST'])
|
||||
@validate_request_data(PROFILE_SCHEMA)
|
||||
def create_profile():
|
||||
data = request.get_json()
|
||||
...
|
||||
"""
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def wrapped(*args, **kwargs):
|
||||
if not request.is_json:
|
||||
return jsonify({'error': 'Content-Type must be application/json'}), 400
|
||||
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({'error': 'Request body is required'}), 400
|
||||
|
||||
try:
|
||||
# Validate each field in schema
|
||||
for field, rules in schema.items():
|
||||
value = data.get(field)
|
||||
|
||||
if rules['type'] == 'string':
|
||||
validate_string(
|
||||
value,
|
||||
field,
|
||||
min_length=rules.get('min_length'),
|
||||
max_length=rules.get('max_length'),
|
||||
pattern=rules.get('pattern'),
|
||||
required=rules.get('required', False)
|
||||
)
|
||||
elif rules['type'] == 'email':
|
||||
if value:
|
||||
validate_email(value, required=rules.get('required', False))
|
||||
|
||||
return f(*args, **kwargs)
|
||||
|
||||
except ValidationError as e:
|
||||
return jsonify({'error': 'validation_error', 'message': str(e)}), 400
|
||||
|
||||
return wrapped
|
||||
return decorator
|
||||
Reference in New Issue
Block a user