Files
thrillwiki_django_no_react/backend/config/settings/rest_framework.py
pacnpal edcd8f2076 Add secret management guide, client-side performance monitoring, and search accessibility enhancements
- Introduced a comprehensive Secret Management Guide detailing best practices, secret classification, development setup, production management, rotation procedures, and emergency protocols.
- Implemented a client-side performance monitoring script to track various metrics including page load performance, paint metrics, layout shifts, and memory usage.
- Enhanced search accessibility with keyboard navigation support for search results, ensuring compliance with WCAG standards and improving user experience.
2025-12-23 16:41:42 -05:00

285 lines
10 KiB
Python

"""
Django REST Framework configuration for thrillwiki project.
This module configures DRF, SimpleJWT, dj-rest-auth, CORS, and
drf-spectacular (OpenAPI documentation).
Why python-decouple?
- Already used in base.py for consistency
- Simpler API than django-environ
- Sufficient for our configuration needs
- Better separation of config from code
"""
from datetime import timedelta
from decouple import config
# =============================================================================
# Django REST Framework Settings
# =============================================================================
REST_FRAMEWORK = {
# Authentication classes (order matters - first match wins)
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework_simplejwt.authentication.JWTAuthentication",
"rest_framework.authentication.SessionAuthentication",
"rest_framework.authentication.TokenAuthentication", # Backward compatibility
],
# Default permissions - require authentication
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated",
],
# Pagination settings
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": config("API_PAGE_SIZE", default=20, cast=int),
"MAX_PAGE_SIZE": config("API_MAX_PAGE_SIZE", default=100, cast=int),
# API versioning via Accept header
"DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.AcceptHeaderVersioning",
"DEFAULT_VERSION": "v1",
"ALLOWED_VERSIONS": ["v1"],
# Response rendering
"DEFAULT_RENDERER_CLASSES": [
"rest_framework.renderers.JSONRenderer",
"rest_framework.renderers.BrowsableAPIRenderer",
],
# Request parsing
"DEFAULT_PARSER_CLASSES": [
"rest_framework.parsers.JSONParser",
"rest_framework.parsers.FormParser",
"rest_framework.parsers.MultiPartParser",
],
# Custom exception handling
"EXCEPTION_HANDLER": "apps.core.api.exceptions.custom_exception_handler",
# Filter backends
"DEFAULT_FILTER_BACKENDS": [
"django_filters.rest_framework.DjangoFilterBackend",
"rest_framework.filters.SearchFilter",
"rest_framework.filters.OrderingFilter",
],
# Rate limiting
"DEFAULT_THROTTLE_CLASSES": [
"rest_framework.throttling.AnonRateThrottle",
"rest_framework.throttling.UserRateThrottle",
],
"DEFAULT_THROTTLE_RATES": {
"anon": f"{config('API_RATE_LIMIT_ANON_PER_MINUTE', default=60, cast=int)}/minute",
"user": f"{config('API_RATE_LIMIT_USER_PER_HOUR', default=1000, cast=int)}/hour",
},
# Test settings
"TEST_REQUEST_DEFAULT_FORMAT": "json",
"NON_FIELD_ERRORS_KEY": "non_field_errors",
# OpenAPI schema
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
}
# =============================================================================
# CORS Settings
# =============================================================================
# Cross-Origin Resource Sharing configuration for API access
# Allow credentials (cookies, authorization headers)
CORS_ALLOW_CREDENTIALS = True
# Allow all origins (not recommended for production)
CORS_ALLOW_ALL_ORIGINS = config(
"CORS_ALLOW_ALL_ORIGINS", default=False, cast=bool
)
# Specific allowed origins (comma-separated)
CORS_ALLOWED_ORIGINS = config(
"CORS_ALLOWED_ORIGINS",
default="",
cast=lambda v: [s.strip() for s in v.split(",") if s.strip()]
)
# Allowed HTTP headers for CORS requests
CORS_ALLOW_HEADERS = [
"accept",
"accept-encoding",
"authorization",
"content-type",
"dnt",
"origin",
"user-agent",
"x-csrftoken",
"x-requested-with",
"x-api-version",
]
# HTTP methods allowed for CORS requests
CORS_ALLOW_METHODS = [
"DELETE",
"GET",
"OPTIONS",
"PATCH",
"POST",
"PUT",
]
# Headers exposed to browsers (for rate limiting)
CORS_EXPOSE_HEADERS = [
"X-RateLimit-Limit",
"X-RateLimit-Remaining",
"X-RateLimit-Reset",
"X-API-Version",
]
# =============================================================================
# API Rate Limiting
# =============================================================================
API_RATE_LIMIT_PER_MINUTE = config(
"API_RATE_LIMIT_PER_MINUTE", default=60, cast=int
)
API_RATE_LIMIT_PER_HOUR = config(
"API_RATE_LIMIT_PER_HOUR", default=1000, cast=int
)
# =============================================================================
# SimpleJWT Settings
# =============================================================================
# JWT token configuration for authentication
# Import SECRET_KEY for signing tokens
# This will be set by base.py before this module is imported
def get_secret_key():
"""Get SECRET_KEY lazily to avoid circular imports."""
return config("SECRET_KEY")
SIMPLE_JWT = {
# Token lifetimes
# Short access tokens (15 min) provide better security
"ACCESS_TOKEN_LIFETIME": timedelta(
minutes=config("JWT_ACCESS_TOKEN_LIFETIME_MINUTES", default=15, cast=int)
),
"REFRESH_TOKEN_LIFETIME": timedelta(
days=config("JWT_REFRESH_TOKEN_LIFETIME_DAYS", default=7, cast=int)
),
# Token rotation and blacklisting
# Rotate refresh tokens on each use and blacklist old ones
"ROTATE_REFRESH_TOKENS": True,
"BLACKLIST_AFTER_ROTATION": True,
# Update last login on token refresh
"UPDATE_LAST_LOGIN": True,
# Cryptographic settings
"ALGORITHM": "HS256",
"SIGNING_KEY": None, # Will use Django's SECRET_KEY
"VERIFYING_KEY": None,
# Token validation
"AUDIENCE": None,
"ISSUER": config("JWT_ISSUER", default="thrillwiki"),
"JWK_URL": None,
"LEEWAY": 0, # No leeway for token expiration
# Authentication header
"AUTH_HEADER_TYPES": ("Bearer",),
"AUTH_HEADER_NAME": "HTTP_AUTHORIZATION",
# User identification
"USER_ID_FIELD": "id",
"USER_ID_CLAIM": "user_id",
"USER_AUTHENTICATION_RULE": (
"rest_framework_simplejwt.authentication.default_user_authentication_rule"
),
# Token classes
"AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",),
"TOKEN_TYPE_CLAIM": "token_type",
"TOKEN_USER_CLASS": "rest_framework_simplejwt.models.TokenUser",
# JTI claim for unique token identification (enables revocation)
"JTI_CLAIM": "jti",
# Sliding token settings
"SLIDING_TOKEN_REFRESH_EXP_CLAIM": "refresh_exp",
"SLIDING_TOKEN_LIFETIME": timedelta(minutes=15),
"SLIDING_TOKEN_REFRESH_LIFETIME": timedelta(days=1),
}
# =============================================================================
# dj-rest-auth Settings
# =============================================================================
# REST authentication endpoints configuration
# Determine if we're in debug mode for secure cookie setting
_debug = config("DEBUG", default=True, cast=bool)
REST_AUTH = {
"USE_JWT": True,
"JWT_AUTH_COOKIE": "thrillwiki-auth",
"JWT_AUTH_REFRESH_COOKIE": "thrillwiki-refresh",
# Only send cookies over HTTPS in production
"JWT_AUTH_SECURE": not _debug,
# Prevent JavaScript access to cookies
"JWT_AUTH_HTTPONLY": True,
# SameSite cookie attribute (Lax is compatible with OAuth flows)
"JWT_AUTH_SAMESITE": "Lax",
"JWT_AUTH_RETURN_EXPIRATION": True,
"JWT_TOKEN_CLAIMS_SERIALIZER": (
"rest_framework_simplejwt.serializers.TokenObtainPairSerializer"
),
}
# =============================================================================
# drf-spectacular Settings (OpenAPI Documentation)
# =============================================================================
SPECTACULAR_SETTINGS = {
"TITLE": "ThrillWiki API",
"DESCRIPTION": """Comprehensive theme park and ride information API.
## API Conventions
### Response Format
All successful responses include a `success: true` field with data nested under `data`.
All error responses include an `error` object with `code` and `message` fields.
### Pagination
List endpoints support pagination with `page` and `page_size` parameters.
Default page size is 20, maximum is 100.
### Filtering
Range filters use `{field}_min` and `{field}_max` naming convention.
Search uses the `search` parameter.
Ordering uses the `ordering` parameter (prefix with `-` for descending).
### Field Naming
All field names use snake_case convention (e.g., `image_url`, `created_at`).
""",
"VERSION": config("API_VERSION", default="1.0.0"),
"SERVE_INCLUDE_SCHEMA": False,
"COMPONENT_SPLIT_REQUEST": True,
"TAGS": [
{"name": "Parks", "description": "Theme park operations"},
{"name": "Rides", "description": "Ride information and management"},
{"name": "Park Media", "description": "Park photos and media management"},
{"name": "Ride Media", "description": "Ride photos and media management"},
{"name": "Authentication", "description": "User authentication and session management"},
{"name": "Social Authentication", "description": "Social provider login and account linking"},
{"name": "User Profile", "description": "User profile management"},
{"name": "User Settings", "description": "User preferences and settings"},
{"name": "User Notifications", "description": "User notification management"},
{"name": "User Content", "description": "User-generated content (top lists, reviews)"},
{"name": "User Management", "description": "Admin user management operations"},
{"name": "Self-Service Account Management", "description": "User account deletion and management"},
{"name": "Core", "description": "Core utility endpoints (search, suggestions)"},
{"name": "Statistics", "description": "Statistical endpoints providing aggregated data and insights"},
],
"SCHEMA_PATH_PREFIX": "/api/",
"DEFAULT_GENERATOR_CLASS": "drf_spectacular.generators.SchemaGenerator",
"DEFAULT_AUTO_SCHEMA": "drf_spectacular.openapi.AutoSchema",
"PREPROCESSING_HOOKS": [
"api.v1.schema.custom_preprocessing_hook",
],
"SERVE_PERMISSIONS": ["rest_framework.permissions.AllowAny"],
"SWAGGER_UI_SETTINGS": {
"deepLinking": True,
"persistAuthorization": True,
"displayOperationId": False,
"displayRequestDuration": True,
},
"REDOC_UI_SETTINGS": {
"hideDownloadButton": False,
"hideHostname": False,
"hideLoading": False,
"hideSchemaPattern": True,
"scrollYOffset": 0,
"theme": {"colors": {"primary": {"main": "#1976d2"}}},
},
}