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.
This commit is contained in:
pacnpal
2025-12-23 16:41:42 -05:00
parent ae31e889d7
commit edcd8f2076
155 changed files with 22046 additions and 4645 deletions

576
docs/FUTURE_WORK.md Normal file
View File

@@ -0,0 +1,576 @@
# Future Work & Deferred Features
This document tracks features that have been deferred for future implementation. Each item includes context, implementation guidance, and priority.
## Priority Levels
- **P0 (Critical)**: Blocks major functionality or has security implications
- **P1 (High)**: Significantly improves user experience or performance
- **P2 (Medium)**: Nice-to-have features that add value
- **P3 (Low)**: Optional enhancements
## Feature Tracking
### Map Service Enhancements
#### THRILLWIKI-106: Map Clustering Algorithm
**Priority**: P1 (High)
**Estimated Effort**: 3-5 days
**Dependencies**: None
**Context**:
Currently, the map API returns all locations within bounds without clustering. At high zoom levels (zoomed out), this can result in hundreds of overlapping markers, degrading performance and UX.
**Proposed Solution**:
Implement a server-side clustering algorithm using one of these approaches:
1. **Grid-based clustering** (Recommended for simplicity):
- Divide the map into a grid based on zoom level
- Group locations within each grid cell
- Return cluster center and count for cells with multiple locations
2. **DBSCAN clustering** (Better quality, more complex):
- Use scikit-learn's DBSCAN algorithm
- Cluster based on geographic distance
- Adjust epsilon parameter based on zoom level
**Implementation Steps**:
1. Create `backend/apps/core/services/map_clustering.py` with clustering logic
2. Add `cluster_locations()` method that accepts:
- List of `UnifiedLocation` objects
- Zoom level (1-20)
- Clustering strategy ('grid' or 'dbscan')
3. Update `MapLocationsAPIView._build_response()` to call clustering service when `params["cluster"]` is True
4. Update `MapClusterSerializer` to include cluster metadata
5. Add tests in `backend/tests/services/test_map_clustering.py`
**API Changes**:
- Response includes `clusters` array with cluster objects
- Each cluster has: `id`, `coordinates`, `count`, `bounds`, `representative_location`
**Performance Considerations**:
- Cache clustered results separately from unclustered
- Use spatial indexes on location tables
- Limit clustering to zoom levels 1-12 (zoomed out views)
**References**:
- [Supercluster.js](https://github.com/mapbox/supercluster) - JavaScript implementation for reference
- [PostGIS ST_ClusterKMeans](https://postgis.net/docs/ST_ClusterKMeans.html) - Database-level clustering
---
#### THRILLWIKI-107: Nearby Locations
**Priority**: P2 (Medium)
**Estimated Effort**: 2-3 days
**Dependencies**: None
**Context**:
Location detail views currently don't show nearby parks or rides. This would help users discover attractions in the same area.
**Proposed Solution**:
Use PostGIS spatial queries to find locations within a radius:
```python
from django.contrib.gis.measure import D # Distance
from django.contrib.gis.db.models.functions import Distance
def get_nearby_locations(location_obj, radius_miles=25, limit=10):
"""Get nearby locations using spatial query."""
point = location_obj.point
# Query parks within radius
nearby_parks = Park.objects.filter(
location__point__distance_lte=(point, D(mi=radius_miles))
).annotate(
distance=Distance('location__point', point)
).exclude(
id=location_obj.park.id # Exclude self
).order_by('distance')[:limit]
return nearby_parks
```
**Implementation Steps**:
1. Add `get_nearby_locations()` method to `backend/apps/core/services/location_service.py`
2. Update `MapLocationDetailAPIView.get()` to call this method
3. Update `MapLocationDetailSerializer.get_nearby_locations()` to return actual data
4. Add distance field to nearby location objects
5. Add tests for spatial queries
**API Response Example**:
```json
{
"nearby_locations": [
{
"id": "park_123",
"name": "Cedar Point",
"type": "park",
"distance_miles": 5.2,
"coordinates": [41.4793, -82.6833]
}
]
}
```
**Performance Considerations**:
- Use spatial indexes (already present on `location__point` fields)
- Cache nearby locations for 1 hour
- Limit radius to 50 miles maximum
---
#### THRILLWIKI-108: Search Relevance Scoring
**Priority**: P2 (Medium)
**Estimated Effort**: 2-3 days
**Dependencies**: None
**Context**:
Search results currently return a hardcoded relevance score of 1.0. Implementing proper relevance scoring would improve search result quality.
**Proposed Solution**:
Implement a weighted scoring algorithm based on:
1. **Text Match Quality** (40% weight):
- Exact name match: 1.0
- Name starts with query: 0.8
- Name contains query: 0.6
- City/state match: 0.4
2. **Popularity** (30% weight):
- Based on `average_rating` and `ride_count`/`coaster_count`
- Normalize to 0-1 scale
3. **Recency** (15% weight):
- Recently opened attractions score higher
- Normalize based on `opening_date`
4. **Status** (15% weight):
- Operating: 1.0
- Seasonal: 0.8
- Closed temporarily: 0.5
- Closed permanently: 0.2
**Implementation Steps**:
1. Create `backend/apps/core/services/search_scoring.py` with scoring logic
2. Add `calculate_relevance_score()` method
3. Update `MapSearchAPIView.get()` to calculate scores
4. Sort results by relevance score (descending)
5. Add tests for scoring algorithm
**Example Implementation**:
```python
def calculate_relevance_score(location, query):
score = 0.0
# Text match (40%)
name_lower = location.name.lower()
query_lower = query.lower()
if name_lower == query_lower:
score += 0.40
elif name_lower.startswith(query_lower):
score += 0.32
elif query_lower in name_lower:
score += 0.24
# Popularity (30%)
if location.average_rating:
score += (location.average_rating / 5.0) * 0.30
# Status (15%)
status_weights = {
'OPERATING': 1.0,
'SEASONAL': 0.8,
'CLOSED_TEMP': 0.5,
'CLOSED_PERM': 0.2
}
score += status_weights.get(location.status, 0.5) * 0.15
return min(score, 1.0)
```
**Performance Considerations**:
- Calculate scores in Python (not database) for flexibility
- Cache search results with scores for 5 minutes
- Consider using PostgreSQL full-text search for better performance
---
#### THRILLWIKI-109: Cache Statistics Tracking
**Priority**: P2 (Medium)
**Estimated Effort**: 1-2 hours
**Dependencies**: None
**Status**: IMPLEMENTED
**Context**:
The `MapStatsAPIView` returns hardcoded cache statistics (0 hits, 0 misses). Implementing real cache statistics provides visibility into caching effectiveness.
**Implementation**:
Added `get_cache_statistics()` method to `EnhancedCacheService` that retrieves Redis INFO statistics when available. The `MapStatsAPIView` now returns real cache hit/miss data.
---
### User Features
#### THRILLWIKI-104: Full User Statistics Tracking
**Priority**: P2 (Medium)
**Estimated Effort**: 3-4 days
**Dependencies**: THRILLWIKI-105 (Photo counting)
**Context**:
Current user statistics are calculated on-demand by querying multiple tables. This is inefficient and doesn't track all desired metrics.
**Proposed Solution**:
Implement a `UserStatistics` model with periodic updates:
```python
class UserStatistics(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
# Content statistics
parks_visited = models.IntegerField(default=0)
rides_ridden = models.IntegerField(default=0)
reviews_written = models.IntegerField(default=0)
photos_uploaded = models.IntegerField(default=0)
top_lists_created = models.IntegerField(default=0)
# Engagement statistics
helpful_votes_received = models.IntegerField(default=0)
comments_made = models.IntegerField(default=0)
badges_earned = models.IntegerField(default=0)
# Activity tracking
last_review_date = models.DateTimeField(null=True, blank=True)
last_photo_upload_date = models.DateTimeField(null=True, blank=True)
streak_days = models.IntegerField(default=0)
# Timestamps
last_calculated = models.DateTimeField(auto_now=True)
class Meta:
verbose_name_plural = "User statistics"
```
**Implementation Steps**:
1. Create migration for `UserStatistics` model in `backend/apps/accounts/models.py`
2. Create Celery task `update_user_statistics` in `backend/apps/accounts/tasks.py`
3. Update statistics on user actions using Django signals:
- `post_save` signal on `ParkReview`, `RideReview` -> increment `reviews_written`
- `post_save` signal on `ParkPhoto`, `RidePhoto` -> increment `photos_uploaded`
4. Add management command `python manage.py recalculate_user_stats` for bulk updates
5. Update `get_user_statistics` view to read from `UserStatistics` model
6. Add periodic Celery task to recalculate statistics daily
**Performance Benefits**:
- Reduces database queries from 5+ to 1
- Enables leaderboards and ranking features
- Supports gamification (badges, achievements)
**Migration Strategy**:
1. Create model and migration
2. Run `recalculate_user_stats` for existing users
3. Enable signal handlers for new activity
4. Monitor for 1 week before removing old calculation logic
---
#### THRILLWIKI-105: Photo Upload Counting
**Priority**: P2 (Medium)
**Estimated Effort**: 30 minutes
**Dependencies**: None
**Status**: IMPLEMENTED
**Context**:
The user statistics endpoint returns `photos_uploaded: 0` for all users. Photo uploads should be counted from `ParkPhoto` and `RidePhoto` models.
**Implementation**:
Updated `get_user_statistics()` in `backend/apps/api/v1/accounts/views.py` to query `ParkPhoto` and `RidePhoto` models where `uploaded_by=user`.
---
### Infrastructure
#### THRILLWIKI-101: Geocoding Service Integration
**Priority**: P3 (Low)
**Estimated Effort**: 2-3 days
**Dependencies**: None
**Context**:
`CompanyHeadquarters` model has address fields but no coordinates. This prevents companies from appearing on the map.
**Proposed Solution**:
Integrate a geocoding service to convert addresses to coordinates:
**Recommended Services**:
1. **Google Maps Geocoding API** (Paid, high quality)
2. **Nominatim (OpenStreetMap)** (Free, rate-limited)
3. **Mapbox Geocoding API** (Paid, good quality)
**Implementation Steps**:
1. Create `backend/apps/core/services/geocoding_service.py`:
```python
class GeocodingService:
def geocode_address(self, address: str) -> tuple[float, float] | None:
"""Convert address to (latitude, longitude)."""
# Implementation using chosen service
```
2. Add geocoding to `CompanyHeadquarters` model:
- Add `latitude` and `longitude` fields
- Add `geocoded_at` timestamp field
- Create migration
3. Update `CompanyLocationAdapter.to_unified_location()` to use coordinates if available
4. Add management command `python manage.py geocode_companies` for bulk geocoding
5. Add Celery task for automatic geocoding on company creation/update
**Configuration**:
Add to `backend/config/settings/base.py`:
```python
GEOCODING_SERVICE = env('GEOCODING_SERVICE', default='nominatim')
GEOCODING_API_KEY = env('GEOCODING_API_KEY', default='')
GEOCODING_RATE_LIMIT = env.int('GEOCODING_RATE_LIMIT', default=1) # requests per second
```
**Rate Limiting**:
- Implement exponential backoff for API errors
- Cache geocoding results to avoid redundant API calls
- Use Celery for async geocoding to avoid blocking requests
**Cost Considerations**:
- Nominatim: Free but limited to 1 request/second
- Google Maps: $5 per 1000 requests (first $200/month free)
- Mapbox: $0.50 per 1000 requests (first 100k free)
**Alternative Approach**:
Store coordinates manually in admin interface for the ~50-100 companies in the database.
---
#### THRILLWIKI-110: ClamAV Malware Scanning Integration
**Priority**: P1 (High) - Security feature
**Estimated Effort**: 2-3 days
**Dependencies**: ClamAV daemon installation
**Context**:
File uploads currently use magic number validation and PIL integrity checks, but don't scan for malware. This is a security gap for user-generated content.
**Proposed Solution**:
Integrate ClamAV antivirus scanning for all file uploads.
**Implementation Steps**:
1. **Install ClamAV**:
```bash
# Docker
docker run -d -p 3310:3310 clamav/clamav:latest
# Ubuntu/Debian
sudo apt-get install clamav clamav-daemon
sudo freshclam # Update virus definitions
sudo systemctl start clamav-daemon
```
2. **Install Python client**:
```bash
uv add clamd
```
3. **Update `backend/apps/core/utils/file_scanner.py`**:
```python
import clamd
def scan_file_for_malware(file: UploadedFile) -> Tuple[bool, str]:
"""Scan file for malware using ClamAV."""
try:
# Connect to ClamAV daemon
cd = clamd.ClamdUnixSocket() # or ClamdNetworkSocket for remote
# Scan file
file.seek(0)
scan_result = cd.instream(file)
file.seek(0)
# Check result
if scan_result['stream'][0] == 'OK':
return True, ""
else:
virus_name = scan_result['stream'][1]
return False, f"Malware detected: {virus_name}"
except clamd.ConnectionError:
# ClamAV not available - log warning and allow upload
logger.warning("ClamAV daemon not available, skipping malware scan")
return True, ""
except Exception as e:
logger.error(f"Malware scan error: {e}")
return False, "Malware scan failed"
```
4. **Configuration**:
Add to `backend/config/settings/base.py`:
```python
CLAMAV_ENABLED = env.bool('CLAMAV_ENABLED', default=False)
CLAMAV_SOCKET = env('CLAMAV_SOCKET', default='/var/run/clamav/clamd.ctl')
CLAMAV_HOST = env('CLAMAV_HOST', default='localhost')
CLAMAV_PORT = env.int('CLAMAV_PORT', default=3310)
```
5. **Update file upload views**:
- Call `scan_file_for_malware()` in avatar upload view
- Call in media upload views
- Log all malware detections for security monitoring
6. **Testing**:
- Use EICAR test file for testing: `X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*`
- Add unit tests with mocked ClamAV responses
**Deployment Considerations**:
- ClamAV requires ~1GB RAM for virus definitions
- Update virus definitions daily via `freshclam`
- Monitor ClamAV daemon health in production
- Consider using cloud-based scanning service (AWS GuardDuty, VirusTotal) for serverless deployments
**Fallback Strategy**:
If ClamAV is unavailable, log warning and allow upload (fail open). This prevents blocking legitimate uploads if ClamAV daemon crashes.
---
### Management Commands
#### THRILLWIKI-111: Sample Data Creation Command
**Priority**: P3 (Low) - Development utility
**Estimated Effort**: 1-2 days
**Dependencies**: None
**Context**:
The `create_sample_data` management command is incomplete. This command is useful for:
- Local development with realistic data
- Demo environments
- Testing with diverse data sets
**Proposed Solution**:
Complete the implementation with comprehensive sample data:
**Sample Data to Create**:
1. **Parks** (10-15):
- Major theme parks (Disney, Universal, Cedar Point)
- Regional parks
- Water parks
- Mix of operating/closed/seasonal statuses
2. **Rides** (50-100):
- Roller coasters (various types)
- Flat rides
- Water rides
- Dark rides
- Mix of statuses and manufacturers
3. **Companies** (20-30):
- Operators (Disney, Six Flags, Cedar Fair)
- Manufacturers (Intamin, B&M, RMC)
- Mix of active/inactive
4. **Users** (10):
- Admin user
- Regular users with various activity levels
- Test user for authentication testing
5. **Reviews** (100-200):
- Park reviews with ratings
- Ride reviews with ratings
- Mix of helpful/unhelpful votes
6. **Media** (50):
- Park photos
- Ride photos
- Mix of approved/pending/rejected
**Implementation Steps**:
1. Create fixtures in `backend/fixtures/sample_data.json`
2. Update `create_sample_data.py` to load fixtures
3. Add `--clear` flag to delete existing data before creating
4. Add `--minimal` flag for quick setup (10 parks, 20 rides)
5. Document usage in `backend/README.md`
**Usage**:
```bash
# Full sample data
python manage.py create_sample_data
# Minimal data for quick testing
python manage.py create_sample_data --minimal
# Clear existing data first
python manage.py create_sample_data --clear
```
**Alternative Approach**:
Use Django fixtures with `loaddata` command:
```bash
python manage.py loaddata sample_parks sample_rides sample_users
```
---
## Completed Items
### THRILLWIKI-103: Admin Permission Checks
**Status**: COMPLETED (Already Implemented)
**Context**:
The `MapCacheView` delete and post methods had TODO comments for adding admin permission checks. Upon review, these checks were already implemented using `request.user.is_authenticated and request.user.is_staff`.
**Resolution**: Removed outdated TODO comments.
---
## Implementation Notes
### Creating GitHub Issues
Each item in this document can be converted to a GitHub issue using this template:
```markdown
## Description
[Copy from Context section]
## Implementation
[Copy from Implementation Steps section]
## Acceptance Criteria
- [ ] Feature implemented as specified
- [ ] Unit tests added with >80% coverage
- [ ] Integration tests pass
- [ ] Documentation updated
- [ ] Code reviewed and approved
## Priority
[Copy Priority value]
## Related
- THRILLWIKI issue number
- Related features or dependencies
```
### Priority Order for Implementation
Based on business value and effort, recommended implementation order:
1. **THRILLWIKI-110**: ClamAV Malware Scanning (P1, security)
2. **THRILLWIKI-106**: Map Clustering (P1, performance)
3. **THRILLWIKI-107**: Nearby Locations (P2, UX)
4. **THRILLWIKI-108**: Search Relevance Scoring (P2, UX)
5. **THRILLWIKI-104**: Full User Statistics (P2, engagement)
6. **THRILLWIKI-101**: Geocoding Service (P3, completeness)
7. **THRILLWIKI-111**: Sample Data Command (P3, development)

414
docs/HEALTH_CHECKS.md Normal file
View File

@@ -0,0 +1,414 @@
# ThrillWiki Health Check Documentation
This document describes the health check endpoints available in ThrillWiki for monitoring and operational purposes.
## Overview
ThrillWiki provides three health check endpoints with different levels of detail:
| Endpoint | Purpose | Authentication |
|----------|---------|----------------|
| `/api/v1/health/` | Comprehensive health check | Public |
| `/api/v1/health/simple/` | Simple OK/ERROR for load balancers | Public |
| `/api/v1/health/performance/` | Performance metrics | Debug mode only |
## Endpoint Details
### Comprehensive Health Check
**Endpoint**: `GET /api/v1/health/`
Returns detailed health information including system metrics, database status, cache status, and individual health checks.
#### Response Format
```json
{
"status": "healthy",
"timestamp": "2025-12-23T10:30:00Z",
"version": "1.0.0",
"environment": "production",
"response_time_ms": 45.23,
"checks": {
"DatabaseBackend": {
"status": "healthy",
"critical": true,
"errors": [],
"response_time_ms": 12.5
},
"CacheBackend": {
"status": "healthy",
"critical": false,
"errors": [],
"response_time_ms": 2.1
},
"DiskUsage": {
"status": "healthy",
"critical": false,
"errors": []
},
"MemoryUsage": {
"status": "healthy",
"critical": false,
"errors": []
}
},
"metrics": {
"cache": {
"redis": {
"used_memory": "45.2MB",
"connected_clients": 15,
"hit_rate": 94.5
}
},
"database": {
"vendor": "postgresql",
"connection_status": "connected",
"test_query_time_ms": 1.2,
"active_connections": 8,
"cache_hit_ratio": 98.3
},
"system": {
"debug_mode": false,
"memory": {
"total_mb": 8192,
"available_mb": 4096,
"percent_used": 50
},
"cpu": {
"percent_used": 25.3,
"core_count": 4
},
"disk": {
"total_gb": 100,
"free_gb": 45,
"percent_used": 55
}
}
}
}
```
#### Status Codes
| Code | Meaning |
|------|---------|
| 200 | All checks passed or non-critical failures |
| 503 | Critical service failure (database, etc.) |
### Simple Health Check
**Endpoint**: `GET /api/v1/health/simple/`
Lightweight health check designed for load balancer health probes.
#### Response Format (Healthy)
```json
{
"status": "ok",
"timestamp": "2025-12-23T10:30:00Z"
}
```
#### Response Format (Unhealthy)
```json
{
"status": "error",
"error": "Database connection failed",
"timestamp": "2025-12-23T10:30:00Z"
}
```
#### Status Codes
| Code | Meaning |
|------|---------|
| 200 | Service healthy |
| 503 | Service unhealthy |
### Performance Metrics
**Endpoint**: `GET /api/v1/health/performance/`
Detailed performance metrics for debugging (only available when `DEBUG=True`).
#### Response Format
```json
{
"timestamp": "2025-12-23T10:30:00Z",
"database_analysis": {
"total_queries": 0,
"query_analysis": {}
},
"cache_performance": {
"redis": {
"used_memory": "45.2MB",
"hit_rate": 94.5
}
},
"recent_slow_queries": []
}
```
#### Status Codes
| Code | Meaning |
|------|---------|
| 200 | Metrics returned |
| 403 | Not available (DEBUG=False) |
## Health Checks Included
### Database Check
Verifies PostgreSQL connectivity by executing a simple query.
```python
# Test query executed
cursor.execute("SELECT 1")
```
**Critical**: Yes (503 returned if fails)
### Cache Check
Verifies Redis connectivity and operation.
**Critical**: No (200 returned with warning if fails)
### Disk Usage Check
Monitors disk space to prevent storage exhaustion.
**Threshold**: Configurable via `HEALTH_CHECK_DISK_USAGE_MAX` (default: 90%)
### Memory Usage Check
Monitors available memory.
**Threshold**: Configurable via `HEALTH_CHECK_MEMORY_MIN` (default: 100MB)
## Integration Examples
### Kubernetes Liveness Probe
```yaml
livenessProbe:
httpGet:
path: /api/v1/health/simple/
port: 8000
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
```
### Kubernetes Readiness Probe
```yaml
readinessProbe:
httpGet:
path: /api/v1/health/simple/
port: 8000
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 2
```
### AWS Application Load Balancer
```json
{
"HealthCheckPath": "/api/v1/health/simple/",
"HealthCheckIntervalSeconds": 30,
"HealthyThresholdCount": 2,
"UnhealthyThresholdCount": 3,
"HealthCheckTimeoutSeconds": 5,
"Matcher": {
"HttpCode": "200"
}
}
```
### Docker Compose Health Check
```yaml
services:
web:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/api/v1/health/simple/"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
```
### Nginx Upstream Health Check
```nginx
upstream django {
server web:8000;
# With nginx-plus
health_check uri=/api/v1/health/simple/ interval=10s fails=3 passes=2;
}
```
## Monitoring Integration
### Prometheus Metrics
Health check data can be exposed as Prometheus metrics using django-prometheus:
```python
# Example custom metrics
from prometheus_client import Gauge
database_response_time = Gauge(
'thrillwiki_database_response_time_seconds',
'Database query response time'
)
cache_hit_rate = Gauge(
'thrillwiki_cache_hit_rate',
'Cache hit rate percentage'
)
```
### Alerting Thresholds
Recommended alerting thresholds:
| Metric | Warning | Critical |
|--------|---------|----------|
| Response time | > 1s | > 5s |
| Database query time | > 100ms | > 500ms |
| Cache hit rate | < 80% | < 50% |
| Disk usage | > 80% | > 90% |
| Memory usage | > 80% | > 90% |
### Grafana Dashboard
Import the health check dashboard:
```json
{
"dashboard": {
"title": "ThrillWiki Health",
"panels": [
{
"title": "Health Status",
"type": "stat",
"targets": [
{
"expr": "probe_success{job='thrillwiki'}"
}
]
},
{
"title": "Response Time",
"type": "graph",
"targets": [
{
"expr": "thrillwiki_health_response_time_ms"
}
]
}
]
}
}
```
## Configuration
### Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `HEALTH_CHECK_DISK_USAGE_MAX` | 90 | Max disk usage percentage |
| `HEALTH_CHECK_MEMORY_MIN` | 100 | Min available memory (MB) |
### Custom Health Checks
Add custom health checks by extending the health check system:
```python
# backend/apps/core/health_checks/custom_checks.py
from health_check.backends import BaseHealthCheckBackend
class CloudflareImagesHealthCheck(BaseHealthCheckBackend):
"""Check Cloudflare Images API connectivity."""
critical_service = False
def check_status(self):
try:
# Test Cloudflare Images API
response = cloudflare_service.test_connection()
if not response.ok:
self.add_error("Cloudflare Images API unavailable")
except Exception as e:
self.add_error(f"Cloudflare Images error: {e}")
def identifier(self):
return "CloudflareImages"
```
Register in `INSTALLED_APPS`:
```python
HEALTH_CHECK = {
'DISK_USAGE_MAX': 90,
'MEMORY_MIN': 100,
}
```
## Troubleshooting
### Health Check Returns 503
1. Check database connectivity:
```bash
uv run manage.py dbshell
```
2. Check Redis connectivity:
```bash
redis-cli ping
```
3. Review application logs:
```bash
tail -f logs/django.log
```
### Slow Health Check Response
1. Check database query performance:
```bash
uv run manage.py shell -c "from django.db import connection; print(connection.ensure_connection())"
```
2. Check cache response time:
```bash
redis-cli --latency
```
### Missing Metrics
Ensure `psutil` is installed for system metrics:
```bash
uv add psutil
```
## Best Practices
1. **Use simple endpoint for load balancers**: The `/simple/` endpoint is lightweight and fast
2. **Monitor comprehensive endpoint**: Use `/health/` for detailed monitoring dashboards
3. **Set appropriate timeouts**: Health check timeouts should be shorter than intervals
4. **Alert on degraded state**: Don't wait for complete failure
5. **Log health check failures**: Include health status in application logs

164
docs/MIGRATION_GUIDE.md Normal file
View File

@@ -0,0 +1,164 @@
# Dependency Update Migration Guide
This guide covers the migration to updated dependencies including critical security patches.
## For Developers
### Local Development Setup
1. **Update Python to 3.13+**
```bash
python --version # Should be 3.13+
```
2. **Install UV** (if not already installed)
```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
```
3. **Update Dependencies**
```bash
cd backend
uv sync --frozen # Use locked versions
```
4. **Run Tests**
```bash
uv run manage.py test
```
## Security Patches Applied
### Critical (CVSS 9.0+)
- **CVE-2025-64459** (Django) - SQL injection vulnerability
- Fixed in: Django 5.2.8+
- Previous version: 5.0.x
### High Severity
- **CVE-2024-21520** (DRF) - XSS in break_long_headers filter
- Fixed in: djangorestframework 3.15.2+
- Previous version: 3.14.x
### Medium Severity
- **CVE-2024-28219** (Pillow) - Buffer overflow
- Fixed in: Pillow 10.4.0+
- Previous version: 10.2.0
## Breaking Changes
### Python Version
- **Minimum**: 3.13+ (previously 3.11+)
- Update your local Python installation before proceeding
### django-allauth (0.60 → 65.3)
Major version jump. Review your configuration:
```python
# Check your SOCIALACCOUNT_PROVIDERS settings
# Some provider configurations may have changed
```
### sentry-sdk (1.x → 2.x)
If using Sentry, review the SDK v2 migration guide:
- https://docs.sentry.io/platforms/python/migration/
### Removed Packages
These packages were removed (not used in codebase):
- channels, channels-redis, daphne
- django-simple-history (using django-pghistory)
- django-oauth-toolkit (using dj-rest-auth)
- django-webpack-loader
- reactivated
## Dependency Groups
Dependencies are now organized into groups:
```bash
# Production only
uv sync
# Development (includes dev tools)
uv sync --group dev
# Testing
uv sync --group test
# Profiling (optional)
uv sync --group profiling
```
## Troubleshooting
### Issue: `uv sync` fails with dependency conflicts
**Solution:** Delete `uv.lock` and regenerate:
```bash
rm uv.lock
uv lock
uv sync
```
### Issue: Tests fail after update
**Solution:**
1. Check for deprecated API usage in test files
2. Review django-allauth changes for auth tests
3. Run `uv run manage.py check` for specific warnings
### Issue: Import errors for removed packages
**Solution:** Search for and remove any imports of:
- `channels`
- `simple_history`
- `oauth2_provider`
- `webpack_loader`
- `reactivated`
### Issue: `ModuleNotFoundError: cryptography`
**Solution:** This is now included. Run:
```bash
uv sync
```
## CI/CD Changes
The CI/CD pipeline now uses UV:
```yaml
- name: Install Dependencies
working-directory: backend
run: uv sync --frozen
```
## Generating requirements.txt
For tools that need requirements.txt:
```bash
cd backend
./scripts/generate_requirements.sh
```
This creates:
- `requirements.txt` - Production
- `requirements-dev.txt` - Development
- `requirements-test.txt` - Testing
## Lock File
The project now uses `uv.lock` for reproducible builds:
```bash
# Use locked versions (recommended for CI/deployment)
uv sync --frozen
# Update lock file
uv lock --upgrade
```
## Rollback Procedure
If you need to rollback:
```bash
git checkout HEAD~1 backend/pyproject.toml uv.lock
uv sync --frozen
```

View File

@@ -0,0 +1,262 @@
# ThrillWiki Production Deployment Checklist
Use this checklist to verify your ThrillWiki deployment is production-ready.
> **Documentation References:**
> - [Environment Variables Reference](./configuration/environment-variables.md) - Complete variable documentation
> - [`.env.example`](../.env.example) - Example configuration file with production notes
## Pre-Deployment
### Environment Configuration
> **Important:** These variables are **required** for production. See [Environment Variables Reference](./configuration/environment-variables.md#production-required-variables) for detailed documentation.
- [ ] `DEBUG=False` is set (**security critical**)
- [ ] `SECRET_KEY` is unique, secure, and not committed to version control
- [ ] `ALLOWED_HOSTS` contains only production domains (no default in production)
- [ ] `CSRF_TRUSTED_ORIGINS` contains all production origins with `https://` prefix
- [ ] `DJANGO_SETTINGS_MODULE=config.django.production`
- [ ] `REDIS_URL` points to production Redis instance (**required for caching/sessions**)
### Database
- [ ] PostgreSQL with PostGIS is configured
- [ ] `DATABASE_URL` uses production credentials
- [ ] Database user has minimum required privileges
- [ ] Connection pooling is configured (`CONN_MAX_AGE=60`)
- [ ] Statement timeout is set (30 seconds recommended)
- [ ] Database backups are scheduled
- [ ] Point-in-time recovery is enabled (if required)
### Cache (Redis)
- [ ] `REDIS_URL` points to production Redis
- [ ] Redis is password protected (if exposed)
- [ ] Redis maxmemory policy is set (allkeys-lru recommended)
- [ ] Redis persistence is configured (if required)
- [ ] Separate Redis databases for cache, sessions, and API
### Security
- [ ] HTTPS is enforced (`SECURE_SSL_REDIRECT=True`)
- [ ] HSTS is enabled (`SECURE_HSTS_SECONDS=31536000`)
- [ ] Secure cookies enabled (`SESSION_COOKIE_SECURE=True`, `CSRF_COOKIE_SECURE=True`)
- [ ] X-Frame-Options set to DENY
- [ ] Content-Type-Nosniff enabled
- [ ] Referrer-Policy configured
- [ ] CORS properly configured (only trusted origins)
- [ ] Rate limiting is enabled
- [ ] SSL certificate is valid and auto-renewed
### Static Files
- [ ] `collectstatic` has been run
- [ ] Static files are served via CDN or nginx
- [ ] WhiteNoise is configured for compression
- [ ] Cache headers are set (long expiry for hashed files)
### Media Files
- [ ] Cloudflare Images credentials are configured
- [ ] Upload size limits are set
- [ ] File type validation is in place
- [ ] Cleanup tasks for orphaned images are scheduled
### Email
- [ ] Email backend is configured for production
- [ ] SPF, DKIM, and DMARC records are set
- [ ] Email sending is tested
- [ ] Admin email addresses are set
### Monitoring
- [ ] Sentry is configured (`SENTRY_DSN` set)
- [ ] Health check endpoints are accessible
- [ ] Logging is configured (JSON format recommended)
- [ ] Log rotation is configured
- [ ] Alerting is set up for errors
## Deployment
### Application
- [ ] All migrations are applied (`manage.py migrate`)
- [ ] Database schema matches expected state
- [ ] Application starts without errors
- [ ] Health check returns 200
### Container/Server
- [ ] Gunicorn is configured with appropriate workers
- [ ] Process manager (systemd/supervisor) is configured
- [ ] Container resource limits are set
- [ ] Restart policy is configured
### Reverse Proxy
- [ ] Nginx is configured correctly
- [ ] SSL termination is working
- [ ] HTMX headers are forwarded
- [ ] Static/media files are served correctly
- [ ] Gzip compression is enabled
- [ ] Security headers are set at proxy level
### Celery (if used)
- [ ] Celery worker is running
- [ ] Celery beat is running (for scheduled tasks)
- [ ] Task queues are configured
- [ ] Task monitoring is set up
## Post-Deployment Verification
### Functional Tests
- [ ] Homepage loads correctly
- [ ] Authentication works (login, logout)
- [ ] Park/ride pages load correctly
- [ ] Search functionality works
- [ ] HTMX interactions work
- [ ] Admin panel is accessible
- [ ] API endpoints respond correctly
- [ ] Image uploads work
### Performance Tests
- [ ] Page load time < 3 seconds
- [ ] API response time < 500ms
- [ ] No N+1 query issues (check Django Debug Toolbar/Silk)
- [ ] Cache hit rate > 80%
- [ ] Database query time reasonable
### Security Tests
- [ ] HTTPS redirect works
- [ ] HSTS header present
- [ ] Security headers present
- [ ] CORS blocks unauthorized origins
- [ ] Rate limiting triggers on excessive requests
- [ ] SQL injection attempts blocked
- [ ] XSS attempts blocked
### Monitoring Verification
- [ ] Sentry receives test error
- [ ] Health check endpoint monitored
- [ ] Logs are being collected
- [ ] Alerts trigger on test conditions
## Rollback Plan
### Preparation
- [ ] Previous version container/image available
- [ ] Database rollback script ready (if migrations changed schema)
- [ ] DNS rollback plan (if CDN change)
- [ ] Communication plan for stakeholders
### Rollback Steps
1. Stop current deployment
2. Restore previous container/code
3. Rollback database migrations (if needed)
4. Verify rollback with health check
5. Communicate status
## Emergency Contacts
| Role | Contact |
|------|---------|
| On-call Engineer | |
| Database Admin | |
| Infrastructure | |
| Product Owner | |
## Sign-off
| Role | Name | Date | Signature |
|------|------|------|-----------|
| Developer | | | |
| DevOps | | | |
| QA | | | |
| Tech Lead | | | |
---
## Quick Verification Commands
```bash
# Check Django configuration
uv run manage.py check --deploy
# Verify database connection
uv run manage.py dbshell -c "SELECT 1;"
# Test Redis connection
redis-cli -u $REDIS_URL ping
# Check health endpoint
curl -s https://yourdomain.com/api/v1/health/ | jq .status
# Verify static files
curl -I https://yourdomain.com/static/css/design-tokens.css
# Test HTTPS redirect
curl -I http://yourdomain.com
# Check security headers
curl -I https://yourdomain.com | grep -E "(Strict-Transport|X-Frame|X-Content)"
```
## Environment Variables Checklist
> **Complete Reference:** See [Environment Variables Reference](./configuration/environment-variables.md) for detailed documentation of all variables.
### Required (Production Will Fail Without These)
These variables have **no defaults** in production settings and must be explicitly set.
See [Production-Required Variables](./configuration/environment-variables.md#production-required-variables) for details.
```bash
# Security & Core (CRITICAL)
SECRET_KEY=<secure-random-key> # Generate with: python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"
DEBUG=False # NEVER True in production
DJANGO_SETTINGS_MODULE=config.django.production
# Host Configuration (NO DEFAULTS IN PRODUCTION)
ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com
CSRF_TRUSTED_ORIGINS=https://yourdomain.com,https://www.yourdomain.com # MUST include https://
# Database & Cache (REQUIRED)
DATABASE_URL=postgis://user:pass@host:5432/dbname
REDIS_URL=redis://host:6379/0 # Required for caching, sessions, and Celery
```
### Recommended
```bash
SENTRY_DSN=https://xxx@sentry.io/xxx
CLOUDFLARE_IMAGES_ACCOUNT_ID=xxx
CLOUDFLARE_IMAGES_API_TOKEN=xxx
EMAIL_HOST=smtp.provider.com
EMAIL_HOST_USER=xxx
EMAIL_HOST_PASSWORD=xxx
```
### Optional
```bash
REDIS_SESSIONS_URL=redis://host:6379/1
REDIS_API_URL=redis://host:6379/2
SENTRY_TRACES_SAMPLE_RATE=0.1
```
### Validation Command
Run this to validate your production configuration:
```bash
DJANGO_SETTINGS_MODULE=config.django.production python manage.py check --deploy
```

View File

@@ -1,344 +1,370 @@
# ThrillWiki Django + Vue.js Monorepo
# ThrillWiki - Django + HTMX Application
A comprehensive theme park and roller coaster information system built with a modern monorepo architecture combining Django REST API backend with Vue.js frontend.
A comprehensive theme park and roller coaster information system built with Django and HTMX, providing a progressive enhancement approach to modern web development.
## 🏗️ Architecture Overview
## Architecture Overview
This project uses a monorepo structure that cleanly separates backend and frontend concerns while maintaining shared resources and documentation:
ThrillWiki is a **Django monolith with HTMX-driven interactivity**, not a traditional SPA. This architecture provides:
- **Server-side rendering** for SEO and initial page load performance
- **Progressive enhancement** with HTMX for dynamic updates without full page reloads
- **REST API** for programmatic access and potential mobile app integration
- **Alpine.js** for minimal client-side state (limited to form validation and UI toggles)
```
thrillwiki-monorepo/
├── backend/ # Django REST API (Port 8000)
thrillwiki/
├── backend/ # Django application (main codebase)
│ ├── apps/ # Modular Django applications
│ │ ├── accounts/ # User management and authentication
│ │ ├── api/v1/ # REST API endpoints
│ │ ├── core/ # Shared utilities and base classes
│ │ ├── location/ # Geographic data and services
│ │ ├── media/ # File management (Cloudflare Images)
│ │ ├── moderation/ # Content moderation workflows
│ │ ├── parks/ # Theme park data and operations
│ │ └── rides/ # Ride information and relationships
│ ├── config/ # Django settings and configuration
│ ├── templates/ # Django templates
│ └── static/ # Static assets
├── frontend/ # Vue.js SPA (Port 5174)
│ ├── src/ # Vue.js source code
│ ├── public/ # Static assets
│ └── dist/ # Build output
├── shared/ # Shared resources and documentation
│ ├── docs/ # Comprehensive documentation
│ ├── django/ # Environment-specific settings
│ └── settings/ # Modular settings modules
│ ├── templates/ # Django templates with HTMX
│ ├── components/ # Reusable UI components
│ ├── htmx/ # HTMX partial templates
│ └── layouts/ # Base layout templates
│ └── static/ # Static assets (CSS, JS, images)
├── shared/ # Shared resources
│ ├── scripts/ # Development and deployment scripts
│ ├── config/ # Shared configuration
│ └── media/ # Shared media files
├── architecture/ # Architecture documentation
── profiles/ # Development profiles
── docs/ # Project documentation
└── tests/ # Test files
```
## 🚀 Quick Start
## Technology Stack
| Technology | Version | Purpose |
|------------|---------|---------|
| **Django** | 5.2.8+ | Web framework |
| **Django REST Framework** | 3.15.2+ | API framework |
| **HTMX** | 1.20.0+ | Dynamic UI updates |
| **Alpine.js** | 3.x | Minimal client-side state |
| **Tailwind CSS** | 3.x | Utility-first styling |
| **PostgreSQL/PostGIS** | 14+ | Database with geospatial support |
| **Redis** | 6+ | Caching and sessions |
| **Celery** | 5.5+ | Background task processing |
| **UV** | Latest | Python package management |
## Quick Start
### Prerequisites
- **Python 3.11+** with [uv](https://docs.astral.sh/uv/) for backend dependencies
- **Node.js 18+** with [pnpm](https://pnpm.io/) for frontend dependencies
- **PostgreSQL 14+** (optional, defaults to SQLite for development)
- **Redis 6+** (optional, for caching and sessions)
- **Python 3.13+** with [uv](https://docs.astral.sh/uv/) for package management
- **PostgreSQL 14+** with PostGIS extension
- **Redis 6+** for caching and sessions
### Development Setup
1. **Clone the repository**
```bash
git clone <repository-url>
cd thrillwiki-monorepo
cd thrillwiki
```
2. **Install dependencies**
```bash
# Install frontend dependencies
pnpm install
# Install backend dependencies
cd backend && uv sync && cd ..
cd backend
uv sync --frozen # Use locked versions for reproducibility
```
3. **Environment configuration**
```bash
# Copy environment files
cp .env.example .env
cp backend/.env.example backend/.env
cp frontend/.env.development frontend/.env.local
# Edit .env files with your settings
# Edit .env with your settings
```
4. **Database setup**
```bash
cd backend
uv run manage.py migrate
uv run manage.py createsuperuser
cd ..
```
5. **Start development servers**
5. **Start development server**
```bash
# Start both servers concurrently
pnpm run dev
# Or start individually
pnpm run dev:frontend # Vue.js on :5174
pnpm run dev:backend # Django on :8000
uv run manage.py runserver
```
## 📁 Project Structure Details
The application will be available at `http://localhost:8000`.
### Backend (`/backend`)
- **Django 5.0+** with REST Framework for API development
- **Modular app architecture** with separate apps for parks, rides, accounts, etc.
- **UV package management** for fast, reliable Python dependency management
- **PostgreSQL/SQLite** database with comprehensive entity relationships
- **Redis** for caching, sessions, and background tasks
- **Comprehensive API** with frontend serializers for camelCase conversion
## Project Structure Details
### Frontend (`/frontend`)
- **Vue 3** with Composition API and `<script setup>` syntax
- **TypeScript** for type safety and better developer experience
- **Vite** for lightning-fast development and optimized production builds
- **Tailwind CSS** with custom design system and dark mode support
- **Pinia** for state management with modular stores
- **Vue Router** for client-side routing
- **Comprehensive UI component library** with shadcn-vue components
### Django Applications
### Shared Resources (`/shared`)
- **Documentation** - Comprehensive guides and API documentation
- **Development scripts** - Automated setup, build, and deployment scripts
- **Configuration** - Shared Docker, CI/CD, and infrastructure configs
- **Media management** - Centralized media file handling and optimization
| App | Description |
|-----|-------------|
| **accounts** | User authentication, profiles, preferences, and social auth |
| **api/v1** | RESTful API endpoints with OpenAPI documentation |
| **core** | Shared utilities, managers, middleware, and services |
| **location** | Geographic models, geocoding, and map services |
| **media** | Cloudflare Images integration and file management |
| **moderation** | Content review workflows and moderation queue |
| **parks** | Theme park models, views, and related operations |
| **rides** | Ride models, coaster statistics, and ride history |
## 🛠️ Development Workflow
### HTMX Patterns
### Available Scripts
ThrillWiki uses HTMX for server-driven interactivity:
```bash
# Development
pnpm run dev # Start both servers concurrently
pnpm run dev:frontend # Frontend only (:5174)
pnpm run dev:backend # Backend only (:8000)
- **Partial templates** (`*_partial.html`) for dynamic content updates
- **HX-Trigger** for cross-component communication
- **hx-indicator** with skeleton loaders for loading states
- **Field-level validation** via HTMX for form feedback
# Building
pnpm run build # Build frontend for production
pnpm run build:staging # Build for staging environment
pnpm run build:production # Build for production environment
See [HTMX Patterns](./htmx-patterns.md) for detailed conventions.
# Testing
pnpm run test # Run all tests
pnpm run test:frontend # Frontend unit and E2E tests
pnpm run test:backend # Backend unit and integration tests
### Hybrid API/HTML Endpoints
# Code Quality
pnpm run lint # Lint all code
pnpm run type-check # TypeScript type checking
Many views serve dual purposes:
# Setup and Maintenance
pnpm run install:all # Install all dependencies
./shared/scripts/dev/setup-dev.sh # Full development setup
./shared/scripts/dev/start-all.sh # Start all services
```
- **HTML response** for browser requests (template rendering)
- **JSON response** for API requests (DRF serialization)
### Backend Development
This is achieved through content negotiation and hybrid view mixins.
## Development Workflow
### Running the Development Server
```bash
cd backend
uv run manage.py runserver
```
# Django management commands
### Django Management Commands
```bash
# Database operations
uv run manage.py migrate
uv run manage.py makemigrations
uv run manage.py createsuperuser
# Static files
uv run manage.py collectstatic
# Testing and quality
# Validate configuration
uv run manage.py validate_settings
# Testing
uv run manage.py test
uv run black . # Format code
uv run flake8 . # Lint code
uv run isort . # Sort imports
```
### Frontend Development
### Code Quality
```bash
cd frontend
# Format code
uv run black .
uv run isort .
# Vue.js development
pnpm run dev # Start dev server
pnpm run build # Production build
pnpm run preview # Preview production build
pnpm run test:unit # Vitest unit tests
pnpm run test:e2e # Playwright E2E tests
pnpm run lint # ESLint
pnpm run type-check # TypeScript checking
# Lint code
uv run ruff check .
uv run flake8 .
# Type checking
uv run pyright
```
## 🔧 Configuration
## Configuration
### Environment Variables
#### Root `.env`
```bash
# Database
DATABASE_URL=postgresql://user:pass@localhost/thrillwiki
REDIS_URL=redis://localhost:6379
ThrillWiki uses environment variables for configuration. Key variables:
# Security
SECRET_KEY=your-secret-key
DEBUG=True
| Variable | Description |
|----------|-------------|
| `SECRET_KEY` | Django secret key (required) |
| `DEBUG` | Debug mode (True/False) |
| `DATABASE_URL` | PostgreSQL connection URL |
| `REDIS_URL` | Redis connection URL |
| `DJANGO_SETTINGS_MODULE` | Settings module to use |
# API Configuration
API_BASE_URL=http://localhost:8000/api
See [Environment Variables](./configuration/environment-variables.md) for complete reference.
### Settings Architecture
ThrillWiki uses a modular settings architecture:
```
config/
├── django/ # Environment-specific settings
│ ├── base.py # Core settings
│ ├── local.py # Development overrides
│ ├── production.py # Production overrides
│ └── test.py # Test overrides
└── settings/ # Modular settings
├── cache.py # Redis caching
├── database.py # Database configuration
├── email.py # Email settings
├── logging.py # Logging configuration
├── rest_framework.py # DRF and CORS
├── security.py # Security headers
└── storage.py # Static/media files
```
#### Backend `.env`
```bash
# Django Settings
DJANGO_SETTINGS_MODULE=config.django.local
DEBUG=True
ALLOWED_HOSTS=localhost,127.0.0.1
## API Documentation
# Database
DATABASE_URL=postgresql://user:pass@localhost/thrillwiki
### Interactive Documentation
# Redis
REDIS_URL=redis://localhost:6379
- **Swagger UI**: `/api/docs/`
- **ReDoc**: `/api/redoc/`
- **OpenAPI Schema**: `/api/schema/`
# Email (optional)
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
EMAIL_USE_TLS=True
```
### API Overview
#### Frontend `.env.local`
```bash
# API Configuration
VITE_API_BASE_URL=http://localhost:8000/api
- **Base URL**: `/api/v1/`
- **Authentication**: JWT Bearer tokens or session-based
- **Content-Type**: `application/json`
# Development
VITE_APP_TITLE=ThrillWiki (Development)
See [API Documentation](./THRILLWIKI_API_DOCUMENTATION.md) for complete endpoint reference.
# Feature Flags
VITE_ENABLE_DEBUG=true
```
## Key Features
## 📊 Key Features
### Park Database
- Comprehensive theme park information worldwide
- Operator and property owner relationships
- Geographic data with PostGIS support
- Operating status and seasonal information
### Backend Features
- **Comprehensive Park Database** - Detailed information about theme parks worldwide
- **Extensive Ride Database** - Complete roller coaster and ride information
- **User Management** - Authentication, profiles, and permissions
- **Content Moderation** - Review and approval workflows
- **API Documentation** - Auto-generated OpenAPI/Swagger docs
- **Background Tasks** - Celery integration for long-running processes
- **Caching Strategy** - Redis-based caching for performance
- **Search Functionality** - Full-text search across all content
### Ride Database
- Complete ride specifications and statistics
- Roller coaster technical data (height, speed, inversions)
- Manufacturer and designer tracking
- Historical status and name changes
### Frontend Features
- **Responsive Design** - Mobile-first approach with Tailwind CSS
- **Dark Mode Support** - Complete dark/light theme system
- **Real-time Search** - Instant search with debouncing and highlighting
- **Interactive Maps** - Park and ride location visualization
- **Photo Galleries** - High-quality image management
- **User Dashboard** - Personalized content and contributions
- **Progressive Web App** - PWA capabilities for mobile experience
- **Accessibility** - WCAG 2.1 AA compliance
### User Features
- User authentication with social login (Google, Discord)
- Profile management and preferences
- Review and rating system
- Personalized content and contributions
## 📖 Documentation
### Content Moderation
- Review and approval workflows
- Moderation queue management
- User moderation actions
- Bulk operations support
### API Capabilities
- Full CRUD for all entities
- Advanced filtering and search
- Pagination and sorting
- Rate limiting and caching
## Documentation
### Core Documentation
- **[Backend Documentation](./backend/README.md)** - Django setup and API details
- **[Frontend Documentation](./frontend/README.md)** - Vue.js setup and development
- **[API Documentation](./shared/docs/api/README.md)** - Complete API reference
- **[Development Workflow](./shared/docs/development/workflow.md)** - Daily development processes
- [Backend Documentation](../backend/README.md) - Setup and development details
- [API Documentation](./THRILLWIKI_API_DOCUMENTATION.md) - Complete API reference
- [Setup Guide](./SETUP_GUIDE.md) - Comprehensive setup instructions
### Architecture & Deployment
- **[Architecture Overview](./architecture/)** - System design and decisions
- **[Deployment Guide](./shared/docs/deployment/)** - Production deployment instructions
- **[Development Scripts](./shared/scripts/)** - Automation and tooling
### Configuration
- [Environment Variables](./configuration/environment-variables.md) - Complete reference
- [Secret Management](./configuration/secret-management.md) - Secret handling
### Additional Resources
- **[Contributing Guide](./CONTRIBUTING.md)** - How to contribute to the project
- **[Code of Conduct](./CODE_OF_CONDUCT.md)** - Community guidelines
- **[Security Policy](./SECURITY.md)** - Security reporting and policies
### Architecture
- [Architecture Decisions](./architecture/) - ADRs for key decisions
- [Deployment Guide](../architecture/deployment-guide.md) - Production deployment
- [Health Checks](./HEALTH_CHECKS.md) - Monitoring endpoints
## 🚀 Deployment
### Accessibility
- [Keyboard Navigation](./accessibility/keyboard-navigation.md) - Keyboard shortcuts
- [Screen Reader Testing](./accessibility/screen-reader-testing.md) - Testing checklist
- [Component Patterns](./accessibility/component-patterns.md) - Accessible patterns
## Testing
### Running Tests
```bash
# Run all tests
uv run manage.py test
# Run specific app tests
uv run manage.py test apps.parks
uv run manage.py test apps.rides
# Run with coverage
uv run coverage run manage.py test
uv run coverage report
```
### Accessibility Testing
```bash
# Run accessibility tests
uv run manage.py test backend.tests.accessibility
```
## Deployment
### Development Environment
```bash
# Quick start with all services
./shared/scripts/dev/start-all.sh
# Start development server
uv run manage.py runserver
# Full development setup
./shared/scripts/dev/setup-dev.sh
```
### Production Deployment
```bash
# Build all components
./shared/scripts/build/build-all.sh
# Deploy to production
./shared/scripts/deploy/deploy.sh
```
See [Deployment Guide](../architecture/deployment-guide.md) for detailed production setup instructions.
See [Deployment Guide](./shared/docs/deployment/) for detailed production setup instructions.
Key production requirements:
- `DEBUG=False`
- SSL/HTTPS enforcement
- Redis for caching and sessions
- PostgreSQL with connection pooling
- Proper secret management
## 🧪 Testing Strategy
### Production Checklist
### Backend Testing
- **Unit Tests** - Individual function and method testing
- **Integration Tests** - API endpoint and database interaction testing
- **E2E Tests** - Full user journey testing with Selenium
See [Production Checklist](./PRODUCTION_CHECKLIST.md) for deployment verification.
### Frontend Testing
- **Unit Tests** - Component and utility function testing with Vitest
- **Integration Tests** - Component interaction testing
- **E2E Tests** - User journey testing with Playwright
## Contributing
### Code Quality
- **Linting** - ESLint for JavaScript/TypeScript, Flake8 for Python
- **Type Checking** - TypeScript for frontend, mypy for Python
- **Code Formatting** - Prettier for frontend, Black for Python
We welcome contributions! Please follow these guidelines:
## 🤝 Contributing
We welcome contributions! Please see our [Contributing Guide](./CONTRIBUTING.md) for details on:
1. **Development Setup** - Getting your development environment ready
2. **Code Standards** - Coding conventions and best practices
3. **Pull Request Process** - How to submit your changes
4. **Issue Reporting** - How to report bugs and request features
1. **Development Setup** - Follow the setup instructions above
2. **Code Standards** - Follow Django coding standards and use provided linters
3. **Testing** - Write tests for new features
4. **Documentation** - Update documentation as needed
### Quick Contribution Start
```bash
# Fork and clone the repository
git clone https://github.com/your-username/thrillwiki-monorepo.git
cd thrillwiki-monorepo
git clone https://github.com/your-username/thrillwiki.git
cd thrillwiki
# Set up development environment
./shared/scripts/dev/setup-dev.sh
cd backend && uv sync
# Create a feature branch
git checkout -b feature/your-feature-name
# Make your changes and test
pnpm run test
uv run manage.py test
# Submit a pull request
```
## 📄 License
## License
This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details.
This project is licensed under the MIT License - see the [LICENSE](../LICENSE) file for details.
## 🙏 Acknowledgments
## Support
- **Theme Park Community** - For providing data and inspiration
- **Open Source Contributors** - For the amazing tools and libraries
- **Vue.js and Django Communities** - For excellent documentation and support
## 📞 Support
- **Issues** - [GitHub Issues](https://github.com/your-repo/thrillwiki-monorepo/issues)
- **Discussions** - [GitHub Discussions](https://github.com/your-repo/thrillwiki-monorepo/discussions)
- **Documentation** - [Project Wiki](https://github.com/your-repo/thrillwiki-monorepo/wiki)
- **Issues**: [GitHub Issues](https://github.com/your-repo/thrillwiki/issues)
- **Discussions**: [GitHub Discussions](https://github.com/your-repo/thrillwiki/discussions)
---
**Built with ❤️ for the theme park and roller coaster community**
**Built with Django + HTMX for the theme park and roller coaster community**

368
docs/SETUP_GUIDE.md Normal file
View File

@@ -0,0 +1,368 @@
# ThrillWiki Setup Guide
This guide provides comprehensive instructions for setting up the ThrillWiki development environment.
## Prerequisites
### Required Software
| Software | Minimum Version | Notes |
|----------|----------------|-------|
| Python | 3.13+ | Required for Django 5.2 |
| PostgreSQL | 14+ | With PostGIS extension |
| Redis | 6+ | For caching and sessions |
| UV | Latest | Python package manager |
### Platform-Specific Prerequisites
#### macOS (with Homebrew)
```bash
# Install Homebrew if not present
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# Install dependencies
brew install python@3.13 postgresql@15 postgis redis gdal geos proj
# Start services
brew services start postgresql@15
brew services start redis
# Install UV
curl -LsSf https://astral.sh/uv/install.sh | sh
```
#### Linux (Ubuntu/Debian)
```bash
# Update package list
sudo apt update
# Install Python 3.13
sudo add-apt-repository ppa:deadsnakes/ppa
sudo apt install python3.13 python3.13-venv python3.13-dev
# Install PostgreSQL with PostGIS
sudo apt install postgresql-15 postgresql-15-postgis-3 postgresql-contrib
# Install Redis
sudo apt install redis-server
# Install GDAL and GEOS for GeoDjango
sudo apt install libgdal-dev libgeos-dev binutils libproj-dev
# Start services
sudo systemctl start postgresql
sudo systemctl start redis
# Install UV
curl -LsSf https://astral.sh/uv/install.sh | sh
```
#### Windows (with WSL2)
```bash
# Use WSL2 with Ubuntu
wsl --install -d Ubuntu
# Inside WSL2, follow Linux instructions above
```
## Step-by-Step Setup
### 1. Clone the Repository
```bash
git clone <repository-url>
cd thrillwiki
```
### 2. Database Setup
#### Create PostgreSQL Database
```bash
# Access PostgreSQL
sudo -u postgres psql
# Create database and user
CREATE USER thrillwiki WITH PASSWORD 'your-password';
CREATE DATABASE thrillwiki OWNER thrillwiki;
# Enable PostGIS extension
\c thrillwiki
CREATE EXTENSION postgis;
CREATE EXTENSION postgis_topology;
# Grant privileges
GRANT ALL PRIVILEGES ON DATABASE thrillwiki TO thrillwiki;
\q
```
#### Verify PostGIS Installation
```bash
sudo -u postgres psql -d thrillwiki -c "SELECT PostGIS_version();"
```
### 3. Install Python Dependencies
```bash
cd backend
# Install dependencies with UV
uv sync --frozen
# Or allow updates within version constraints
uv sync
```
### 4. Configure Environment Variables
```bash
# Copy example environment file
cp .env.example .env
```
Edit `.env` with your settings:
```bash
# Required settings
SECRET_KEY=generate-a-secure-key-here
DEBUG=True
DJANGO_SETTINGS_MODULE=config.django.local
ALLOWED_HOSTS=localhost,127.0.0.1
# Database
DATABASE_URL=postgis://thrillwiki:your-password@localhost:5432/thrillwiki
# Redis
REDIS_URL=redis://localhost:6379/1
# GeoDjango (macOS with Homebrew)
GDAL_LIBRARY_PATH=/opt/homebrew/lib/libgdal.dylib
GEOS_LIBRARY_PATH=/opt/homebrew/lib/libgeos_c.dylib
```
Generate a secure secret key:
```bash
python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"
```
### 5. Run Database Migrations
```bash
cd backend
uv run manage.py migrate
```
### 6. Create Superuser
```bash
uv run manage.py createsuperuser
```
### 7. Collect Static Files (Optional for Development)
```bash
uv run manage.py collectstatic --noinput
```
### 8. Start Development Server
```bash
uv run manage.py runserver
```
The application will be available at `http://localhost:8000`.
## Verification
### Check Django Configuration
```bash
uv run manage.py check
```
### Validate Settings
```bash
uv run manage.py validate_settings
```
### Run Tests
```bash
uv run manage.py test
```
### Access Admin Panel
1. Navigate to `http://localhost:8000/admin/`
2. Login with superuser credentials
## Common Setup Issues
### Issue: PostGIS Not Found
**Error**: `Could not find the GDAL library`
**Solution**:
```bash
# macOS
export GDAL_LIBRARY_PATH=/opt/homebrew/lib/libgdal.dylib
export GEOS_LIBRARY_PATH=/opt/homebrew/lib/libgeos_c.dylib
# Add to .env file for persistence
```
### Issue: Database Connection Error
**Error**: `could not connect to server`
**Solution**:
```bash
# Check PostgreSQL is running
sudo systemctl status postgresql # Linux
brew services list # macOS
# Check connection string
psql $DATABASE_URL
```
### Issue: Redis Connection Error
**Error**: `Error connecting to Redis`
**Solution**:
```bash
# Check Redis is running
redis-cli ping
# Should return: PONG
# Start Redis if not running
sudo systemctl start redis # Linux
brew services start redis # macOS
```
### Issue: Python Version Mismatch
**Error**: `Python 3.13+ required`
**Solution**:
```bash
# Check Python version
python --version
# Use specific Python version with UV
UV_PYTHON=python3.13 uv sync
```
### Issue: Migration Errors
**Error**: `django.db.utils.ProgrammingError`
**Solution**:
```bash
# Reset migrations (development only!)
uv run manage.py migrate --fake-initial
# Or recreate database
sudo -u postgres dropdb thrillwiki
sudo -u postgres createdb thrillwiki -O thrillwiki
sudo -u postgres psql -d thrillwiki -c "CREATE EXTENSION postgis;"
uv run manage.py migrate
```
## Optional Configuration
### Enable Debug Toolbar
```bash
# Install with profiling group
uv sync --group profiling
# Debug toolbar is enabled by default in local.py
```
### Configure Email for Development
```python
# Uses console backend by default
# Emails are printed to console in development
EMAIL_BACKEND=django.core.mail.backends.console.EmailBackend
```
### Load Sample Data
```bash
# If sample data fixtures exist
uv run manage.py loaddata sample_parks.json
uv run manage.py loaddata sample_rides.json
```
## Development Workflow
### Running the Server
```bash
cd backend
uv run manage.py runserver
```
### Running with Celery (for background tasks)
```bash
# Terminal 1: Django server
uv run manage.py runserver
# Terminal 2: Celery worker
uv run celery -A config.celery worker -l info
# Terminal 3: Celery beat (for scheduled tasks)
uv run celery -A config.celery beat -l info
```
### Code Quality Commands
```bash
# Format code
uv run black .
uv run isort .
# Lint code
uv run ruff check .
uv run flake8 .
# Type checking
uv run pyright
```
### Making Changes
```bash
# Create migrations after model changes
uv run manage.py makemigrations
# Apply migrations
uv run manage.py migrate
# Run tests
uv run manage.py test
```
## Next Steps
After completing setup:
1. **Explore the Admin Panel**: `/admin/`
2. **View API Documentation**: `/api/docs/`
3. **Read HTMX Patterns**: [htmx-patterns.md](./htmx-patterns.md)
4. **Review Architecture**: [Architecture Decisions](./architecture/)
## Getting Help
- Check [Common Issues](#common-setup-issues) above
- Review [Environment Variables](./configuration/environment-variables.md)
- See [Backend README](../backend/README.md) for development details

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,350 @@
# Accessible Component Patterns
This document provides code examples for creating accessible components in ThrillWiki. Follow these patterns to ensure WCAG 2.1 AA compliance.
## Button
### Standard Button
```django
{% include 'components/ui/button.html' with
text='Save Changes'
variant='primary'
type='submit'
%}
```
### Icon Button (REQUIRED: aria_label)
```django
{% include 'components/ui/button.html' with
icon=close_svg
size='icon'
aria_label='Close dialog'
variant='ghost'
%}
```
**Important**: Icon-only buttons MUST include `aria_label` for screen reader accessibility.
### Disabled Button
```django
{% include 'components/ui/button.html' with
text='Submit'
disabled=True
%}
```
### Button with HTMX
```django
{% include 'components/ui/button.html' with
text='Load More'
hx_get='/api/items?page=2'
hx_target='#item-list'
hx_swap='beforeend'
%}
```
## Form Field
### Standard Field
```django
{% include 'forms/partials/form_field.html' with
field=form.email
%}
```
The form_field.html component automatically handles:
- Label association via `for` attribute
- Error message display with proper ARIA
- Required field indication
- Help text with `aria-describedby`
### Field with Help Text
```django
{% include 'forms/partials/form_field.html' with
field=form.password
help_text='Must be at least 8 characters'
%}
```
### Field with HTMX Validation
```django
{% include 'forms/partials/form_field.html' with
field=form.username
hx_validate=True
hx_validate_url='/api/validate-username/'
%}
```
### Complete Form Example
```django
<form method="post" role="form" aria-label="User registration form">
{% csrf_token %}
<fieldset>
<legend class="sr-only">Account Information</legend>
{% include 'forms/partials/form_field.html' with field=form.username %}
{% include 'forms/partials/form_field.html' with field=form.email %}
</fieldset>
<fieldset>
<legend class="sr-only">Security</legend>
{% include 'forms/partials/form_field.html' with field=form.password1 %}
{% include 'forms/partials/form_field.html' with field=form.password2 %}
</fieldset>
{% include 'components/ui/button.html' with
text='Create Account'
type='submit'
variant='primary'
%}
</form>
```
## Modal
### Basic Modal
```django
{% extends 'components/modals/modal_base.html' %}
{% block modal_body %}
<p>Are you sure you want to delete this item?</p>
{% endblock %}
```
### Modal with Actions
```django
{% extends 'components/modals/modal_base.html' %}
{% block modal_body %}
<p>This action cannot be undone.</p>
{% endblock %}
{% block modal_footer %}
<button @click="{{ show_var }} = false" type="button">
Cancel
</button>
{% include 'components/ui/button.html' with
text='Delete'
variant='destructive'
x_on_click='confirmDelete()'
%}
{% endblock %}
```
The modal_inner.html component automatically provides:
- `role="dialog"` and `aria-modal="true"`
- `aria-labelledby` pointing to title
- `aria-describedby` pointing to body (and subtitle if present)
- Focus trap with Tab/Shift+Tab cycling
- Home/End key support for first/last focusable element
- Escape key to close (configurable)
- Auto-focus on first focusable element
## Navigation Menu
### Dropdown Menu
```django
<div x-data="{ open: false }" @click.outside="open = false" @keydown.escape="open = false">
<button
@click="open = !open"
aria-haspopup="true"
:aria-expanded="open.toString()"
aria-label="User menu">
<span>Menu</span>
<i class="fas fa-chevron-down" aria-hidden="true"></i>
</button>
<div
x-show="open"
x-transition
role="menu"
aria-label="User account options"
class="dropdown-menu">
<a role="menuitem" href="/profile" class="menu-item">Profile</a>
<a role="menuitem" href="/settings" class="menu-item">Settings</a>
<div role="separator" class="border-t"></div>
<button role="menuitem" type="submit" class="menu-item">Logout</button>
</div>
</div>
```
## Search
### Accessible Search with Results
```django
<div role="search">
<label for="search-input" class="sr-only">Search parks and rides</label>
<input
id="search-input"
type="search"
placeholder="Search..."
hx-get="/search"
hx-target="#search-results"
hx-trigger="input changed delay:300ms"
autocomplete="off"
aria-describedby="search-status"
aria-controls="search-results"
/>
<div
id="search-results"
role="listbox"
aria-label="Search results"
aria-live="polite">
<!-- Results populated by HTMX -->
</div>
<div id="search-status" class="sr-only" aria-live="polite" aria-atomic="true">
<!-- Status announcements -->
</div>
</div>
```
### Search Result Item
```django
<div role="option" aria-selected="false" class="search-result">
<a href="{{ result.url }}">
<span>{{ result.name }}</span>
<span class="text-muted-foreground">{{ result.type }}</span>
</a>
</div>
```
## Live Regions
### Status Announcements
```django
<div role="status" aria-live="polite" aria-atomic="true" class="sr-only">
{{ status_message }}
</div>
```
Use `aria-live="polite"` for non-urgent updates that can wait for user to finish current action.
### Alert Messages
```django
<div role="alert" aria-live="assertive">
<i class="fas fa-exclamation-circle" aria-hidden="true"></i>
{{ error_message }}
</div>
```
Use `aria-live="assertive"` for critical errors that require immediate attention.
### Loading State
```django
<div role="status" aria-live="polite" aria-busy="true">
<span class="sr-only">Loading results...</span>
<div class="htmx-indicator" aria-hidden="true">
<i class="fas fa-spinner fa-spin"></i>
</div>
</div>
```
## Images
### Meaningful Image
```django
<img
src="{{ park.image.url }}"
alt="{{ park.name }} - {{ park.location }}"
/>
```
### Decorative Image
```django
<img src="/decorative-pattern.svg" alt="" role="presentation" />
```
### Avatar with Name
```django
{% if user.profile.avatar %}
<img
src="{{ user.profile.avatar.url }}"
alt="{{ user.get_full_name|default:user.username }}'s profile picture"
class="avatar"
/>
{% else %}
<div class="avatar-placeholder" aria-hidden="true">
{{ user.username.0|upper }}
</div>
{% endif %}
```
## Breadcrumbs
```django
{% include 'components/navigation/breadcrumbs.html' %}
```
The breadcrumbs component automatically provides:
- `<nav>` element with `aria-label="Breadcrumb"`
- Ordered list for semantic structure
- `aria-current="page"` on current page
- Hidden separators with `aria-hidden="true"`
- Schema.org JSON-LD structured data
## Skip Link
Add to the top of your base template:
```django
<a href="#main-content" class="skip-link sr-only-focusable">
Skip to main content
</a>
<!-- ... header and navigation ... -->
<main id="main-content" tabindex="-1">
{% block content %}{% endblock %}
</main>
```
## Theme Toggle
```django
<button
@click="toggleTheme()"
aria-label="Toggle theme"
:aria-pressed="isDarkMode.toString()"
class="theme-toggle">
<i class="fas fa-sun" aria-hidden="true"></i>
<i class="fas fa-moon" aria-hidden="true"></i>
</button>
```
## Focus Management Utilities
### Focus Trap (Alpine.js)
```javascript
// Already implemented in modal_inner.html
@keydown.tab.prevent="trapFocus($event)"
```
### Return Focus After Action
```javascript
// Store the trigger element
const trigger = document.activeElement;
// After modal closes
trigger.focus();
```
## Testing Your Components
### Quick Accessibility Audit
1. Can you Tab through all interactive elements?
2. Is focus indicator visible?
3. Does Enter/Space activate buttons?
4. Does Escape close modals/dropdowns?
5. Does screen reader announce all content?
6. Is color not the only indicator of state?
### Automated Testing
```bash
# Run accessibility tests
python manage.py test backend.tests.accessibility
# Use axe browser extension for quick audits
# Install from: https://www.deque.com/axe/
```

View File

@@ -0,0 +1,138 @@
# Keyboard Navigation Guide
ThrillWiki is designed to be fully accessible via keyboard. This guide documents all keyboard shortcuts and navigation patterns.
## Global Shortcuts
| Key | Action |
|-----|--------|
| Tab | Move focus forward |
| Shift + Tab | Move focus backward |
| Enter | Activate focused element |
| Space | Activate buttons/checkboxes |
| Escape | Close modals/dropdowns |
## Navigation Menu
| Key | Action |
|-----|--------|
| Tab | Focus menu button |
| Enter/Space | Open menu |
| Arrow Down | Next menu item |
| Arrow Up | Previous menu item |
| Escape | Close menu |
| Home | First menu item |
| End | Last menu item |
## Search
| Key | Action |
|-----|--------|
| Tab | Focus search input |
| Type | Start searching (debounced 300ms) |
| Arrow Down | Navigate to next result |
| Arrow Up | Navigate to previous result (or back to input) |
| Enter | Select current result (navigate to link) |
| Escape | Close results and blur input |
| Home | Jump to first result |
| End | Jump to last result |
## Modals
| Key | Action |
|-----|--------|
| Tab | Next focusable element (trapped within modal) |
| Shift + Tab | Previous focusable element (trapped within modal) |
| Escape | Close modal |
| Home | Jump to first focusable element |
| End | Jump to last focusable element |
## Forms
| Key | Action |
|-----|--------|
| Tab | Move to next field |
| Shift + Tab | Move to previous field |
| Enter | Submit form (when focused on submit button) |
| Space | Toggle checkbox/radio button |
| Arrow Down/Up | Navigate select options |
| Escape | Close select dropdown |
## Dropdowns (User Menu, Browse Menu)
| Key | Action |
|-----|--------|
| Enter/Space | Toggle dropdown open/closed |
| Escape | Close dropdown |
| Arrow Down | Navigate to next item |
| Arrow Up | Navigate to previous item |
| Tab | Move through items (or exit dropdown) |
## Mobile Menu
| Key | Action |
|-----|--------|
| Enter/Space | Open mobile menu |
| Escape | Close mobile menu |
| Tab | Navigate through menu items |
## Theme Toggle
| Key | Action |
|-----|--------|
| Enter/Space | Toggle between light and dark mode |
## Implementation Notes
### Focus Management
- All interactive elements must be reachable via keyboard
- Focus order follows logical reading order (top to bottom, left to right)
- Focus is trapped within modals when open
- Focus returns to trigger element when modal/dropdown closes
### Focus Indicators
- All focusable elements have visible focus indicators
- Focus ring: 2px solid primary color with 2px offset
- Enhanced visibility in high contrast mode
- Custom focus styles for complex components
### ARIA Attributes
- `aria-expanded`: Indicates dropdown/menu open state
- `aria-haspopup`: Indicates element triggers a popup
- `aria-label`: Provides accessible name for icon-only buttons
- `aria-modal`: Indicates modal dialog
- `aria-controls`: Links trigger to controlled element
- `aria-describedby`: Links element to description
- `aria-live`: Announces dynamic content changes
### Skip Links
- Skip to main content link available (hidden until focused)
- Appears at top of page when Tab is pressed first
## Testing Keyboard Navigation
### Manual Testing Checklist
1. Navigate entire site using only keyboard
2. Verify all interactive elements are reachable with Tab
3. Verify Enter/Space activates all buttons and links
4. Verify Escape closes all modals and dropdowns
5. Verify focus indicators are visible
6. Verify focus order is logical
7. Verify focus is trapped in modals
8. Verify focus returns to trigger after closing modals
### Automated Testing
Run accessibility tests:
```bash
python manage.py test backend.tests.accessibility
```
## Browser Compatibility
Keyboard navigation is tested on:
- Chrome (latest)
- Firefox (latest)
- Safari (latest)
- Edge (latest)
Note: Some keyboard shortcuts may conflict with browser shortcuts. In those cases, browser shortcuts take precedence.

View File

@@ -0,0 +1,200 @@
# Screen Reader Testing Checklist
This document provides a comprehensive checklist for testing ThrillWiki with screen readers to ensure WCAG 2.1 AA compliance.
## Recommended Screen Readers
| Screen Reader | Platform | Cost | Notes |
|--------------|----------|------|-------|
| NVDA | Windows | Free | Most widely used free option |
| JAWS | Windows | Commercial | Industry standard |
| VoiceOver | macOS/iOS | Built-in | Activate with Cmd+F5 |
| TalkBack | Android | Built-in | Enable in Accessibility settings |
| Narrator | Windows | Built-in | Basic testing |
## Test Scenarios
### Navigation
- [ ] Skip to main content link works and is announced
- [ ] All navigation items are announced with their role
- [ ] Current page is indicated (aria-current="page")
- [ ] Dropdown menus announce as "button, collapsed" when closed
- [ ] Dropdown menus announce as "button, expanded" when open
- [ ] Menu items announce with role="menuitem"
- [ ] Breadcrumbs are announced as navigation landmark
### Search
- [ ] Search input is announced with label "Search parks and rides"
- [ ] Search results count is announced when results appear
- [ ] Each result is announced with its content
- [ ] Arrow key navigation announces current selection
- [ ] "No results found" is announced when applicable
### Forms
- [ ] All form fields have associated labels announced
- [ ] Required fields announce "required"
- [ ] Error messages are announced immediately when form validates
- [ ] Success messages are announced after form submission
- [ ] Help text is associated and announced with fields
- [ ] Field types are announced (text input, checkbox, select, etc.)
### Interactive Components
- [ ] Buttons announce their purpose/label
- [ ] Icon-only buttons announce their aria-label
- [ ] Links announce destination or purpose
- [ ] Modals announce their title when opened
- [ ] Modal content is announced as dialog
- [ ] Closing modal announces return to previous context
- [ ] Theme toggle announces current state (pressed/not pressed)
### Dynamic Content
- [ ] Search results are announced via aria-live region
- [ ] Filter changes announce result count
- [ ] Status updates (success, error) are announced
- [ ] Loading states are announced ("Loading...")
- [ ] HTMX content swaps announce via live regions
### Images
- [ ] All meaningful images have descriptive alt text
- [ ] Decorative images are hidden (alt="" or aria-hidden)
- [ ] Complex images have long descriptions available
- [ ] Avatar images include user's name in alt text
### Tables (if applicable)
- [ ] Tables have proper headers (th elements)
- [ ] Table caption or aria-label describes table purpose
- [ ] Data cells associate with headers
### Headings
- [ ] Page has exactly one h1
- [ ] Heading hierarchy is logical (h1 > h2 > h3)
- [ ] No skipped heading levels
- [ ] Headings describe section content
## Testing Commands
### NVDA (Windows)
| Command | Action |
|---------|--------|
| Insert + Down Arrow | Read next item |
| Insert + Up Arrow | Read previous item |
| Insert + Space | Read current item |
| Insert + F7 | Elements list (links, headings, landmarks) |
| H | Next heading |
| Shift + H | Previous heading |
| K | Next link |
| F | Next form field |
| D | Next landmark |
| Insert + Ctrl + N | Read notifications |
### VoiceOver (macOS)
| Command | Action |
|---------|--------|
| VO + Right Arrow | Next item |
| VO + Left Arrow | Previous item |
| VO + U | Rotor (elements list) |
| VO + A | Read all from current position |
| VO + Cmd + H | Next heading |
| VO + Cmd + J | Next form control |
| VO + Cmd + L | Next link |
Note: VO = Control + Option
### VoiceOver (iOS)
| Gesture | Action |
|---------|--------|
| Swipe Right | Next item |
| Swipe Left | Previous item |
| Double Tap | Activate item |
| Two-finger Swipe Up | Read all from current position |
| Rotor (two-finger twist) | Change navigation mode |
### JAWS (Windows)
| Command | Action |
|---------|--------|
| Down Arrow | Next item |
| Up Arrow | Previous item |
| Insert + F5 | Forms list |
| Insert + F6 | Headings list |
| Insert + F7 | Links list |
| H | Next heading |
| F | Next form field |
| T | Next table |
| R | Next region/landmark |
### TalkBack (Android)
| Gesture | Action |
|---------|--------|
| Swipe Right | Next item |
| Swipe Left | Previous item |
| Double Tap | Activate item |
| Swipe Up then Down | Navigation settings |
## Common Issues to Watch For
### Problematic Patterns
1. **Missing labels**: Form fields without associated labels
2. **Duplicate IDs**: Multiple elements with same ID breaks aria-describedby
3. **Empty buttons**: Buttons with no text or aria-label
4. **Inaccessible modals**: Focus not trapped, no escape to close
5. **Auto-playing media**: Audio/video that plays automatically
6. **Timeout without warning**: Sessions expiring without notice
7. **Moving focus unexpectedly**: Focus jumping after interactions
8. **Color-only information**: Status conveyed only by color
### Good Patterns
1. **Clear labels**: Every form field has descriptive label
2. **Error prevention**: Clear instructions, validation before submit
3. **Focus management**: Logical order, visible indicators, trapped in modals
4. **Consistent navigation**: Same navigation pattern on all pages
5. **Multiple ways**: Multiple paths to same content
6. **Descriptive links**: Link text describes destination (not "click here")
## Reporting Issues
When reporting accessibility issues, include:
1. Screen reader and version used
2. Browser and version
3. Page URL
4. Steps to reproduce
5. Expected behavior
6. Actual behavior (what was announced)
7. WCAG success criterion violated
## WCAG 2.1 AA Quick Reference
### Level A (Must Have)
- 1.1.1 Non-text Content (alt text)
- 1.3.1 Info and Relationships (semantic HTML)
- 2.1.1 Keyboard (all functionality via keyboard)
- 2.4.1 Bypass Blocks (skip links)
- 4.1.2 Name, Role, Value (ARIA)
### Level AA (Should Have)
- 1.4.3 Contrast (Minimum) (4.5:1 ratio)
- 1.4.4 Resize Text (200% zoom)
- 2.4.6 Headings and Labels (descriptive)
- 2.4.7 Focus Visible (visible focus indicator)
- 3.2.3 Consistent Navigation
- 3.2.4 Consistent Identification
## Resources
- [WCAG 2.1 Guidelines](https://www.w3.org/TR/WCAG21/)
- [WebAIM Screen Reader Survey](https://webaim.org/projects/screenreadersurvey9/)
- [NVDA User Guide](https://www.nvaccess.org/files/nvda/documentation/userGuide.html)
- [VoiceOver User Guide](https://support.apple.com/guide/voiceover/welcome/mac)
- [JAWS Quick Start](https://www.freedomscientific.com/products/software/jaws/)

138
docs/admin/CHANGELOG.md Normal file
View File

@@ -0,0 +1,138 @@
# Admin Interface Changelog
## Phase 11: Django Admin Standardization
### Added
- **Base Admin Classes and Mixins** (`apps/core/admin/`)
- `BaseModelAdmin` - Standard base class with consistent pagination and settings
- `QueryOptimizationMixin` - Automatic select_related/prefetch_related optimization
- `ReadOnlyAdminMixin` - Disable modifications for auto-generated data
- `TimestampFieldsMixin` - Standard handling for created_at/updated_at
- `SlugFieldMixin` - Automatic slug prepopulation
- `ExportActionMixin` - CSV/JSON export functionality
- `BulkStatusChangeMixin` - Bulk status change actions
- `ModerationMixin` - Standard moderation approve/reject actions
- **Query Optimization Across All Admin Classes**
- `list_select_related` for all ForeignKey fields in list_display
- `list_prefetch_related` for reverse relations and M2M fields
- Queryset annotations for calculated fields (ride counts, average ratings)
- **Custom Bulk Actions**
- Parks: `bulk_activate`, `bulk_deactivate`, `recalculate_stats`
- Rides: `bulk_set_operating`, `bulk_set_closed`, `bulk_set_sbno`, `recalculate_ratings`
- Accounts: `activate_users`, `deactivate_users`, `ban_users`, `unban_users`, `send_verification_email`, `recalculate_credits`
- Reviews: `bulk_approve`, `bulk_reject`, `flag_for_review`
- Moderation: `bulk_approve`, `bulk_reject`, `bulk_escalate`
- Photos: `set_primary`, `remove_primary`, `flag_missing_alt`
- Tokens: `resend_verification`, `delete_expired`, `cleanup_old_tokens`
- Lists: `publish_lists`, `unpublish_lists`, `move_up`, `move_down`
- **Export Functionality**
- CSV export for all major models
- JSON export for all major models
- Audit trail export for state logs
- SEO redirect export for slug history
- **Enhanced Display Methods**
- Color-coded status badges for all status fields
- Clickable links to related objects in list views
- Rating displays with star formatting
- Thumbnail previews for images
- Profile completeness indicators
- Moderation status indicators
- **Comprehensive Fieldsets**
- Standardized fieldset organization across all admins
- Collapsible sections for advanced/metadata fields
- Descriptive help text for all fieldsets
- **Documentation**
- `docs/admin/base_classes.md` - Base admin architecture documentation
- `docs/admin/CHANGELOG.md` - This changelog
- **Test Coverage**
- `apps/core/tests/test_admin.py` - Tests for base classes and mixins
- `apps/parks/tests/test_admin.py` - Tests for parks admin
- `apps/rides/tests/test_admin.py` - Tests for rides admin
- `apps/accounts/tests/test_admin.py` - Tests for accounts admin
- `apps/moderation/tests/test_admin.py` - Tests for moderation admin
### Changed
- **Parks Admin** (`apps/parks/admin.py`)
- Optimized all querysets with select_related/prefetch_related
- Added ride count and average rating annotations
- Enhanced list_display with clickable links
- Added autocomplete_fields for ForeignKeys
- Standardized fieldsets with descriptions
- **Rides Admin** (`apps/rides/admin.py`)
- Optimized all querysets with select_related/prefetch_related
- Added review count and average rating annotations
- Enhanced category and status badges with color coding
- Added FSM-aware status change actions
- Maintained read-only status for rankings
- **Accounts Admin** (`apps/accounts/admin.py`)
- Optimized user queryset with profile select_related
- Added total credits display with breakdown tooltip
- Enhanced user status badge display
- Added profile completeness indicator
- Added social media presence indicator
- **Core Admin** (`apps/core/admin.py`)
- Enhanced SlugHistory with content type display
- Added admin URL linking for related objects
- Added SEO export functionality
- **Media Admin** (`shared/media/admin.py`)
- Enhanced thumbnail preview with lazy loading
- Added alt text validation warnings
- Added primary photo management actions
- **Moderation Admin** (`apps/moderation/admin.py`)
- Enhanced dashboard with pending counts
- Added changes preview formatting
- Enhanced state log display with badges
- Added audit trail export
### Fixed
- N+1 query issues in all admin list views
- Inconsistent permission handling across admins
- Missing help text on admin fields
- Content object link errors when objects are deleted
### Removed
- Duplicate admin file at `apps/accounts/admin.py` (root level)
- Redundant code patterns replaced by mixins
### Performance Improvements
- **Query Optimization**
- Parks list view: Reduced from ~50+ queries to <10 queries
- Rides list view: Reduced from ~100+ queries to <15 queries
- User list view: Reduced from ~30+ queries to <10 queries
- Moderation views: Reduced from ~40+ queries to <12 queries
- **Page Settings**
- Consistent pagination at 50 items per page
- Disabled full result count for large datasets
- Added date hierarchy for time-based filtering
### Security Enhancements
- Standardized read-only permissions for auto-generated data
- Superuser-only delete permissions for audit logs
- Self-protection in user ban/deactivate actions
- FSM validation in status change actions
### Developer Experience
- Comprehensive docstrings on all admin classes
- Consistent patterns across all apps
- Reusable mixins reduce code duplication
- Clear fieldset descriptions guide admin usage

180
docs/admin/base_classes.md Normal file
View File

@@ -0,0 +1,180 @@
# Admin Base Classes and Mixins
This document describes the base admin classes and mixins available for building standardized Django admin interfaces in ThrillWiki.
## Overview
The admin infrastructure provides reusable components that ensure consistency, optimize performance, and reduce code duplication across all admin interfaces.
## Base Classes
### BaseModelAdmin
The foundational admin class that all model admins should inherit from.
```python
from apps.core.admin import BaseModelAdmin
class MyModelAdmin(BaseModelAdmin):
list_display = ['name', 'status', 'created_at']
```
**Features:**
- Consistent pagination (50 items per page)
- Optimized result count behavior
- Standard empty value display
- Save buttons at top of forms
- Filter preservation after saves
## Mixins
### QueryOptimizationMixin
Provides automatic query optimization to prevent N+1 queries.
```python
from apps.core.admin import QueryOptimizationMixin, BaseModelAdmin
class RideAdmin(QueryOptimizationMixin, BaseModelAdmin):
list_display = ['name', 'park', 'manufacturer']
list_select_related = ['park', 'manufacturer']
list_prefetch_related = ['reviews', 'photos']
```
**Attributes:**
- `list_select_related`: List of ForeignKey fields to select
- `list_prefetch_related`: List of related fields to prefetch
### ReadOnlyAdminMixin
Disables add, change, and delete permissions for auto-generated data.
```python
from apps.core.admin import ReadOnlyAdminMixin, BaseModelAdmin
class RankingAdmin(ReadOnlyAdminMixin, BaseModelAdmin):
list_display = ['ride', 'rank', 'calculated_at']
```
**Use cases:**
- Rankings and leaderboards
- Audit logs and history
- Calculated statistics
- State transition logs
### TimestampFieldsMixin
Provides standard handling for `created_at` and `updated_at` fields.
```python
from apps.core.admin import TimestampFieldsMixin, BaseModelAdmin
class MyModelAdmin(TimestampFieldsMixin, BaseModelAdmin):
fieldsets = [
('Basic Info', {'fields': ['name', 'description']}),
] + TimestampFieldsMixin.get_timestamp_fieldset()
```
**Features:**
- Automatically adds timestamp fields to readonly_fields
- Provides a collapsible fieldset for metadata
### SlugFieldMixin
Configures automatic slug population from name field.
```python
from apps.core.admin import SlugFieldMixin, BaseModelAdmin
class ParkAdmin(SlugFieldMixin, BaseModelAdmin):
slug_source_field = 'name' # Optional, defaults to 'name'
```
### ExportActionMixin
Adds CSV and JSON export functionality.
```python
from apps.core.admin import ExportActionMixin, BaseModelAdmin
class ParkAdmin(ExportActionMixin, BaseModelAdmin):
export_fields = ['id', 'name', 'status', 'created_at']
export_filename_prefix = 'parks'
```
**Actions added:**
- Export selected to CSV
- Export selected to JSON
### BulkStatusChangeMixin
Provides bulk status change actions.
```python
from apps.core.admin import BulkStatusChangeMixin, BaseModelAdmin
class RideAdmin(BulkStatusChangeMixin, BaseModelAdmin):
status_field = 'status'
status_choices = [
('active', 'Activate'),
('inactive', 'Deactivate'),
]
```
### ModerationMixin
Adds moderation actions for user-generated content.
```python
from apps.core.admin import ModerationMixin, BaseModelAdmin
class ReviewAdmin(ModerationMixin, BaseModelAdmin):
moderation_status_field = 'moderation_status'
moderated_by_field = 'moderated_by'
moderated_at_field = 'moderated_at'
```
**Actions added:**
- Approve selected items
- Reject selected items
## Combining Mixins
Mixins can be combined to create feature-rich admin classes:
```python
from apps.core.admin import (
BaseModelAdmin,
QueryOptimizationMixin,
ExportActionMixin,
TimestampFieldsMixin,
SlugFieldMixin,
)
class ParkAdmin(
QueryOptimizationMixin,
ExportActionMixin,
TimestampFieldsMixin,
SlugFieldMixin,
BaseModelAdmin
):
list_display = ['name', 'operator', 'status', 'created_at']
list_select_related = ['operator', 'location']
list_prefetch_related = ['areas', 'rides']
export_fields = ['id', 'name', 'slug', 'status']
```
## Best Practices
1. **Always use BaseModelAdmin** as the final parent class
2. **List mixins before BaseModelAdmin** in inheritance order
3. **Define list_select_related** for all ForeignKey fields in list_display
4. **Use prefetch_related** for reverse relations and M2M fields
5. **Test query counts** using Django Debug Toolbar
6. **Add export_fields** explicitly for control over exported data
## Performance Targets
- List views: < 10 queries
- Change views: < 15 queries
- Page load time: < 500ms for 100 records

View File

@@ -0,0 +1,267 @@
# Admin Best Practices
This guide outlines best practices for developing and maintaining Django admin interfaces in ThrillWiki.
## Query Optimization
### Always Use select_related for ForeignKeys
```python
class RideAdmin(BaseModelAdmin):
list_display = ['name', 'park', 'manufacturer']
list_select_related = ['park', 'manufacturer'] # Prevents N+1 queries
```
### Use prefetch_related for Reverse Relations
```python
class ParkAdmin(BaseModelAdmin):
list_display = ['name', 'ride_count']
list_prefetch_related = ['rides'] # For efficient count queries
```
### Use Annotations for Calculated Fields
```python
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.annotate(
_ride_count=Count('rides', distinct=True),
_avg_rating=Avg('reviews__rating'),
)
```
## Display Methods
### Always Use format_html for Safety
```python
@admin.display(description="Status")
def status_badge(self, obj):
return format_html(
'<span style="color: {};">{}</span>',
'green' if obj.is_active else 'red',
obj.get_status_display(),
)
```
### Handle Missing Data Gracefully
```python
@admin.display(description="Park")
def park_link(self, obj):
if obj.park:
url = reverse("admin:parks_park_change", args=[obj.park.pk])
return format_html('<a href="{}">{}</a>', url, obj.park.name)
return "-" # Or format_html for styled "N/A"
```
## Fieldsets Organization
### Standard Fieldset Structure
1. Basic Information - Name, slug, description
2. Relationships - ForeignKeys, ManyToMany
3. Status & Dates - Status fields, timestamps
4. Specifications - Technical details (collapsed)
5. Media - Images, photos (collapsed)
6. Metadata - created_at, updated_at (collapsed)
### Include Descriptions
```python
fieldsets = (
(
"Basic Information",
{
"fields": ("name", "slug", "description"),
"description": "Core identification for this record.",
},
),
)
```
## Custom Actions
### Use Proper Action Decorator
```python
@admin.action(description="Approve selected items")
def bulk_approve(self, request, queryset):
count = queryset.update(status='approved')
self.message_user(request, f"Approved {count} items.")
```
### Handle Errors Gracefully
```python
@admin.action(description="Process selected items")
def process_items(self, request, queryset):
processed = 0
errors = 0
for item in queryset:
try:
item.process()
processed += 1
except Exception as e:
errors += 1
self.message_user(
request,
f"Error processing {item}: {e}",
level=messages.ERROR,
)
if processed:
self.message_user(request, f"Processed {processed} items.")
```
### Protect Against Dangerous Operations
```python
@admin.action(description="Ban selected users")
def ban_users(self, request, queryset):
# Prevent banning self
queryset = queryset.exclude(pk=request.user.pk)
# Prevent banning superusers
queryset = queryset.exclude(is_superuser=True)
updated = queryset.update(is_banned=True)
self.message_user(request, f"Banned {updated} users.")
```
## Permissions
### Read-Only for Auto-Generated Data
```python
class RankingAdmin(ReadOnlyAdminMixin, BaseModelAdmin):
"""Rankings are calculated automatically - no manual editing."""
pass
```
### Field-Level Permissions
```python
def get_readonly_fields(self, request, obj=None):
readonly = list(super().get_readonly_fields(request, obj))
if not request.user.is_superuser:
readonly.extend(['sensitive_field', 'admin_notes'])
return readonly
```
### Object-Level Permissions
```python
def has_change_permission(self, request, obj=None):
if obj is None:
return super().has_change_permission(request)
# Only allow editing own content or moderator access
if obj.user == request.user:
return True
return request.user.has_perm('app.moderate_content')
```
## Performance Tips
### Disable Full Result Count
```python
class MyAdmin(BaseModelAdmin):
show_full_result_count = False # Faster for large datasets
```
### Use Pagination
```python
class MyAdmin(BaseModelAdmin):
list_per_page = 50 # Reasonable page size
```
### Limit Inline Items
```python
class ItemInline(admin.TabularInline):
model = Item
extra = 1 # Only show 1 empty form
max_num = 20 # Limit total items
```
## Testing
### Test Query Counts
```python
def test_list_view_query_count(self):
with self.assertNumQueries(10): # Set target
response = self.client.get('/admin/app/model/')
```
### Test Permissions
```python
def test_readonly_permissions(self):
request = self.factory.get('/admin/')
request.user = User(is_superuser=False)
assert self.admin.has_add_permission(request) is False
assert self.admin.has_change_permission(request) is False
```
### Test Custom Actions
```python
def test_bulk_approve_action(self):
items = [create_item(status='pending') for _ in range(5)]
queryset = Item.objects.filter(pk__in=[i.pk for i in items])
self.admin.bulk_approve(self.request, queryset)
for item in items:
item.refresh_from_db()
assert item.status == 'approved'
```
## Common Pitfalls
### Avoid N+1 Queries
```python
# BAD: Creates N+1 queries
@admin.display()
def related_count(self, obj):
return obj.related_set.count()
# GOOD: Use annotation
def get_queryset(self, request):
return super().get_queryset(request).annotate(
_related_count=Count('related_set')
)
@admin.display()
def related_count(self, obj):
return obj._related_count
```
### Don't Forget Error Handling
```python
# BAD: May raise AttributeError
@admin.display()
def user_name(self, obj):
return obj.user.username
# GOOD: Handle missing relations
@admin.display()
def user_name(self, obj):
return obj.user.username if obj.user else "-"
```
### Use Autocomplete for Large Relations
```python
# BAD: Loads all options
class MyAdmin(BaseModelAdmin):
raw_id_fields = ['park'] # Works but poor UX
# GOOD: Search with autocomplete
class MyAdmin(BaseModelAdmin):
autocomplete_fields = ['park']
```

128
docs/admin/overview.md Normal file
View File

@@ -0,0 +1,128 @@
# Admin Interface Overview
This document provides an overview of the Django admin interface architecture for ThrillWiki.
## Architecture
The admin interface is built on a layered architecture:
```
┌─────────────────────────────────────────────────────┐
│ App-Specific Admins │
│ (ParkAdmin, RideAdmin, UserAdmin, etc.) │
├─────────────────────────────────────────────────────┤
│ Mixins │
│ (QueryOptimizationMixin, ExportActionMixin, etc.) │
├─────────────────────────────────────────────────────┤
│ BaseModelAdmin │
│ (Standard settings, base functionality) │
├─────────────────────────────────────────────────────┤
│ django.contrib.admin │
└─────────────────────────────────────────────────────┘
```
## Key Components
### Base Classes (`apps/core/admin/`)
- **BaseModelAdmin**: Standard settings (pagination, display, ordering)
- **Mixins**: Reusable functionality (query optimization, export, permissions)
### Admin Sites
- **Default Admin Site**: Full admin access at `/admin/`
- **Moderation Admin Site**: Dedicated moderation interface at `/moderation/`
## Performance Targets
| View Type | Query Target | Load Time |
|-----------|--------------|-----------|
| List View | < 15 queries | < 500ms |
| Change View | < 20 queries | < 500ms |
| Bulk Actions | < 2 seconds | per 100 records |
## Quick Reference
### Adding a New Admin
```python
from apps.core.admin import (
BaseModelAdmin,
QueryOptimizationMixin,
ExportActionMixin,
)
class MyModelAdmin(
QueryOptimizationMixin,
ExportActionMixin,
BaseModelAdmin,
):
list_display = ['name', 'related_obj', 'status', 'created_at']
list_select_related = ['related_obj']
list_prefetch_related = ['many_to_many_field']
export_fields = ['id', 'name', 'status']
export_filename_prefix = 'my_model'
```
### Read-Only Admin for Auto-Generated Data
```python
from apps.core.admin import ReadOnlyAdminMixin, BaseModelAdmin
class RankingAdmin(ReadOnlyAdminMixin, BaseModelAdmin):
list_display = ['ride', 'rank', 'calculated_at']
```
### Admin with Moderation Actions
```python
from apps.core.admin import ModerationMixin, BaseModelAdmin
class ReviewAdmin(ModerationMixin, BaseModelAdmin):
moderation_status_field = 'status'
```
## Files Structure
```
backend/
├── apps/
│ ├── core/
│ │ ├── admin/
│ │ │ ├── __init__.py
│ │ │ ├── base.py
│ │ │ └── mixins.py
│ │ └── admin.py
│ ├── parks/
│ │ └── admin.py
│ ├── rides/
│ │ └── admin.py
│ ├── accounts/
│ │ └── admin.py
│ └── moderation/
│ └── admin.py
└── shared/
└── media/
└── admin.py
docs/admin/
├── overview.md
├── base_classes.md
└── CHANGELOG.md
```
## Best Practices
1. **Always use BaseModelAdmin** as the final parent class
2. **List mixins before BaseModelAdmin** in inheritance order
3. **Define list_select_related** for all ForeignKeys in list_display
4. **Use prefetch_related** for reverse relations and M2M fields
5. **Add export_fields** explicitly for control over exported data
6. **Include descriptive help text** in fieldset descriptions
7. **Test query counts** using Django Debug Toolbar
## Related Documentation
- [Base Classes and Mixins](base_classes.md)
- [Changelog](CHANGELOG.md)

View File

@@ -0,0 +1,57 @@
# Architecture Decision Records (ADRs)
This directory contains Architecture Decision Records (ADRs) documenting the key architectural decisions made in the ThrillWiki project.
## What is an ADR?
An Architecture Decision Record is a document that captures an important architectural decision made along with its context and consequences.
## Related Documentation
- [Future Work](../FUTURE_WORK.md) - Deferred features and implementation plans
## ADR Index
| ADR | Title | Status | Date |
|-----|-------|--------|------|
| [ADR-001](./adr-001-django-htmx-architecture.md) | Django + HTMX Architecture | Accepted | 2025-01 |
| [ADR-002](./adr-002-hybrid-api-design.md) | Hybrid API Design Pattern | Accepted | 2025-01 |
| [ADR-003](./adr-003-state-machine-pattern.md) | State Machine Pattern | Accepted | 2025-01 |
| [ADR-004](./adr-004-caching-strategy.md) | Caching Strategy | Accepted | 2025-01 |
| [ADR-005](./adr-005-authentication-approach.md) | Authentication Approach | Accepted | 2025-01 |
| [ADR-006](./adr-006-media-handling-cloudflare.md) | Media Handling with Cloudflare | Accepted | 2025-01 |
## ADR Template
New ADRs should follow this template:
```markdown
# ADR-XXX: Title
## Status
[Proposed | Accepted | Deprecated | Superseded]
## Context
What is the issue that we're seeing that is motivating this decision or change?
## Decision
What is the change that we're proposing and/or doing?
## Consequences
What becomes easier or more difficult to do because of this change?
## Alternatives Considered
What other options were considered and why were they rejected?
```
## Decision Status
- **Proposed**: The decision is under discussion
- **Accepted**: The decision has been accepted and implemented
- **Deprecated**: The decision is no longer relevant
- **Superseded**: The decision has been replaced by a newer ADR

View File

@@ -0,0 +1,108 @@
# ADR-001: Django + HTMX Architecture
## Status
Accepted
## Context
ThrillWiki needed to choose a frontend architecture for building an interactive web application. The options considered were:
1. **Single Page Application (SPA)** with React/Vue.js and a separate API backend
2. **Django monolith with HTMX** for server-driven interactivity
3. **Traditional Multi-Page Application (MPA)** with full page reloads
The team needed an architecture that would:
- Minimize development complexity
- Provide good SEO out of the box
- Enable fast initial page loads
- Support dynamic interactions without full page reloads
- Be maintainable by a small team
## Decision
We chose to build ThrillWiki as a **Django monolith with HTMX** for dynamic interactivity, supplemented by minimal Alpine.js for client-side UI state.
### Key Components
1. **Django Templates**: Server-side rendering for all pages
2. **HTMX**: Dynamic partial updates without full page reloads
3. **Alpine.js**: Minimal client-side state (form validation, UI toggles)
4. **Tailwind CSS**: Utility-first styling
5. **REST API**: Available for programmatic access (mobile apps, integrations)
### Architecture Pattern
```
Browser Request
┌─────────────┐
│ Django │
│ Views │
└─────────────┘
┌─────────────────────────────────┐
│ HTMX Request? │
│ ├── Yes: Render partial │
│ └── No: Render full page │
└─────────────────────────────────┘
┌─────────────┐
│ Response │
│ (HTML) │
└─────────────┘
```
## Consequences
### Benefits
1. **Reduced Complexity**: Single codebase, no separate frontend build process
2. **SEO-Friendly**: Server-rendered HTML by default
3. **Fast Initial Load**: No JavaScript bundle to download before content appears
4. **Progressive Enhancement**: Works without JavaScript, enhanced with HTMX
5. **Easier Debugging**: Server logs show all application state
6. **Simpler Deployment**: Single Django container
7. **Django Ecosystem**: Full access to Django's batteries-included features
### Trade-offs
1. **Learning Curve**: Developers need to learn HTMX patterns
2. **Limited Offline Support**: No client-side data caching
3. **Network Dependency**: Every interaction requires a server round-trip
4. **Complex Client State**: Harder to manage complex client-side state (mitigated by Alpine.js)
### HTMX Patterns Adopted
1. **Partial Templates**: Views return partial HTML for HTMX requests
2. **HX-Trigger Events**: Cross-component communication via custom events
3. **Loading Indicators**: Skeleton loaders shown during requests
4. **Field Validation**: Real-time form validation via HTMX
## Alternatives Considered
### React/Vue.js SPA
**Rejected because:**
- Increased development complexity with separate codebases
- SEO requires server-side rendering setup (Next.js, Nuxt.js)
- Larger bundle sizes for initial load
- More complex deployment with API + frontend containers
- Overkill for this application's interactivity needs
### Traditional MPA
**Rejected because:**
- Poor user experience with full page reloads
- Higher server load for every interaction
- Slower perceived performance
## References
- [HTMX Documentation](https://htmx.org/docs/)
- [Django + HTMX Guide](https://htmx.org/essays/hypermedia-applications/)
- [Alpine.js Documentation](https://alpinejs.dev/)
- [ThrillWiki HTMX Patterns](../htmx-patterns.md)

View File

@@ -0,0 +1,160 @@
# ADR-002: Hybrid API Design Pattern
## Status
Accepted
## Context
ThrillWiki serves two types of clients:
1. **Web browsers**: Need HTML responses for rendering pages
2. **API clients**: Need JSON responses for mobile apps and integrations
We needed to decide how to handle these different client types efficiently without duplicating business logic.
## Decision
We implemented a **Hybrid API Design Pattern** where views can serve both HTML and JSON responses based on content negotiation.
### Implementation Strategy
1. **API-first approach**: All business logic is implemented in serializers and services
2. **Content negotiation**: Views check request headers to determine response format
3. **Dual endpoints**: Some resources are available at both web and API URLs
4. **Shared serializers**: Same serializers used for both API responses and template context
### URL Structure
```
# Web URLs (HTML responses)
/parks/ # Park list page
/parks/cedar-point/ # Park detail page
# API URLs (JSON responses)
/api/v1/parks/ # Park list (JSON)
/api/v1/parks/cedar-point/ # Park detail (JSON)
```
### View Implementation
```python
class HybridViewMixin:
"""
Mixin that enables views to serve both HTML and JSON responses.
"""
serializer_class = None
def get_response_format(self, request):
"""Determine response format from Accept header or query param."""
if request.htmx:
return 'html'
if 'application/json' in request.headers.get('Accept', ''):
return 'json'
if request.GET.get('format') == 'json':
return 'json'
return 'html'
def render_response(self, request, context, **kwargs):
"""Render appropriate response based on format."""
format = self.get_response_format(request)
if format == 'json':
serializer = self.serializer_class(context['object'])
return JsonResponse(serializer.data)
return super().render_to_response(context, **kwargs)
```
### Serializer Patterns
```python
# API serializers use camelCase for JSON responses
class ParkSerializer(serializers.ModelSerializer):
operatorName = serializers.CharField(source='operator.name')
rideCount = serializers.IntegerField(source='ride_count')
class Meta:
model = Park
fields = ['id', 'name', 'slug', 'operatorName', 'rideCount']
```
## Consequences
### Benefits
1. **Code Reuse**: Single set of business logic serves both web and API
2. **Consistency**: API and web views always return consistent data
3. **Flexibility**: Easy to add new response formats
4. **Progressive Enhancement**: API available without duplicating work
5. **Mobile-Ready**: API ready for mobile app development
### Trade-offs
1. **Complexity**: Views need to handle multiple response formats
2. **Testing**: Need to test both HTML and JSON responses
3. **Documentation**: Must document both web and API interfaces
### Response Format Decision
| Request Type | Response Format |
|-------------|-----------------|
| HTMX request | HTML partial |
| Browser (Accept: text/html) | Full HTML page |
| API (Accept: application/json) | JSON |
| Query param (?format=json) | JSON |
## Alternatives Considered
### Separate API and Web Views
**Rejected because:**
- Duplicate business logic
- Risk of divergence between API and web
- More code to maintain
### API-Only with JavaScript Frontend
**Rejected because:**
- Conflicts with ADR-001 (Django + HTMX architecture)
- Poor SEO without SSR
- Increased complexity
### GraphQL
**Rejected because:**
- Overkill for current requirements
- Steeper learning curve
- Less mature Django ecosystem support
## Implementation Details
### Hybrid Loader Services
For complex data loading scenarios, we use hybrid loader services:
```python
# backend/apps/parks/services/hybrid_loader.py
class ParkHybridLoader:
"""
Loads park data optimized for both API and web contexts.
"""
def load_park_detail(self, slug, context='web'):
park = Park.objects.optimized_for_detail().get(slug=slug)
if context == 'api':
return ParkSerializer(park).data
return {'park': park}
```
### API Versioning
API endpoints are versioned to allow breaking changes:
```
/api/v1/parks/ # Current version
/api/v2/parks/ # Future version (if needed)
```
## References
- [Django REST Framework](https://www.django-rest-framework.org/)
- [Content Negotiation](https://www.django-rest-framework.org/api-guide/content-negotiation/)
- [ThrillWiki API Documentation](../THRILLWIKI_API_DOCUMENTATION.md)

View File

@@ -0,0 +1,186 @@
# ADR-003: State Machine Pattern
## Status
Accepted
## Context
Parks and rides in ThrillWiki go through various operational states:
- Parks: Operating, Closed Temporarily, Closed Permanently, Under Construction
- Rides: Operating, Closed, Under Construction, Removed, Relocated
Managing these state transitions requires:
- Valid state transition enforcement
- Audit trail of state changes
- Business logic tied to state changes (notifications, cache invalidation)
## Decision
We implemented a **Finite State Machine (FSM) pattern** for managing entity states, using django-fsm with custom enhancements.
### State Model
```python
from django_fsm import FSMField, transition
class Park(models.Model):
status = FSMField(default='OPERATING')
@transition(
field=status,
source=['OPERATING', 'CLOSED_TEMP'],
target='CLOSED_PERM'
)
def close_permanently(self, reason=None):
"""Close the park permanently."""
self.closure_reason = reason
self.closure_date = timezone.now()
@transition(
field=status,
source='CLOSED_TEMP',
target='OPERATING'
)
def reopen(self):
"""Reopen a temporarily closed park."""
self.closure_reason = None
```
### State Diagram
```
Park States:
┌──────────────┐
│ PLANNED │
└──────┬───────┘
┌──────────────────────────────────────┐
│ UNDER_CONSTRUCTION │
└──────────────────┬───────────────────┘
┌──────────────────────────────────────┐
│ OPERATING │◄────┐
└──────────────────┬───────────────────┘ │
│ │
┌───────────┼───────────┐ │
│ │ │ │
▼ ▼ ▼ │
┌────────────┐ ┌────────┐ ┌────────────┐ │
│CLOSED_TEMP │ │SEASONAL│ │CLOSED_PERM │ │
└─────┬──────┘ └────────┘ └────────────┘ │
│ │
└───────────────────────────────────────┘
```
### Transition Validation
```python
class ParkStateTransition(models.Model):
"""Audit log for park state transitions."""
park = models.ForeignKey(Park, on_delete=models.CASCADE)
from_state = models.CharField(max_length=20)
to_state = models.CharField(max_length=20)
transition_date = models.DateTimeField(auto_now_add=True)
transitioned_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
reason = models.TextField(blank=True)
```
## Consequences
### Benefits
1. **Valid Transitions Only**: Invalid state changes are rejected at the model level
2. **Audit Trail**: All transitions are logged with timestamps and users
3. **Business Logic Encapsulation**: Transition methods contain related logic
4. **Testability**: State machines are easy to unit test
5. **Documentation**: State diagrams document valid workflows
### Trade-offs
1. **Learning Curve**: Developers need to understand FSM concepts
2. **Migration Complexity**: Adding new states requires careful migration
3. **Flexibility**: Rigid state transitions can be limiting for edge cases
### State Change Hooks
```python
from django.db.models.signals import pre_save
from django.dispatch import receiver
@receiver(pre_save, sender=Park)
def park_state_change(sender, instance, **kwargs):
if instance.pk:
old_instance = Park.objects.get(pk=instance.pk)
if old_instance.status != instance.status:
# Log transition
ParkStateTransition.objects.create(
park=instance,
from_state=old_instance.status,
to_state=instance.status,
)
# Invalidate caches
invalidate_park_caches(instance)
# Send notifications
notify_state_change(instance, old_instance.status)
```
## Alternatives Considered
### Simple Status Field
**Rejected because:**
- No validation of state transitions
- Business logic scattered across codebase
- No built-in audit trail
### Event Sourcing
**Rejected because:**
- Overkill for current requirements
- Significant complexity increase
- Steeper learning curve
### Workflow Engine
**Rejected because:**
- External dependency overhead
- More complex than needed
- FSM sufficient for current use cases
## Implementation Details
### Ride Status States
```python
class RideStatus(models.TextChoices):
OPERATING = 'OPERATING', 'Operating'
CLOSED_TEMP = 'CLOSED_TEMP', 'Temporarily Closed'
CLOSED_PERM = 'CLOSED_PERM', 'Permanently Closed'
UNDER_CONSTRUCTION = 'UNDER_CONSTRUCTION', 'Under Construction'
REMOVED = 'REMOVED', 'Removed'
RELOCATED = 'RELOCATED', 'Relocated'
```
### Testing State Transitions
```python
class ParkStateTransitionTest(TestCase):
def test_cannot_reopen_permanently_closed_park(self):
park = ParkFactory(status='CLOSED_PERM')
with self.assertRaises(TransitionNotAllowed):
park.reopen()
def test_can_close_operating_park_temporarily(self):
park = ParkFactory(status='OPERATING')
park.close_temporarily(reason='Maintenance')
self.assertEqual(park.status, 'CLOSED_TEMP')
```
## References
- [django-fsm Documentation](https://github.com/viewflow/django-fsm)
- [State Machine Diagrams](../state_machines/diagrams.md)
- [Finite State Machine Wikipedia](https://en.wikipedia.org/wiki/Finite-state_machine)

View File

@@ -0,0 +1,222 @@
# ADR-004: Caching Strategy
## Status
Accepted
## Context
ThrillWiki serves data that is:
- Read-heavy (browsing parks and rides)
- Moderately updated (user contributions, moderation)
- Geographically queried (map views, location searches)
We needed a caching strategy that would:
- Reduce database load for common queries
- Provide fast response times for users
- Handle cache invalidation correctly
- Support different caching needs (sessions, API, geographic)
## Decision
We implemented a **Multi-Layer Caching Strategy** using Redis with multiple cache backends for different purposes.
### Cache Architecture
```
┌─────────────────────────────────────────────────────┐
│ Application │
└─────────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Default Cache │ │ Session Cache │ │ API Cache │
│ (General data) │ │ (User sessions)│ │ (API responses)│
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
└────────────────┼────────────────┘
┌─────────────────┐
│ Redis │
│ (with pools) │
└─────────────────┘
```
### Cache Configuration
```python
# backend/config/django/production.py
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": redis_url,
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
"PARSER_CLASS": "redis.connection.HiredisParser",
"COMPRESSOR": "django_redis.compressors.zlib.ZlibCompressor",
"CONNECTION_POOL_CLASS_KWARGS": {
"max_connections": 100,
"timeout": 20,
},
},
"KEY_PREFIX": "thrillwiki",
},
"sessions": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": redis_sessions_url,
"KEY_PREFIX": "sessions",
},
"api": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": redis_api_url,
"OPTIONS": {
"COMPRESSOR": "django_redis.compressors.zlib.ZlibCompressor",
},
"KEY_PREFIX": "api",
},
}
```
### Caching Layers
| Layer | Purpose | TTL | Invalidation |
|-------|---------|-----|--------------|
| QuerySet | Expensive database queries | 1 hour | On model save |
| API Response | Serialized API responses | 30 min | On data change |
| Geographic | Map data and location queries | 30 min | On location update |
| Template Fragment | Rendered template parts | 15 min | On context change |
## Consequences
### Benefits
1. **Reduced Database Load**: Common queries served from cache
2. **Fast Response Times**: Sub-millisecond cache hits
3. **Scalability**: Cache can be distributed across Redis cluster
4. **Flexibility**: Different TTLs for different data types
5. **Compression**: Reduced memory usage with zlib compression
### Trade-offs
1. **Cache Invalidation**: Must carefully invalidate on data changes
2. **Memory Usage**: Redis memory must be monitored
3. **Consistency**: Potential for stale data during TTL window
4. **Complexity**: Multiple cache backends to manage
### Cache Key Naming Convention
```
{prefix}:{entity_type}:{identifier}:{context}
Examples:
thrillwiki:park:123:detail
thrillwiki:park:123:rides
api:parks:list:page1:filter_operating
geo:bounds:40.7:-74.0:41.0:-73.5:z10
```
### Cache Invalidation Patterns
```python
# Model signal for cache invalidation
@receiver(post_save, sender=Park)
def invalidate_park_cache(sender, instance, **kwargs):
cache_service = EnhancedCacheService()
# Invalidate specific park cache
cache_service.invalidate_model_cache('park', instance.id)
# Invalidate list caches
cache_service.invalidate_pattern('api:parks:list:*')
# Invalidate geographic caches if location changed
if instance.location_changed:
cache_service.invalidate_pattern('geo:*')
```
## Alternatives Considered
### Database-Only (No Caching)
**Rejected because:**
- High database load for read-heavy traffic
- Slower response times
- Database as bottleneck for scaling
### Memcached
**Rejected because:**
- Less feature-rich than Redis
- No data persistence
- No built-in data structures
### Application-Level Caching Only
**Rejected because:**
- Not shared across application instances
- Memory per-instance overhead
- Cache cold on restart
## Implementation Details
### EnhancedCacheService
```python
# backend/apps/core/services/enhanced_cache_service.py
class EnhancedCacheService:
"""Comprehensive caching service with multiple cache backends."""
def cache_queryset(self, cache_key, queryset_func, timeout=3600, **kwargs):
"""Cache expensive querysets with logging."""
cached = self.default_cache.get(cache_key)
if cached is None:
result = queryset_func(**kwargs)
self.default_cache.set(cache_key, result, timeout)
return result
return cached
def invalidate_pattern(self, pattern):
"""Invalidate cache keys matching pattern."""
if hasattr(self.default_cache, 'delete_pattern'):
return self.default_cache.delete_pattern(pattern)
```
### Cache Warming
```python
# Proactive cache warming for common queries
class CacheWarmer:
"""Context manager for batch cache warming."""
def warm_popular_parks(self):
parks = Park.objects.operating()[:100]
for park in parks:
self.cache_service.warm_cache(
f'park:{park.id}:detail',
lambda: ParkSerializer(park).data,
timeout=3600
)
```
### Cache Monitoring
```python
class CacheMonitor:
"""Monitor cache performance and statistics."""
def get_cache_stats(self):
redis_client = self.cache_service.default_cache._cache.get_client()
info = redis_client.info()
hits = info.get('keyspace_hits', 0)
misses = info.get('keyspace_misses', 0)
return {
'used_memory': info.get('used_memory_human'),
'hit_rate': hits / (hits + misses) * 100 if hits + misses > 0 else 0,
}
```
## References
- [Django Redis](https://github.com/jazzband/django-redis)
- [Redis Documentation](https://redis.io/documentation)
- [Cache Invalidation Strategies](https://en.wikipedia.org/wiki/Cache_invalidation)

View File

@@ -0,0 +1,216 @@
# ADR-005: Authentication Approach
## Status
Accepted
## Context
ThrillWiki needs to authenticate users for:
- Web browsing (session-based)
- API access (token-based)
- Social login (Google, Discord)
We needed an authentication approach that would:
- Support multiple authentication methods
- Provide secure token handling for API
- Enable social authentication
- Work seamlessly with Django + HTMX architecture
## Decision
We implemented a **Hybrid Authentication System** using django-allauth for social auth and djangorestframework-simplejwt for API tokens.
### Authentication Methods
| Context | Method | Library |
|---------|--------|---------|
| Web browsing | Session-based | Django sessions |
| API access | JWT tokens | djangorestframework-simplejwt |
| Social login | OAuth2 | django-allauth |
| Password reset | Email tokens | Django built-in |
### Architecture
```
┌─────────────────────────────────────────────────────────┐
│ User Request │
└─────────────────────────────────────────────────────────┘
┌───────────────┼───────────────┐
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Web Request │ │ API Request │ │ Social Auth │
│ │ │ │ │ │
│ Session Cookie │ │ Bearer Token │ │ OAuth Flow │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
└───────────────┼───────────────┘
┌─────────────────────┐
│ Django User │
│ (Authenticated) │
└─────────────────────┘
```
### JWT Token Configuration
```python
# backend/config/settings/rest_framework.py
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=15),
'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
'ROTATE_REFRESH_TOKENS': True,
'BLACKLIST_AFTER_ROTATION': True,
'ALGORITHM': 'HS256',
'SIGNING_KEY': SECRET_KEY,
'AUTH_HEADER_TYPES': ('Bearer',),
}
```
### Social Authentication
```python
# backend/config/settings/third_party.py
SOCIALACCOUNT_PROVIDERS = {
'google': {
'SCOPE': ['profile', 'email'],
'AUTH_PARAMS': {'access_type': 'online'},
},
'discord': {
'SCOPE': ['identify', 'email'],
},
}
```
## Consequences
### Benefits
1. **Flexibility**: Multiple auth methods for different use cases
2. **Security**: JWT with short-lived access tokens
3. **User Experience**: Social login reduces friction
4. **Standards-Based**: OAuth2 and JWT are industry standards
5. **Django Integration**: Seamless with Django's user model
### Trade-offs
1. **Complexity**: Multiple auth systems to maintain
2. **Token Management**: Must handle token refresh client-side
3. **Social Provider Dependency**: Reliance on third-party OAuth providers
### Authentication Flow
#### Web Session Authentication
```
1. User visits /login/
2. User submits credentials
3. Django validates credentials
4. Session created, cookie set
5. Subsequent requests include session cookie
```
#### API JWT Authentication
```
1. Client POST /api/v1/auth/login/
{username, password}
2. Server validates, returns tokens
{access: "...", refresh: "..."}
3. Client includes in requests:
Authorization: Bearer <access_token>
4. On 401, client refreshes:
POST /api/v1/auth/token/refresh/
{refresh: "..."}
```
#### Social Authentication
```
1. User clicks "Login with Google"
2. Redirect to Google OAuth
3. User authorizes application
4. Google redirects with auth code
5. Server exchanges code for tokens
6. Server creates/updates user
7. Session created
```
## Alternatives Considered
### Session-Only Authentication
**Rejected because:**
- Not suitable for mobile apps
- Not RESTful for API access
- CSRF complexity for API clients
### JWT-Only Authentication
**Rejected because:**
- More complex for web browsing
- Token storage in browser has security concerns
- Session logout not immediate
### OAuth2 Server (Self-Hosted)
**Rejected because:**
- Significant complexity for current needs
- django-oauth-toolkit overkill
- django-allauth sufficient for social auth
## Implementation Details
### Permission Classes
```python
# API views use JWT or session authentication
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
'rest_framework.authentication.SessionAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticatedOrReadOnly',
],
}
```
### Custom User Model
```python
# backend/apps/accounts/models.py
class User(AbstractUser):
email = models.EmailField(unique=True)
display_name = models.CharField(max_length=50, blank=True)
avatar_url = models.URLField(blank=True)
email_verified = models.BooleanField(default=False)
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['username']
```
### Email Verification
```python
# Required before full access
ACCOUNT_EMAIL_VERIFICATION = 'mandatory'
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_AUTHENTICATION_METHOD = 'email'
```
### Security Measures
1. **Password Hashing**: Django's PBKDF2 with SHA256
2. **Token Blacklisting**: Invalidated refresh tokens stored
3. **Rate Limiting**: Login attempts limited
4. **HTTPS Required**: Tokens only sent over secure connections
## References
- [django-allauth Documentation](https://django-allauth.readthedocs.io/)
- [djangorestframework-simplejwt](https://django-rest-framework-simplejwt.readthedocs.io/)
- [OAuth 2.0 Specification](https://oauth.net/2/)
- [JWT Best Practices](https://auth0.com/blog/jwt-security-best-practices/)

View File

@@ -0,0 +1,226 @@
# ADR-006: Media Handling with Cloudflare Images
## Status
Accepted
## Context
ThrillWiki handles user-uploaded images for:
- Park photos
- Ride photos
- User avatars
We needed a media handling solution that would:
- Handle image uploads efficiently
- Optimize images for different devices
- Provide CDN delivery globally
- Support image transformations (resizing, cropping)
- Minimize storage costs
## Decision
We chose **Cloudflare Images** as our image hosting and transformation service.
### Architecture
```
┌─────────────────────────────────────────────────────────┐
│ User Upload │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Django Backend │
│ ┌─────────────────────────────────────────────────┐ │
│ │ MediaService │ │
│ │ - Validate upload │ │
│ │ - Generate upload URL │ │
│ │ - Store Cloudflare ID │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Cloudflare Images │
│ ┌─────────────────────────────────────────────────┐ │
│ │ - Image storage │ │
│ │ - On-the-fly transformations │ │
│ │ - Global CDN delivery │ │
│ │ - Multiple variants │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
```
### Image Variants
| Variant | Dimensions | Use Case |
|---------|------------|----------|
| `thumbnail` | 150x150 | List views, avatars |
| `card` | 400x300 | Card components |
| `hero` | 1200x600 | Park/ride headers |
| `public` | Original (max 2000px) | Full size view |
### Configuration
```python
# Environment variables
CLOUDFLARE_IMAGES_ACCOUNT_ID = config('CLOUDFLARE_IMAGES_ACCOUNT_ID')
CLOUDFLARE_IMAGES_API_TOKEN = config('CLOUDFLARE_IMAGES_API_TOKEN')
CLOUDFLARE_IMAGES_ACCOUNT_HASH = config('CLOUDFLARE_IMAGES_ACCOUNT_HASH')
```
### Upload Flow
```python
# backend/apps/core/services/media_service.py
class CloudflareImagesService:
"""Service for handling image uploads to Cloudflare Images."""
def get_direct_upload_url(self):
"""Get a one-time upload URL for direct uploads."""
response = self._api_request('POST', 'direct_upload')
return {
'upload_url': response['uploadURL'],
'image_id': response['id'],
}
def get_image_url(self, image_id, variant='public'):
"""Get the delivery URL for an image."""
return f"https://imagedelivery.net/{self.account_hash}/{image_id}/{variant}"
def delete_image(self, image_id):
"""Delete an image from Cloudflare."""
return self._api_request('DELETE', f'images/{image_id}')
```
## Consequences
### Benefits
1. **Global CDN**: Fast image delivery worldwide
2. **Automatic Optimization**: WebP/AVIF conversion as appropriate
3. **On-Demand Transformations**: Resize/crop without pre-generating
4. **Cost Effective**: Pay per image stored, not per transformation
5. **Reduced Server Load**: Direct uploads bypass our servers
6. **Security**: Signed URLs for private content if needed
### Trade-offs
1. **Vendor Lock-in**: Cloudflare-specific API
2. **External Dependency**: Service availability dependency
3. **Cost Scaling**: Costs increase with storage volume
4. **Migration Complexity**: Moving away requires re-uploading
### URL Structure
```
https://imagedelivery.net/{account_hash}/{image_id}/{variant}
Examples:
https://imagedelivery.net/abc123/img-xyz/thumbnail
https://imagedelivery.net/abc123/img-xyz/hero
https://imagedelivery.net/abc123/img-xyz/public
```
## Alternatives Considered
### Django + S3 + CloudFront
**Rejected because:**
- Requires pre-generating all sizes
- More complex infrastructure
- Higher storage costs for variants
- Manual CDN configuration
### Self-Hosted with ImageMagick
**Rejected because:**
- Server CPU overhead for transformations
- No built-in CDN
- Storage management complexity
- Scaling challenges
### Imgix
**Rejected because:**
- Higher costs for our volume
- Already using Cloudflare for other services
- Similar feature set to Cloudflare Images
## Implementation Details
### Photo Model
```python
class Photo(models.Model):
"""Base photo model using Cloudflare Images."""
cloudflare_id = models.CharField(max_length=100, unique=True)
original_filename = models.CharField(max_length=255)
uploaded_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
uploaded_at = models.DateTimeField(auto_now_add=True)
is_approved = models.BooleanField(default=False)
caption = models.TextField(blank=True)
@property
def thumbnail_url(self):
return get_image_url(self.cloudflare_id, 'thumbnail')
@property
def public_url(self):
return get_image_url(self.cloudflare_id, 'public')
```
### Direct Upload API
```python
# backend/apps/api/v1/views/upload.py
class DirectUploadView(APIView):
"""Get a direct upload URL for Cloudflare Images."""
permission_classes = [IsAuthenticated]
def post(self, request):
service = CloudflareImagesService()
upload_data = service.get_direct_upload_url()
return Response({
'upload_url': upload_data['upload_url'],
'image_id': upload_data['image_id'],
})
```
### Cleanup Task
```python
# Celery task for cleaning up orphaned images
@shared_task
def cleanup_orphaned_images():
"""Delete images not referenced by any model."""
cutoff = timezone.now() - timedelta(hours=24)
orphaned = CloudflareImage.objects.filter(
created_at__lt=cutoff,
park_photos__isnull=True,
ride_photos__isnull=True,
user_avatars__isnull=True,
)
for image in orphaned:
service.delete_image(image.cloudflare_id)
image.delete()
```
### Fallback Strategy
```python
def get_image_url(image_id, variant='public', fallback='/static/images/placeholder.jpg'):
"""Get image URL with fallback for missing images."""
if not image_id:
return fallback
return f"https://imagedelivery.net/{ACCOUNT_HASH}/{image_id}/{variant}"
```
## References
- [Cloudflare Images Documentation](https://developers.cloudflare.com/images/)
- [Direct Creator Upload](https://developers.cloudflare.com/images/cloudflare-images/upload-images/direct-creator-upload/)
- [Image Variants](https://developers.cloudflare.com/images/cloudflare-images/transform/flexible-variants/)

View File

@@ -0,0 +1,259 @@
# ThrillWiki Configuration System
This document provides an overview of the ThrillWiki configuration system, including how settings are organized, how to configure different environments, and best practices for managing configuration.
## Architecture Overview
ThrillWiki uses a modular configuration architecture that separates concerns and makes settings easy to manage across different environments.
### Directory Structure
```
backend/config/
├── django/ # Environment-specific settings
│ ├── base.py # Core Django settings (imports from settings/)
│ ├── local.py # Local development settings
│ ├── production.py # Production settings
│ └── test.py # Test settings
├── settings/ # Modular settings modules
│ ├── __init__.py # Package documentation
│ ├── cache.py # Redis caching configuration
│ ├── database.py # Database and GeoDjango settings
│ ├── email.py # Email backend configuration
│ ├── logging.py # Logging formatters, handlers, loggers
│ ├── rest_framework.py # DRF, JWT, CORS, API docs
│ ├── secrets.py # Secret management utilities
│ ├── security.py # Security headers and authentication
│ ├── storage.py # Static/media files and WhiteNoise
│ ├── third_party.py # Allauth, Celery, Cloudflare, etc.
│ └── validation.py # Environment variable validation
└── celery.py # Celery task queue configuration
```
### How Settings Are Loaded
1. **base.py** defines core Django settings and imports all modular settings
2. Environment-specific files (**local.py**, **production.py**, **test.py**) extend base.py
3. The active settings module is determined by `DJANGO_SETTINGS_MODULE`
4. Modular settings use **python-decouple** to read environment variables
### Environment Selection
The settings module is selected in this order:
1. `DJANGO_SETTINGS_MODULE` environment variable (explicit override)
2. Test command detection (`manage.py test``config.django.test`)
3. Production indicators (cloud provider environment variables)
4. `DEBUG=False``config.django.production`
5. Default → `config.django.local`
## Configuration Methods
### Using Environment Variables
Environment variables are the primary configuration method. Create a `.env` file from the template:
```bash
cp .env.example .env
# Edit .env with your values
```
### python-decouple
All settings modules use python-decouple for consistent environment variable handling:
```python
from decouple import config
# String with default
DEBUG = config("DEBUG", default=True, cast=bool)
# Integer with validation
PORT = config("PORT", default=8000, cast=int)
# List from comma-separated string
ALLOWED_HOSTS = config(
"ALLOWED_HOSTS",
default="localhost",
cast=lambda v: [s.strip() for s in v.split(",")]
)
```
## Environment-Specific Configuration
### Local Development (`config.django.local`)
- Debug mode enabled
- Local memory cache (no Redis required)
- Console email backend
- Development middleware (nplusone, debug toolbar)
- Relaxed security settings
### Production (`config.django.production`)
- Debug mode disabled
- Redis caching required
- Strict security settings (HSTS, secure cookies)
- JSON logging for log aggregation
- Sentry integration (optional)
### Test (`config.django.test`)
- In-memory SpatiaLite database
- In-memory cache
- Fast password hashing
- Logging disabled
- Celery tasks run synchronously
## Modular Settings Reference
### database.py
- `DATABASES` - Database connection configuration
- `GDAL_LIBRARY_PATH`, `GEOS_LIBRARY_PATH` - GeoDjango libraries
- `CONN_MAX_AGE` - Connection pooling
- `DATABASE_OPTIONS` - PostgreSQL-specific settings
### cache.py
- `CACHES` - Redis cache backends (default, sessions, api)
- `SESSION_*` - Session storage settings
- `CACHE_MIDDLEWARE_*` - Cache middleware settings
### security.py
- `SECURE_*` - Security header settings
- `SESSION_COOKIE_*`, `CSRF_COOKIE_*` - Cookie security
- `AUTH_PASSWORD_VALIDATORS` - Password validation rules
- `PERMISSIONS_POLICY` - Browser feature permissions
### email.py
- `EMAIL_BACKEND` - Email sending backend
- `FORWARD_EMAIL_*` - ForwardEmail configuration
- SMTP settings for custom email servers
### rest_framework.py
- `REST_FRAMEWORK` - DRF configuration
- `CORS_*` - Cross-origin settings
- `SIMPLE_JWT` - JWT token settings
- `REST_AUTH` - dj-rest-auth settings
- `SPECTACULAR_SETTINGS` - OpenAPI documentation
### third_party.py
- `ACCOUNT_*`, `SOCIALACCOUNT_*` - Allauth settings
- `CLOUDFLARE_IMAGES` - Image CDN configuration
- `ROADTRIP_*` - Road trip service settings
- `HEALTH_CHECK` - Health check thresholds
### storage.py
- `STATIC_*`, `MEDIA_*` - File serving configuration
- `STORAGES` - Django 4.2+ storage backends
- `WHITENOISE_*` - Static file optimization
- `FILE_UPLOAD_*` - Upload security limits
### logging.py
- `LOGGING` - Complete logging configuration
- Formatters: verbose, json, simple
- Handlers: console, file, error_file, performance
- Loggers for Django, application, and third-party
## Secret Management
### Validation
Use the management command to validate configuration:
```bash
# Full validation
python manage.py validate_settings
# Strict mode (warnings become errors)
python manage.py validate_settings --strict
# JSON output
python manage.py validate_settings --json
# Secrets only
python manage.py validate_settings --secrets-only
```
### Required Secrets
These secrets must be set in all environments:
| Secret | Description |
|--------|-------------|
| `SECRET_KEY` | Django cryptographic signing key (50+ chars) |
| `DATABASE_URL` | Database connection URL |
### Secret Rotation
For production environments:
1. Enable rotation checking: `SECRET_ROTATION_ENABLED=True`
2. Track versions: `SECRET_KEY_VERSION=1`
3. Monitor expiry warnings in logs
4. Rotate secrets before expiry
See [Secret Management Guide](./secret-management.md) for detailed procedures.
## Troubleshooting
### Common Issues
**Settings module not found:**
```
ModuleNotFoundError: No module named 'config.django.local'
```
Ensure you're running commands from the `backend/` directory.
**Required environment variable missing:**
```
decouple.UndefinedValueError: SECRET_KEY not found
```
Create a `.env` file or set the environment variable.
**Database connection failed:**
```
django.db.utils.OperationalError: could not connect to server
```
Check `DATABASE_URL` format and database server status.
### Validation Errors
Run validation to identify configuration issues:
```bash
python manage.py validate_settings
```
### Debug Configuration
To debug configuration loading:
```python
# In Django shell
from django.conf import settings
print(settings.DEBUG)
print(settings.DATABASES)
```
## Best Practices
1. **Never commit secrets** - Use `.env` files or secret management services
2. **Use environment variables** - Don't hardcode configuration values
3. **Validate on startup** - Catch configuration errors early
4. **Separate environments** - Use different settings for dev/staging/production
5. **Document custom settings** - Add comments explaining non-obvious configuration
6. **Use appropriate defaults** - Secure defaults for production, convenient for development
## Related Documentation
- [Environment Variables Reference](./environment-variables.md)
- [Secret Management Guide](./secret-management.md)
- [Deployment Guide](./deployment.md)

View File

@@ -0,0 +1,501 @@
# Deployment Guide
This guide covers deploying ThrillWiki to production environments, including configuration, security, and monitoring setup.
## Pre-Deployment Checklist
### Configuration
- [ ] All required environment variables set
- [ ] `DEBUG=False`
- [ ] Strong `SECRET_KEY` (50+ characters, randomly generated)
- [ ] `ALLOWED_HOSTS` properly configured
- [ ] `CSRF_TRUSTED_ORIGINS` set
- [ ] Database URL points to production database
- [ ] Redis URL configured for caching
### Security
- [ ] `SECURE_SSL_REDIRECT=True`
- [ ] `SESSION_COOKIE_SECURE=True`
- [ ] `CSRF_COOKIE_SECURE=True`
- [ ] `SECURE_HSTS_SECONDS` set (31536000 recommended)
- [ ] `SECURE_HSTS_INCLUDE_SUBDOMAINS=True`
- [ ] No debug tools enabled
- [ ] Turnstile keys configured
### Validation
Run the configuration validator:
```bash
python manage.py validate_settings --strict
python manage.py check --deploy
```
## Environment Configuration
### Required Environment Variables
```bash
# Core Django
SECRET_KEY=<generate-secure-key>
DEBUG=False
DJANGO_SETTINGS_MODULE=config.django.production
ALLOWED_HOSTS=thrillwiki.com,www.thrillwiki.com
CSRF_TRUSTED_ORIGINS=https://thrillwiki.com,https://www.thrillwiki.com
# Database
DATABASE_URL=postgis://user:pass@db-host:5432/thrillwiki
# Cache
REDIS_URL=redis://:password@redis-host:6379/1
# Email
EMAIL_BACKEND=django_forwardemail.backends.ForwardEmailBackend
FORWARD_EMAIL_API_KEY=your-api-key
FORWARD_EMAIL_DOMAIN=thrillwiki.com
# Security
TURNSTILE_SITE_KEY=your-site-key
TURNSTILE_SECRET_KEY=your-secret-key
SECURE_SSL_REDIRECT=True
SESSION_COOKIE_SECURE=True
CSRF_COOKIE_SECURE=True
# Cloudflare Images
CLOUDFLARE_IMAGES_ACCOUNT_ID=your-account-id
CLOUDFLARE_IMAGES_API_TOKEN=your-api-token
CLOUDFLARE_IMAGES_ACCOUNT_HASH=your-account-hash
# Road Trip Service
ROADTRIP_USER_AGENT=ThrillWiki/1.0 (https://thrillwiki.com)
# Frontend
FRONTEND_DOMAIN=https://thrillwiki.com
```
### Optional Production Settings
```bash
# Monitoring
SENTRY_DSN=https://xxx@xxx.ingest.sentry.io/xxx
SENTRY_ENVIRONMENT=production
SENTRY_TRACES_SAMPLE_RATE=0.1
# Secret rotation
SECRET_ROTATION_ENABLED=True
SECRET_KEY_VERSION=1
# Performance tuning
DATABASE_CONN_MAX_AGE=600
REDIS_MAX_CONNECTIONS=100
```
## Deployment Platforms
### Docker Deployment
Create a production Dockerfile:
```dockerfile
FROM python:3.11-slim
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
libpq-dev \
libgdal-dev \
libgeos-dev \
&& rm -rf /var/lib/apt/lists/*
# Install Python dependencies
COPY backend/pyproject.toml backend/uv.lock ./
RUN pip install uv && uv sync --frozen
# Copy application
COPY backend/ .
# Collect static files
RUN python manage.py collectstatic --noinput
# Run with Gunicorn
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "4", "thrillwiki.wsgi:application"]
```
Docker Compose for production:
```yaml
version: '3.8'
services:
web:
build: .
environment:
- DJANGO_SETTINGS_MODULE=config.django.production
env_file:
- .env
ports:
- "8000:8000"
depends_on:
- db
- redis
db:
image: postgis/postgis:14-3.3
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=thrillwiki
- POSTGRES_USER=thrillwiki
- POSTGRES_PASSWORD=${DB_PASSWORD}
redis:
image: redis:7-alpine
command: redis-server --requirepass ${REDIS_PASSWORD}
volumes:
- redis_data:/data
celery:
build: .
command: celery -A config.celery worker -l info
env_file:
- .env
depends_on:
- db
- redis
celery-beat:
build: .
command: celery -A config.celery beat -l info
env_file:
- .env
depends_on:
- db
- redis
volumes:
postgres_data:
redis_data:
```
### Heroku Deployment
```bash
# Create app
heroku create thrillwiki
# Add buildpacks
heroku buildpacks:add heroku/python
heroku buildpacks:add https://github.com/heroku/heroku-geo-buildpack.git
# Add add-ons
heroku addons:create heroku-postgresql:standard-0
heroku addons:create heroku-redis:premium-0
# Set environment variables
heroku config:set DJANGO_SETTINGS_MODULE=config.django.production
heroku config:set SECRET_KEY=$(python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())")
heroku config:set DEBUG=False
# ... set other variables
# Deploy
git push heroku main
# Run migrations
heroku run python manage.py migrate
```
### AWS ECS Deployment
Task definition example:
```json
{
"family": "thrillwiki",
"containerDefinitions": [
{
"name": "web",
"image": "your-ecr-repo/thrillwiki:latest",
"portMappings": [
{
"containerPort": 8000,
"protocol": "tcp"
}
],
"environment": [
{
"name": "DJANGO_SETTINGS_MODULE",
"value": "config.django.production"
}
],
"secrets": [
{
"name": "SECRET_KEY",
"valueFrom": "arn:aws:secretsmanager:region:account:secret:thrillwiki/SECRET_KEY"
},
{
"name": "DATABASE_URL",
"valueFrom": "arn:aws:secretsmanager:region:account:secret:thrillwiki/DATABASE_URL"
}
]
}
]
}
```
## Database Setup
### PostgreSQL with PostGIS
```sql
-- Create database
CREATE DATABASE thrillwiki;
-- Enable PostGIS
\c thrillwiki
CREATE EXTENSION postgis;
CREATE EXTENSION postgis_topology;
-- Create user
CREATE USER thrillwiki_app WITH ENCRYPTED PASSWORD 'secure-password';
GRANT ALL PRIVILEGES ON DATABASE thrillwiki TO thrillwiki_app;
```
### Run Migrations
```bash
python manage.py migrate
python manage.py createsuperuser
```
## Static Files
### Collect Static Files
```bash
python manage.py collectstatic --noinput
```
### WhiteNoise Configuration
WhiteNoise is configured for production static file serving. No additional web server configuration needed for static files.
### CDN Integration (Optional)
For CDN, update `STATIC_URL`:
```bash
STATIC_URL=https://cdn.thrillwiki.com/static/
```
## Monitoring Setup
### Sentry Integration
```bash
SENTRY_DSN=https://xxx@xxx.ingest.sentry.io/xxx
SENTRY_ENVIRONMENT=production
SENTRY_TRACES_SAMPLE_RATE=0.1
```
### Health Checks
Health check endpoint: `/health/`
```bash
# Test health check
curl https://thrillwiki.com/health/
```
Configure load balancer health checks to use this endpoint.
### Logging
Production logging outputs JSON to stdout (suitable for log aggregation):
```json
{
"levelname": "INFO",
"asctime": "2024-01-15 10:30:00,000",
"module": "views",
"message": "Request processed"
}
```
## Performance Tuning
### Database Connection Pooling
```bash
DATABASE_CONN_MAX_AGE=600 # 10 minutes
DATABASE_CONNECT_TIMEOUT=10
DATABASE_STATEMENT_TIMEOUT=30000
```
### Redis Connection Pooling
```bash
REDIS_MAX_CONNECTIONS=100
REDIS_CONNECTION_TIMEOUT=20
```
### Gunicorn Configuration
```bash
# gunicorn.conf.py
workers = 4 # (2 * CPU cores) + 1
worker_class = "gevent"
worker_connections = 1000
timeout = 30
keepalive = 5
max_requests = 1000
max_requests_jitter = 50
```
## Celery Workers
### Start Workers
```bash
# Main worker
celery -A config.celery worker -l info
# Beat scheduler
celery -A config.celery beat -l info
# With specific queues
celery -A config.celery worker -l info -Q default,trending,analytics,cache
```
### Supervisor Configuration
```ini
[program:celery]
command=/app/.venv/bin/celery -A config.celery worker -l info
directory=/app
user=www-data
numprocs=1
autostart=true
autorestart=true
stdout_logfile=/var/log/celery/worker.log
stderr_logfile=/var/log/celery/worker-error.log
[program:celery-beat]
command=/app/.venv/bin/celery -A config.celery beat -l info
directory=/app
user=www-data
numprocs=1
autostart=true
autorestart=true
stdout_logfile=/var/log/celery/beat.log
stderr_logfile=/var/log/celery/beat-error.log
```
## SSL/TLS Configuration
### Nginx Configuration
```nginx
server {
listen 443 ssl http2;
server_name thrillwiki.com www.thrillwiki.com;
ssl_certificate /etc/letsencrypt/live/thrillwiki.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/thrillwiki.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
server {
listen 80;
server_name thrillwiki.com www.thrillwiki.com;
return 301 https://$server_name$request_uri;
}
```
## Post-Deployment Verification
### Functional Tests
```bash
# API health
curl -s https://thrillwiki.com/api/health/ | jq
# Static files
curl -I https://thrillwiki.com/static/css/tailwind.css
# API endpoint
curl -s https://thrillwiki.com/api/v1/parks/ | jq
```
### Security Headers
```bash
# Check security headers
curl -I https://thrillwiki.com/ | grep -E "(Strict-Transport|X-Frame|X-Content|Content-Security)"
```
### Performance
```bash
# Response time
time curl -s https://thrillwiki.com/api/v1/parks/ > /dev/null
```
## Rollback Procedure
If deployment fails:
1. **Immediate rollback:**
```bash
# Docker
docker-compose down
docker-compose -f docker-compose.previous.yml up -d
# Heroku
heroku rollback
# Kubernetes
kubectl rollout undo deployment/thrillwiki
```
2. **Database rollback (if needed):**
```bash
python manage.py migrate app_name migration_number
```
3. **Investigate and fix:**
- Check error logs
- Review configuration differences
- Test in staging environment
## Maintenance
### Regular Tasks
- **Daily:** Review error logs, check health endpoints
- **Weekly:** Review performance metrics, check disk usage
- **Monthly:** Rotate secrets, review security updates
- **Quarterly:** Full security audit, dependency updates
### Backup Procedures
```bash
# Database backup
pg_dump thrillwiki > backup_$(date +%Y%m%d).sql
# Media files backup
aws s3 sync /app/media s3://thrillwiki-backups/media/
```

View File

@@ -0,0 +1,338 @@
# Environment Variables Reference
Complete reference for all environment variables used in ThrillWiki.
## Quick Start
1. Copy the example file: `cp .env.example .env`
2. Edit `.env` with your values
3. Run validation: `python manage.py validate_settings`
## Required Variables
These must be set in all environments.
| Variable | Type | Description |
|----------|------|-------------|
| `SECRET_KEY` | string | Django secret key (minimum 50 characters) |
| `DATABASE_URL` | url | Database connection URL |
## Production-Required Variables
These variables **must** be explicitly set for production deployments. The application will not function correctly in production without them.
> **Important:** See [`.env.example`](../../.env.example) for example values and [`PRODUCTION_CHECKLIST.md`](../PRODUCTION_CHECKLIST.md) for deployment verification steps.
| Variable | Type | Format/Example | Description |
|----------|------|----------------|-------------|
| `DEBUG` | bool | `False` | **Must be `False` in production.** Enables detailed error pages and debug toolbar when `True`. |
| `DJANGO_SETTINGS_MODULE` | string | `config.django.production` | **Must use `config.django.production` for production.** Controls which settings module Django loads. |
| `ALLOWED_HOSTS` | list | `thrillwiki.com,www.thrillwiki.com` | **Required.** Comma-separated list of valid hostnames. No default in production - requests will fail without this. |
| `CSRF_TRUSTED_ORIGINS` | list | `https://thrillwiki.com,https://www.thrillwiki.com` | **Required.** Must include `https://` prefix. CSRF validation fails without proper configuration. |
| `REDIS_URL` | url | `redis://[:password@]host:6379/0` | **Required for caching and sessions.** Production uses Redis for caching, session storage, and Celery broker. |
### Production Settings Behavior
When `DJANGO_SETTINGS_MODULE=config.django.production`:
- `DEBUG` is always `False`
- `ALLOWED_HOSTS` has **no default** - must be explicitly set
- `CSRF_TRUSTED_ORIGINS` has **no default** - must be explicitly set
- SSL redirect is enforced (`SECURE_SSL_REDIRECT=True`)
- Secure cookies are enforced (`SESSION_COOKIE_SECURE=True`, `CSRF_COOKIE_SECURE=True`)
- HSTS is enabled with 1-year max-age
- Redis cache is required (application will fail if `REDIS_URL` is not set)
### Validation
Run the following to validate your production configuration:
```bash
DJANGO_SETTINGS_MODULE=config.django.production python manage.py check --deploy
```
## Core Django Settings
| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `DEBUG` | bool | `True` | Enable debug mode |
| `DJANGO_SETTINGS_MODULE` | string | `config.django.local` | Settings module to use |
| `ALLOWED_HOSTS` | list | `localhost,127.0.0.1` | Comma-separated allowed hosts |
| `CSRF_TRUSTED_ORIGINS` | list | `` | Comma-separated trusted origins |
## Database Configuration
| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `DATABASE_URL` | url | (required) | Database connection URL |
| `DATABASE_CONN_MAX_AGE` | int | `600` | Connection pool timeout (seconds) |
| `DATABASE_CONNECT_TIMEOUT` | int | `10` | Connection timeout (seconds) |
| `DATABASE_STATEMENT_TIMEOUT` | int | `30000` | Query timeout (milliseconds) |
| `DATABASE_READ_REPLICA_URL` | url | `` | Optional read replica URL |
### Database URL Formats
```bash
# PostgreSQL with PostGIS
DATABASE_URL=postgis://user:password@host:5432/dbname
# PostgreSQL without PostGIS
DATABASE_URL=postgres://user:password@host:5432/dbname
# SQLite (development only)
DATABASE_URL=sqlite:///path/to/db.sqlite3
```
## GeoDjango Settings
| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `GDAL_LIBRARY_PATH` | path | `/opt/homebrew/lib/libgdal.dylib` | Path to GDAL library |
| `GEOS_LIBRARY_PATH` | path | `/opt/homebrew/lib/libgeos_c.dylib` | Path to GEOS library |
### Platform-Specific Paths
```bash
# macOS (Homebrew)
GDAL_LIBRARY_PATH=/opt/homebrew/lib/libgdal.dylib
GEOS_LIBRARY_PATH=/opt/homebrew/lib/libgeos_c.dylib
# Linux (Ubuntu/Debian)
GDAL_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu/libgdal.so
GEOS_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu/libgeos_c.so
```
## Cache Configuration
| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `REDIS_URL` | url | `redis://127.0.0.1:6379/1` | Redis connection URL |
| `REDIS_SESSIONS_URL` | url | (from REDIS_URL) | Redis URL for sessions |
| `REDIS_API_URL` | url | (from REDIS_URL) | Redis URL for API cache |
| `REDIS_MAX_CONNECTIONS` | int | `100` | Maximum Redis connections |
| `REDIS_CONNECTION_TIMEOUT` | int | `20` | Redis connection timeout |
| `REDIS_IGNORE_EXCEPTIONS` | bool | `True` | Graceful degradation |
| `CACHE_MIDDLEWARE_SECONDS` | int | `300` | Cache middleware timeout |
| `CACHE_MIDDLEWARE_KEY_PREFIX` | string | `thrillwiki` | Cache key prefix |
| `CACHE_KEY_PREFIX` | string | `thrillwiki` | Default cache key prefix |
## Email Configuration
| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `EMAIL_BACKEND` | string | `django_forwardemail.backends.ForwardEmailBackend` | Email backend class |
| `SERVER_EMAIL` | email | `django_webmaster@thrillwiki.com` | Server email address |
| `DEFAULT_FROM_EMAIL` | string | `ThrillWiki <noreply@thrillwiki.com>` | Default from address |
| `EMAIL_SUBJECT_PREFIX` | string | `[ThrillWiki]` | Subject prefix |
| `EMAIL_TIMEOUT` | int | `30` | Email operation timeout |
### ForwardEmail Settings
| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `FORWARD_EMAIL_BASE_URL` | url | `https://api.forwardemail.net` | API base URL |
| `FORWARD_EMAIL_API_KEY` | string | `` | API key |
| `FORWARD_EMAIL_DOMAIN` | string | `` | Sending domain |
### SMTP Settings
| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `EMAIL_HOST` | string | `localhost` | SMTP server |
| `EMAIL_PORT` | int | `587` | SMTP port |
| `EMAIL_USE_TLS` | bool | `True` | Use TLS |
| `EMAIL_USE_SSL` | bool | `False` | Use SSL |
| `EMAIL_HOST_USER` | string | `` | SMTP username |
| `EMAIL_HOST_PASSWORD` | string | `` | SMTP password |
## Security Settings
### SSL/HTTPS
| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `SECURE_SSL_REDIRECT` | bool | `False` | Redirect HTTP to HTTPS |
| `SECURE_PROXY_SSL_HEADER` | tuple | `` | Proxy SSL header |
### HSTS (HTTP Strict Transport Security)
| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `SECURE_HSTS_SECONDS` | int | `31536000` | HSTS max-age |
| `SECURE_HSTS_INCLUDE_SUBDOMAINS` | bool | `True` | Include subdomains |
| `SECURE_HSTS_PRELOAD` | bool | `False` | Allow preload |
| `SECURE_REDIRECT_EXEMPT` | list | `` | URLs exempt from redirect |
### Security Headers
| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `SECURE_BROWSER_XSS_FILTER` | bool | `True` | XSS filter header |
| `SECURE_CONTENT_TYPE_NOSNIFF` | bool | `True` | Nosniff header |
| `X_FRAME_OPTIONS` | string | `DENY` | Frame options |
| `SECURE_REFERRER_POLICY` | string | `strict-origin-when-cross-origin` | Referrer policy |
| `SECURE_CROSS_ORIGIN_OPENER_POLICY` | string | `same-origin` | COOP header |
### Cookie Security
| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `SESSION_COOKIE_SECURE` | bool | `False` | HTTPS-only session cookie |
| `SESSION_COOKIE_HTTPONLY` | bool | `True` | No JS access |
| `SESSION_COOKIE_SAMESITE` | string | `Lax` | SameSite attribute |
| `SESSION_COOKIE_AGE` | int | `3600` | Cookie age (seconds) |
| `CSRF_COOKIE_SECURE` | bool | `False` | HTTPS-only CSRF cookie |
| `CSRF_COOKIE_HTTPONLY` | bool | `True` | No JS access |
| `CSRF_COOKIE_SAMESITE` | string | `Lax` | SameSite attribute |
### Cloudflare Turnstile
| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `TURNSTILE_SITE_KEY` | string | `` | Turnstile site key |
| `TURNSTILE_SECRET_KEY` | string | `` | Turnstile secret key |
| `TURNSTILE_VERIFY_URL` | url | `https://challenges.cloudflare.com/turnstile/v0/siteverify` | Verification URL |
## API Configuration
### CORS
| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `CORS_ALLOWED_ORIGINS` | list | `` | Allowed origins |
| `CORS_ALLOW_ALL_ORIGINS` | bool | `False` | Allow all origins |
### Rate Limiting
| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `API_RATE_LIMIT_PER_MINUTE` | int | `60` | Requests per minute |
| `API_RATE_LIMIT_PER_HOUR` | int | `1000` | Requests per hour |
| `API_RATE_LIMIT_ANON_PER_MINUTE` | int | `60` | Anonymous rate limit |
| `API_RATE_LIMIT_USER_PER_HOUR` | int | `1000` | Authenticated rate limit |
### Pagination
| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `API_PAGE_SIZE` | int | `20` | Default page size |
| `API_MAX_PAGE_SIZE` | int | `100` | Maximum page size |
| `API_VERSION` | string | `1.0.0` | API version string |
## JWT Configuration
| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `JWT_ACCESS_TOKEN_LIFETIME_MINUTES` | int | `15` | Access token lifetime |
| `JWT_REFRESH_TOKEN_LIFETIME_DAYS` | int | `7` | Refresh token lifetime |
| `JWT_ISSUER` | string | `thrillwiki` | Token issuer |
## Cloudflare Images
| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `CLOUDFLARE_IMAGES_ACCOUNT_ID` | string | `` | Account ID |
| `CLOUDFLARE_IMAGES_API_TOKEN` | string | `` | API token |
| `CLOUDFLARE_IMAGES_ACCOUNT_HASH` | string | `` | Account hash |
| `CLOUDFLARE_IMAGES_WEBHOOK_SECRET` | string | `` | Webhook secret |
| `CLOUDFLARE_IMAGES_DEFAULT_VARIANT` | string | `public` | Default variant |
| `CLOUDFLARE_IMAGES_UPLOAD_TIMEOUT` | int | `300` | Upload timeout |
| `CLOUDFLARE_IMAGES_CLEANUP_HOURS` | int | `24` | Cleanup interval |
| `CLOUDFLARE_IMAGES_MAX_FILE_SIZE` | int | `10485760` | Max file size (bytes) |
| `CLOUDFLARE_IMAGES_REQUIRE_SIGNED_URLS` | bool | `False` | Require signed URLs |
## Road Trip Service
| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `ROADTRIP_USER_AGENT` | string | `ThrillWiki/1.0` | OSM API user agent |
| `ROADTRIP_CACHE_TIMEOUT` | int | `86400` | Geocoding cache timeout |
| `ROADTRIP_ROUTE_CACHE_TIMEOUT` | int | `21600` | Route cache timeout |
| `ROADTRIP_MAX_REQUESTS_PER_SECOND` | int | `1` | Rate limit |
| `ROADTRIP_REQUEST_TIMEOUT` | int | `10` | Request timeout |
| `ROADTRIP_MAX_RETRIES` | int | `3` | Max retries |
| `ROADTRIP_BACKOFF_FACTOR` | int | `2` | Retry backoff |
## Logging Configuration
| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `LOG_DIR` | path | `logs` | Log directory |
| `ROOT_LOG_LEVEL` | string | `INFO` | Root log level |
| `DJANGO_LOG_LEVEL` | string | `WARNING` | Django log level |
| `DB_LOG_LEVEL` | string | `WARNING` | Database log level |
| `APP_LOG_LEVEL` | string | `INFO` | Application log level |
| `PERFORMANCE_LOG_LEVEL` | string | `INFO` | Performance log level |
| `QUERY_LOG_LEVEL` | string | `WARNING` | Query log level |
| `NPLUSONE_LOG_LEVEL` | string | `WARNING` | N+1 detection level |
| `REQUEST_LOG_LEVEL` | string | `INFO` | Request log level |
| `CELERY_LOG_LEVEL` | string | `INFO` | Celery log level |
| `CONSOLE_LOG_LEVEL` | string | `INFO` | Console output level |
| `FILE_LOG_LEVEL` | string | `INFO` | File output level |
| `FILE_LOG_FORMATTER` | string | `json` | File log format |
## Monitoring
### Sentry
| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `SENTRY_DSN` | url | `` | Sentry DSN |
| `SENTRY_ENVIRONMENT` | string | `production` | Sentry environment |
| `SENTRY_TRACES_SAMPLE_RATE` | float | `0.1` | Trace sampling rate |
### Health Checks
| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `HEALTH_CHECK_DISK_USAGE_MAX` | int | `90` | Max disk usage % |
| `HEALTH_CHECK_MEMORY_MIN` | int | `100` | Min available memory MB |
## Feature Flags
| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `ENABLE_DEBUG_TOOLBAR` | bool | `True` | Enable debug toolbar |
| `ENABLE_SILK_PROFILER` | bool | `False` | Enable Silk profiler |
| `TEMPLATES_ENABLED` | bool | `True` | Enable Django templates |
| `AUTOCOMPLETE_BLOCK_UNAUTHENTICATED` | bool | `False` | Require auth for autocomplete |
## File Upload Settings
| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `FILE_UPLOAD_MAX_MEMORY_SIZE` | int | `2621440` | Max in-memory upload (bytes) |
| `DATA_UPLOAD_MAX_MEMORY_SIZE` | int | `10485760` | Max request size (bytes) |
| `DATA_UPLOAD_MAX_NUMBER_FIELDS` | int | `1000` | Max form fields |
## WhiteNoise Settings
| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `WHITENOISE_COMPRESSION_QUALITY` | int | `90` | Compression quality |
| `WHITENOISE_MAX_AGE` | int | `31536000` | Cache max-age |
| `WHITENOISE_MANIFEST_STRICT` | bool | `False` | Strict manifest mode |
## Secret Management
| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `SECRET_ROTATION_ENABLED` | bool | `False` | Enable rotation checks |
| `SECRET_KEY_VERSION` | string | `1` | Current secret version |
| `SECRET_EXPIRY_WARNING_DAYS` | int | `30` | Warning threshold |
| `PASSWORD_MIN_LENGTH` | int | `8` | Minimum password length |
## Celery Configuration
| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `CELERY_TASK_ALWAYS_EAGER` | bool | `False` | Run tasks synchronously |
| `CELERY_TASK_EAGER_PROPAGATES` | bool | `False` | Propagate task errors |
## Third-Party Integrations
| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `FRONTEND_DOMAIN` | url | `https://thrillwiki.com` | Frontend URL |
| `LOGIN_REDIRECT_URL` | path | `/` | Post-login redirect |
| `ACCOUNT_LOGOUT_REDIRECT_URL` | path | `/` | Post-logout redirect |
| `ACCOUNT_EMAIL_VERIFICATION` | string | `mandatory` | Email verification mode |

View File

@@ -0,0 +1,330 @@
# Secret Management Guide
This guide covers best practices for managing secrets in ThrillWiki, including rotation procedures, integration with secret management services, and emergency procedures.
## Overview
Secrets are sensitive configuration values that must be protected:
- Database credentials
- API keys and tokens
- Encryption keys
- Service passwords
ThrillWiki provides a flexible secret management system that can work with environment variables (development) or integrate with enterprise secret management services (production).
## Secret Classification
### Critical Secrets (Immediate rotation required if compromised)
| Secret | Impact | Rotation Frequency |
|--------|--------|-------------------|
| `SECRET_KEY` | All cryptographic operations | 90 days recommended |
| `DATABASE_URL` | Database access | On suspected breach |
| `SENTRY_DSN` | Error tracking access | On suspected breach |
### High-Priority Secrets
| Secret | Impact | Rotation Frequency |
|--------|--------|-------------------|
| `CLOUDFLARE_IMAGES_API_TOKEN` | Image CDN access | 180 days |
| `FORWARD_EMAIL_API_KEY` | Email sending | 180 days |
| `TURNSTILE_SECRET_KEY` | CAPTCHA bypass | 365 days |
### Service Secrets
| Secret | Impact | Rotation Frequency |
|--------|--------|-------------------|
| `CLOUDFLARE_IMAGES_WEBHOOK_SECRET` | Webhook verification | 365 days |
| `REDIS_URL` (with password) | Cache access | On suspected breach |
## Development Setup
For development, use a `.env` file:
```bash
# Copy the template
cp .env.example .env
# Generate a secure SECRET_KEY
python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"
# Edit .env with your values
```
### Validation
Validate your configuration:
```bash
python manage.py validate_settings
```
## Production Secret Management
### Option 1: Environment Variables (Simple)
Set secrets as environment variables in your deployment platform:
```bash
# Heroku
heroku config:set SECRET_KEY="your-secret-key"
# Docker
docker run -e SECRET_KEY="your-secret-key" ...
# Kubernetes (from secret)
kubectl create secret generic thrillwiki-secrets \
--from-literal=SECRET_KEY="your-secret-key"
```
### Option 2: AWS Secrets Manager
For AWS deployments, integrate with Secrets Manager:
```python
# In config/settings/secrets.py, implement:
from config.settings.secrets import SecretProvider
class AWSSecretsProvider(SecretProvider):
def __init__(self, secret_name: str):
import boto3
self.client = boto3.client('secretsmanager')
self.secret_name = secret_name
self._cache = {}
def get_secret(self, name: str) -> str:
if not self._cache:
response = self.client.get_secret_value(
SecretId=self.secret_name
)
import json
self._cache = json.loads(response['SecretString'])
return self._cache.get(name)
# Usage in settings
from config.settings.secrets import set_secret_provider
set_secret_provider(AWSSecretsProvider('thrillwiki/production'))
```
### Option 3: HashiCorp Vault
For Vault integration:
```python
class VaultSecretsProvider(SecretProvider):
def __init__(self, vault_addr: str, token: str, path: str):
import hvac
self.client = hvac.Client(url=vault_addr, token=token)
self.path = path
def get_secret(self, name: str) -> str:
response = self.client.secrets.kv.read_secret_version(
path=self.path
)
return response['data']['data'].get(name)
```
## Secret Rotation Procedures
### Rotating SECRET_KEY
**Impact:** All sessions will be invalidated. Users will need to log in again.
1. **Prepare the new key:**
```bash
python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"
```
2. **Update deployment:**
```bash
# Update environment variable
export SECRET_KEY="new-secret-key-here"
export SECRET_KEY_VERSION="2" # Increment version
```
3. **Deploy during low-traffic period**
4. **Clear session cache:**
```bash
python manage.py clearsessions
```
5. **Monitor for issues:**
- Check error rates
- Monitor login success rates
- Watch for authentication errors
### Rotating Database Credentials
**Impact:** Application downtime if not done carefully.
1. **Create new database user:**
```sql
CREATE USER thrillwiki_new WITH PASSWORD 'new-password';
GRANT ALL PRIVILEGES ON DATABASE thrillwiki TO thrillwiki_new;
```
2. **Update application:**
```bash
export DATABASE_URL="postgis://thrillwiki_new:new-password@host:5432/thrillwiki"
```
3. **Deploy and verify**
4. **Remove old user:**
```sql
DROP USER thrillwiki_old;
```
### Rotating API Keys
For third-party API keys:
1. **Generate new key** in the service's dashboard
2. **Update environment variable**
3. **Deploy**
4. **Revoke old key** in the service's dashboard
## Automated Rotation
### Enable Rotation Monitoring
```bash
# In .env
SECRET_ROTATION_ENABLED=True
SECRET_KEY_VERSION=1
SECRET_EXPIRY_WARNING_DAYS=30
```
### Check Rotation Status
```bash
python manage.py validate_settings --secrets-only
```
### Automated Alerts
Configure logging to alert on expiry warnings:
```python
# In your monitoring setup, watch for:
# Logger: security
# Level: WARNING
# Message contains: "Secret expiry"
```
## Emergency Procedures
### Suspected Credential Compromise
1. **Immediate Actions:**
- Rotate the compromised credential immediately
- Check access logs for unauthorized use
- Review related systems for compromise
2. **Investigation:**
- Identify the source of the leak
- Review access patterns
- Document the incident
3. **Remediation:**
- Fix the vulnerability that caused the leak
- Update security procedures
- Conduct a post-mortem
### Complete Secret Rotation (Breach Response)
If a major breach is suspected, rotate all secrets:
```bash
# 1. Generate new SECRET_KEY
python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"
# 2. Create new database credentials
# 3. Regenerate all API keys in respective dashboards
# 4. Update all environment variables
# 5. Deploy
# 6. Clear all caches
python manage.py clear_cache # If you have this command
redis-cli FLUSHALL # Or clear Redis directly
# 7. Invalidate all sessions
python manage.py clearsessions
# 8. Revoke all old credentials
```
## Security Best Practices
### Do's
- ✅ Use different secrets for each environment
- ✅ Rotate secrets regularly (90-180 days)
- ✅ Use secret management services in production
- ✅ Monitor for secret exposure (git-secrets, truffleHog)
- ✅ Log secret access for audit trails
- ✅ Use strong, randomly generated secrets
### Don'ts
- ❌ Never commit secrets to version control
- ❌ Never share secrets via chat or email
- ❌ Never use the same secret across environments
- ❌ Never use default or example secrets in production
- ❌ Never log secret values (even partially)
## Audit and Compliance
### Access Logging
Secret access is logged to the security logger:
```python
# Logged events:
# - Secret validation failures
# - Secret rotation warnings
# - Invalid secret format detections
```
### Regular Audits
Monthly security checklist:
- [ ] Review secret rotation schedule
- [ ] Check for exposed secrets in logs
- [ ] Verify secret strength requirements
- [ ] Audit secret access patterns
- [ ] Test secret rotation procedures
## Troubleshooting
### "Secret validation failed"
```bash
# Check the specific error
python manage.py validate_settings
# Common issues:
# - Secret too short
# - Placeholder value detected
# - Missing required secret
```
### "SECRET_KEY does not meet requirements"
```bash
# Generate a proper key
python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"
```
### Secret provider errors
```bash
# Test secret provider connectivity
python -c "
from config.settings.secrets import get_secret_provider
provider = get_secret_provider()
print(provider.list_secrets())
"
```