""" 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"}}}, }, }