mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2026-02-05 02:35:18 -05:00
feat: add passkey authentication and enhance user preferences - Add passkey login security event type with fingerprint icon - Include request and site context in email confirmation for backend - Add user_id exact match filter to prevent incorrect user lookups - Enable PATCH method for updating user preferences via API - Add moderation_preferences support to user settings - Optimize ticket queries with select_related and prefetch_related This commit introduces passkey authentication tracking, improves user profile filtering accuracy, and extends the preferences API to support updates. Query optimizations reduce database hits for ticket listings.
273 lines
10 KiB
Python
273 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",
|
|
"x-session-token", # Required for allauth headless app client
|
|
]
|
|
|
|
# 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"}}},
|
|
},
|
|
}
|