From 3f44d4e1cf2125cbff4ac9aa4ebd0afe69d8603e Mon Sep 17 00:00:00 2001 From: dendencat Date: Sun, 7 Sep 2025 16:49:22 +0900 Subject: [PATCH] =?UTF-8?q?conflict=E3=81=AE=E8=A7=A3=E6=B6=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 15 + .gitignore | 5 +- docker-compose.override.yml.example | 34 ++ docker-compose.yml | 19 +- docs/CONFIGURATION.md | 324 ++++++++++++++++++ docs/QUICK_CONFIG_REFERENCE.md | 128 +++++++ requirements.txt | 47 ++- scripts/production_checklist.sh | 112 ++++++ techblog_cms/health.py | 58 ++++ techblog_cms/management/__init__.py | 0 techblog_cms/management/commands/__init__.py | 0 techblog_cms/management/commands/backup_db.py | 90 +++++ techblog_cms/settings.py | 146 ++++++++ techblog_cms/settings_production.py | 59 ++++ 14 files changed, 1025 insertions(+), 12 deletions(-) create mode 100644 docker-compose.override.yml.example create mode 100644 docs/CONFIGURATION.md create mode 100644 docs/QUICK_CONFIG_REFERENCE.md create mode 100755 scripts/production_checklist.sh create mode 100644 techblog_cms/health.py create mode 100644 techblog_cms/management/__init__.py create mode 100644 techblog_cms/management/commands/__init__.py create mode 100644 techblog_cms/management/commands/backup_db.py create mode 100644 techblog_cms/settings_production.py diff --git a/.env.example b/.env.example index c820134..c02609a 100644 --- a/.env.example +++ b/.env.example @@ -31,3 +31,18 @@ CSRF_TRUSTED_ORIGINS=https://yourdomain.com SECURE_SSL_REDIRECT=True SESSION_COOKIE_SECURE=True CSRF_COOKIE_SECURE=True + +# Email Configuration (Production) +EMAIL_HOST=smtp.gmail.com +EMAIL_PORT=587 +EMAIL_USE_TLS=True +EMAIL_HOST_USER=your-email@gmail.com +EMAIL_HOST_PASSWORD=your-app-password +DEFAULT_FROM_EMAIL=noreply@yourdomain.com +ADMIN_EMAIL=admin@yourdomain.com + +# Domain Configuration +DOMAIN=yourdomain.com + +# Sentry Error Tracking (Optional) +# SENTRY_DSN=https://your-sentry-dsn@sentry.io/project-id diff --git a/.gitignore b/.gitignore index 5a35e8e..26cd526 100644 --- a/.gitignore +++ b/.gitignore @@ -222,4 +222,7 @@ _note/ # backup files *.bak -*.tmp \ No newline at end of file +*.tmp + +# Docker override files +docker-compose.override.yml diff --git a/docker-compose.override.yml.example b/docker-compose.override.yml.example new file mode 100644 index 0000000..f22cfc9 --- /dev/null +++ b/docker-compose.override.yml.example @@ -0,0 +1,34 @@ +# Docker Compose Override for Development +# Copy this file to docker-compose.override.yml for local development + +version: '3' + +services: + django: + # Mount source code for hot reloading + volumes: + - .:/app + - logs:/app/logs + # Enable debug mode for development + environment: + DEBUG: "True" + ALLOWED_HOSTS: "localhost,127.0.0.1" + # Use development server instead of gunicorn + command: python manage.py runserver 0.0.0.0:8000 + + db: + # Expose PostgreSQL port for local development tools + ports: + - "5432:5432" + + redis: + # Expose Redis port for local development tools + ports: + - "6379:6379" + + nginx: + # Disable HTTPS for local development + ports: + - "80:80" + # Comment out HTTPS port for local dev + # - "443:443" \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 0fbc639..6ec70b9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -51,12 +51,11 @@ services: expose: - 8000 environment: - SECRET_KEY: ${SECRET_KEY} + SECRET_KEY: ${SECRET_KEY} # Loaded from .env file DEBUG: ${DEBUG:-False} - ALLOWED_HOSTS: ${ALLOWED_HOSTS:-localhost,127.0.0.1,blog.iohub.link} - # Prefer passing through DATABASE_URL from .env to avoid duplication + ALLOWED_HOSTS: ${ALLOWED_HOSTS} DATABASE_URL: ${DATABASE_URL} - REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379/0 + REDIS_URL: ${REDIS_URL} DJANGO_SETTINGS_MODULE: techblog_cms.settings PYTHONPATH: /app DJANGO_ENV: production @@ -75,6 +74,12 @@ services: memory: 512M # Security: Runs the application as a non-root user. # Environment variables for sensitive settings. + healthcheck: + test: ["CMD", "python", "-c", "import requests; requests.get('http://localhost:8000/health/')"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s # Database - PostgreSQL db: @@ -105,9 +110,9 @@ services: # Caching - Redis redis: image: redis:7-alpine - command: redis-server --requirepass ${REDIS_PASSWORD} - environment: - REDIS_PASSWORD: ${REDIS_PASSWORD} + command: redis-server --requirepass ${REDIS_PASSWORD:-your_redis_password} # Set a password + ports: + - "6379:6379" networks: - techblog_network restart: always diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md new file mode 100644 index 0000000..29e45ca --- /dev/null +++ b/docs/CONFIGURATION.md @@ -0,0 +1,324 @@ +# Tech Blog CMS Configuration Guide + +This guide covers all configuration aspects of the Tech Blog CMS application. + +## Table of Contents +- [Environment Variables](#environment-variables) +- [Django Settings](#django-settings) +- [Docker Configuration](#docker-configuration) +- [Nginx Configuration](#nginx-configuration) +- [Database Configuration](#database-configuration) +- [Redis Configuration](#redis-configuration) +- [SSL/TLS Configuration](#ssltls-configuration) +- [Development Setup](#development-setup) +- [Production Deployment](#production-deployment) + +## Environment Variables + +Create a `.env` file in the project root (copy from `.env.example`): + +```bash +cp .env.example .env +``` + +### Required Environment Variables + +| Variable | Description | Example | +|----------|-------------|---------| +| `SECRET_KEY` | Django secret key (generate a secure one) | `your-50-char-secret-key` | +| `DEBUG` | Debug mode (False for production) | `False` | +| `ALLOWED_HOSTS` | Comma-separated list of allowed hosts | `.localhost,127.0.0.1,yourdomain.com` | +| `DATABASE_URL` | PostgreSQL connection string | `postgres://user:pass@host:5432/dbname` | +| `REDIS_URL` | Redis connection string | `redis://redis:6379/1` | + +### Database Credentials + +| Variable | Description | Default | +|----------|-------------|---------| +| `POSTGRES_USER` | PostgreSQL username | `techblog` | +| `POSTGRES_PASSWORD` | PostgreSQL password | `techblogpass` | +| `POSTGRES_DB` | PostgreSQL database name | `techblogdb` | + +### Optional OAuth Settings + +| Variable | Description | +|----------|-------------| +| `GOOGLE_CLIENT_ID` | Google OAuth client ID | +| `GOOGLE_CLIENT_SECRET` | Google OAuth client secret | +| `GITHUB_CLIENT_ID` | GitHub OAuth client ID | +| `GITHUB_CLIENT_SECRET` | GitHub OAuth client secret | + +### Security Settings + +| Variable | Description | Production Value | +|----------|-------------|------------------| +| `CSRF_TRUSTED_ORIGINS` | Trusted origins for CSRF | `https://yourdomain.com` | +| `SECURE_SSL_REDIRECT` | Force HTTPS redirect | `True` | +| `SESSION_COOKIE_SECURE` | Secure session cookies | `True` | +| `CSRF_COOKIE_SECURE` | Secure CSRF cookies | `True` | + +## Django Settings + +The main Django settings file is located at `techblog_cms/settings.py`. + +### Key Configuration Areas + +1. **Secret Key Management** + ```python + SECRET_KEY = os.environ.get('SECRET_KEY', 'django-insecure-default-key') + ``` + +2. **Debug Mode** + ```python + DEBUG = config('DEBUG', default=False, cast=bool) + ``` + +3. **Database Configuration** + ```python + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': os.environ.get('POSTGRES_DB', 'techblogdb'), + 'USER': os.environ.get('POSTGRES_USER', 'techblog'), + 'PASSWORD': os.environ.get('POSTGRES_PASSWORD', 'techblogpass'), + 'HOST': 'db', + 'PORT': '5432', + } + } + ``` + +4. **Static Files** + ```python + STATIC_URL = 'static/' + STATIC_ROOT = os.path.join(BASE_DIR, 'static') + ``` + +## Docker Configuration + +### Docker Compose Services + +The application uses Docker Compose with the following services: + +1. **nginx** - Load balancer and web server +2. **django** - Web application server +3. **db** - PostgreSQL database +4. **redis** - Caching and session storage +5. **static** - Static file server +6. **certbot** - SSL certificate management + +### Development Override + +For local development, create a `docker-compose.override.yml`: + +```bash +cp docker-compose.override.yml.example docker-compose.override.yml +``` + +This file enables: +- Hot reloading +- Debug mode +- Exposed database ports +- Simplified networking + +## Nginx Configuration + +### Production Configuration +- HTTPS with TLS 1.2/1.3 +- HTTP/2 support +- Security headers (HSTS, X-Frame-Options, etc.) +- OCSP stapling +- Gzip compression + +### Development Configuration +- HTTP only on port 80 +- Simplified proxy settings +- No SSL requirements + +### Key Locations +- Static files: `/static/` +- Media files: `/media/` +- ACME challenges: `/.well-known/acme-challenge/` + +## Database Configuration + +### PostgreSQL Settings +- Version: 16 (Alpine) +- Default port: 5432 +- Data persistence: Docker volume `db_data` + +### Connection Pooling +Configure in Django settings: +```python +DATABASES['default']['CONN_MAX_AGE'] = 60 +``` + +### Backup Strategy +Regular backups recommended using: +```bash +docker-compose exec db pg_dump -U $POSTGRES_USER $POSTGRES_DB > backup.sql +``` + +## Redis Configuration + +### Usage +- Session storage +- Cache backend +- Celery broker (if implemented) + +### Security +- Password protection enabled +- Network isolation via Docker networks +- Version: Redis 7 (Alpine) + +### Configuration in Django +```python +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.redis.RedisCache', + 'LOCATION': os.environ.get('REDIS_URL', 'redis://redis:6379/1'), + } +} +``` + +## SSL/TLS Configuration + +### Let's Encrypt Integration +1. Domain configuration in `.env`: + ``` + DOMAIN=yourdomain.com + ``` + +2. Initialize certificates: + ```bash + ./scripts/init-letsencrypt.sh + ``` + +3. Automatic renewal via Certbot container + +### SSL Settings +- Protocols: TLS 1.2, TLS 1.3 +- Strong cipher suites +- OCSP stapling enabled +- HSTS with preload + +## Development Setup + +1. **Create environment file** + ```bash + cp .env.example .env + # Edit .env with your settings + ``` + +2. **Create override file** + ```bash + cp docker-compose.override.yml.example docker-compose.override.yml + ``` + +3. **Start services** + ```bash + docker-compose up -d + ``` + +4. **Run migrations** + ```bash + docker-compose exec django python manage.py migrate + ``` + +5. **Create superuser** + ```bash + docker-compose exec django python manage.py createsuperuser + ``` + +## Production Deployment + +### Pre-deployment Checklist + +- [ ] Set `DEBUG=False` in `.env` +- [ ] Generate strong `SECRET_KEY` +- [ ] Configure `ALLOWED_HOSTS` with your domain +- [ ] Set secure database passwords +- [ ] Configure Redis password +- [ ] Enable all security settings +- [ ] Set up SSL certificates +- [ ] Configure backups +- [ ] Set up monitoring + +### Deployment Steps + +1. **Prepare environment** + ```bash + # Copy and configure .env + cp .env.example .env + # Edit with production values + ``` + +2. **Build and start services** + ```bash + docker-compose build + docker-compose up -d + ``` + +3. **Initialize SSL** + ```bash + ./scripts/init-letsencrypt.sh + ``` + +4. **Run database migrations** + ```bash + docker-compose exec django python manage.py migrate + ``` + +5. **Collect static files** + ```bash + docker-compose exec django python manage.py collectstatic --noinput + ``` + +### Health Checks + +Monitor service health: +```bash +docker-compose ps +docker-compose logs -f [service_name] +``` + +### Scaling + +To scale Django workers: +```bash +docker-compose up -d --scale django=3 +``` + +## Troubleshooting + +### Common Issues + +1. **Database connection errors** + - Check `DATABASE_URL` format + - Verify PostgreSQL container is running + - Check network connectivity + +2. **Static files not loading** + - Run `collectstatic` command + - Check nginx volume mounts + - Verify `STATIC_ROOT` setting + +3. **SSL certificate issues** + - Ensure domain DNS is configured + - Check Certbot logs + - Verify nginx SSL configuration + +### Debug Commands + +```bash +# Check Django logs +docker-compose logs django + +# Access Django shell +docker-compose exec django python manage.py shell + +# Check nginx configuration +docker-compose exec nginx nginx -t + +# Database console +docker-compose exec db psql -U $POSTGRES_USER $POSTGRES_DB +``` \ No newline at end of file diff --git a/docs/QUICK_CONFIG_REFERENCE.md b/docs/QUICK_CONFIG_REFERENCE.md new file mode 100644 index 0000000..ce4d45f --- /dev/null +++ b/docs/QUICK_CONFIG_REFERENCE.md @@ -0,0 +1,128 @@ +# Tech Blog CMS - Quick Configuration Reference + +## Essential Files + +1. **`.env`** - Environment variables (create from `.env.example`) +2. **`docker-compose.yml`** - Service orchestration +3. **`techblog_cms/settings.py`** - Django settings +4. **`nginx/conf.d/default.conf`** - Web server configuration + +## Quick Start + +```bash +# 1. Copy and configure environment +cp .env.example .env +# Edit .env with your settings + +# 2. For development +cp docker-compose.override.yml.example docker-compose.override.yml + +# 3. Start services +docker-compose up -d + +# 4. Initialize database +docker-compose exec django python manage.py migrate + +# 5. Create admin user +docker-compose exec django python manage.py createsuperuser +``` + +## Key Environment Variables + +```bash +# Security +SECRET_KEY= # python -c "import secrets; print(secrets.token_urlsafe(50))" +DEBUG=False # Always False in production + +# Database +POSTGRES_USER=techblog_prod_user +POSTGRES_PASSWORD= +POSTGRES_DB=techblog_prod_db + +# Redis +REDIS_PASSWORD= + +# Domain +DOMAIN=yourdomain.com +ALLOWED_HOSTS=.yourdomain.com,yourdomain.com +``` + +## Service Endpoints + +- **Application**: http://localhost (dev) / https://yourdomain.com (prod) +- **Admin Panel**: /admin/ +- **Health Check**: /health/ +- **Readiness Check**: /ready/ +- **Static Files**: /static/ +- **Media Files**: /media/ + +## Useful Commands + +```bash +# Check production readiness +./scripts/production_checklist.sh + +# Database backup +docker-compose exec django python manage.py backup_db + +# Collect static files +docker-compose exec django python manage.py collectstatic --noinput + +# View logs +docker-compose logs -f django +docker-compose logs -f nginx + +# Shell access +docker-compose exec django python manage.py shell +docker-compose exec db psql -U $POSTGRES_USER $POSTGRES_DB +``` + +## Directory Structure + +``` +techblog_cms/ +├── .env # Environment variables (git ignored) +├── docker-compose.yml # Service definitions +├── docker-compose.override.yml # Development overrides (git ignored) +├── techblog_cms/ # Django application +│ ├── settings.py # Main settings +│ ├── settings_production.py # Production overrides +│ └── management/ # Custom commands +├── nginx/ # Web server config +│ ├── conf.d/ # Site configurations +│ └── ssl/ # SSL certificates +├── logs/ # Application logs +├── static/ # Static assets +├── media/ # User uploads +└── backups/ # Database backups +``` + +## Security Checklist + +- [ ] Strong SECRET_KEY generated +- [ ] DEBUG=False in production +- [ ] Database passwords changed from defaults +- [ ] Redis password configured +- [ ] SSL certificates installed +- [ ] ALLOWED_HOSTS properly configured +- [ ] Security headers enabled +- [ ] Regular backups scheduled + +## Troubleshooting + +1. **Can't connect to database** + - Check DATABASE_URL format + - Verify PostgreSQL is running: `docker-compose ps db` + +2. **Static files not loading** + - Run: `docker-compose exec django python manage.py collectstatic` + - Check nginx volumes in docker-compose.yml + +3. **SSL issues** + - Ensure domain DNS points to server + - Run: `./scripts/init-letsencrypt.sh` + - Check: `docker-compose logs certbot` + +4. **Application errors** + - Check logs: `docker-compose logs django` + - Verify migrations: `docker-compose exec django python manage.py showmigrations` \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 957bbbc..df69e54 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,4 @@ -# Add other dependencies as needed -redis>=5.0,<6.0 -psycopg2-binary>=2.9,<3.0 -gunicorn>=21.2,<22.0 +# Core dependencies Django==4.2.10 python-decouple>=3.8,<4.0 Pillow>=10.0,<11.0 @@ -10,3 +7,45 @@ pytest>=7.0,<8.0 pytest-django>=4.5,<5.0 Markdown>=3.4,<4.0 Pygments>=2.15,<3.0 + +# Database +psycopg2-binary>=2.9,<3.0 + +# Caching and sessions +redis>=5.0,<6.0 +django-redis>=5.4,<6.0 + +# Web server +gunicorn>=21.2,<22.0 + +# Production optimizations +whitenoise>=6.5,<7.0 # Static file serving +django-compressor>=4.4,<5.0 # CSS/JS compression + +# Security +django-cors-headers>=4.3,<5.0 +django-csp>=3.7,<4.0 # Content Security Policy + +# Monitoring and debugging (production) +sentry-sdk>=1.39,<2.0 +django-extensions>=3.2,<4.0 + +# Testing +pytest>=7.4,<8.0 +pytest-django>=4.7,<5.0 +pytest-cov>=4.1,<5.0 + +# Code quality +black>=23.12,<24.0 +flake8>=6.1,<7.0 +isort>=5.13,<6.0 + +# Documentation +markdown>=3.5,<4.0 + +# Optional: Image processing +# Pillow>=10.1,<11.0 + +# Optional: API framework +# djangorestframework>=3.14,<4.0 +# django-filter>=23.5,<24.0 diff --git a/scripts/production_checklist.sh b/scripts/production_checklist.sh new file mode 100755 index 0000000..50fccb7 --- /dev/null +++ b/scripts/production_checklist.sh @@ -0,0 +1,112 @@ +#!/bin/bash + +# Production Deployment Checklist Script +# This script helps verify that the system is ready for production deployment + +echo "=========================================" +echo "Tech Blog CMS Production Checklist" +echo "=========================================" +echo "" + +# Color codes +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Track overall status +READY=true + +# Function to check a condition +check() { + local description=$1 + local command=$2 + + echo -n "Checking: $description... " + + if eval $command > /dev/null 2>&1; then + echo -e "${GREEN}✓ PASS${NC}" + else + echo -e "${RED}✗ FAIL${NC}" + READY=false + fi +} + +# Function to check environment variable +check_env() { + local var_name=$1 + local description=$2 + + echo -n "Checking: $description... " + + if [ -n "${!var_name}" ]; then + echo -e "${GREEN}✓ SET${NC}" + else + echo -e "${RED}✗ NOT SET${NC}" + READY=false + fi +} + +echo "1. Environment Variables" +echo "------------------------" +check_env "SECRET_KEY" "Django SECRET_KEY" +check_env "POSTGRES_PASSWORD" "PostgreSQL password" +check_env "REDIS_PASSWORD" "Redis password" +check_env "DOMAIN" "Domain name" + +echo "" +echo "2. Configuration Files" +echo "----------------------" +check "Environment file exists" "[ -f .env ]" +check "Production settings exist" "[ -f techblog_cms/settings_production.py ]" +check ".env is in .gitignore" "grep -q '^\.env$' .gitignore" + +echo "" +echo "3. Security Settings" +echo "-------------------" +check "DEBUG is False" "grep -q 'DEBUG=False' .env" +check "SECRET_KEY is not default" "! grep -q 'django-insecure' .env" +check "SECURE_SSL_REDIRECT is True" "grep -q 'SECURE_SSL_REDIRECT=True' .env" + +echo "" +echo "4. Docker Services" +echo "-----------------" +check "Docker is installed" "command -v docker" +check "Docker Compose is installed" "command -v docker-compose" +check "Docker daemon is running" "docker info" + +echo "" +echo "5. SSL/TLS Setup" +echo "----------------" +check "SSL init script exists" "[ -f scripts/init-letsencrypt.sh ]" +check "SSL init script is executable" "[ -x scripts/init-letsencrypt.sh ]" +check "Nginx SSL directory exists" "[ -d nginx/ssl ]" + +echo "" +echo "6. Directory Structure" +echo "---------------------" +check "Logs directory exists" "[ -d logs ]" +check "Static directory exists" "[ -d static ]" +check "Media directory exists" "[ -d media ] || mkdir -p media" +check "Backups directory exists" "[ -d backups ] || mkdir -p backups" + +echo "" +echo "=========================================" +if [ "$READY" = true ]; then + echo -e "${GREEN}✓ System is ready for production deployment!${NC}" + echo "" + echo "Next steps:" + echo "1. Run: docker-compose build" + echo "2. Run: ./scripts/init-letsencrypt.sh (for SSL setup)" + echo "3. Run: docker-compose up -d" + echo "4. Run: docker-compose exec django python manage.py migrate" + echo "5. Run: docker-compose exec django python manage.py collectstatic --noinput" + echo "6. Run: docker-compose exec django python manage.py createsuperuser" +else + echo -e "${RED}✗ System is NOT ready for production deployment${NC}" + echo "" + echo "Please fix the issues above before deploying to production." +fi +echo "=========================================" + +exit $([ "$READY" = true ] && echo 0 || echo 1) \ No newline at end of file diff --git a/techblog_cms/health.py b/techblog_cms/health.py new file mode 100644 index 0000000..f959c27 --- /dev/null +++ b/techblog_cms/health.py @@ -0,0 +1,58 @@ +""" +Health check views for monitoring +""" +import logging +from django.http import JsonResponse +from django.db import connection +from django.core.cache import cache +from django.views import View + +logger = logging.getLogger(__name__) + + +class HealthCheckView(View): + """Basic health check endpoint""" + + def get(self, request): + return JsonResponse({ + 'status': 'healthy', + 'service': 'techblog_cms' + }) + + +class ReadinessCheckView(View): + """Readiness check - verifies all dependencies are accessible""" + + def get(self, request): + checks = { + 'database': self._check_database(), + 'cache': self._check_cache(), + } + + all_healthy = all(checks.values()) + status_code = 200 if all_healthy else 503 + + return JsonResponse({ + 'status': 'ready' if all_healthy else 'not_ready', + 'checks': checks + }, status=status_code) + + def _check_database(self): + """Check database connectivity""" + try: + with connection.cursor() as cursor: + cursor.execute("SELECT 1") + return True + except Exception as e: + logger.error(f"Database health check failed: {e}") + return False + + def _check_cache(self): + """Check cache connectivity""" + try: + cache.set('health_check', 'ok', 10) + value = cache.get('health_check') + return value == 'ok' + except Exception as e: + logger.error(f"Cache health check failed: {e}") + return False \ No newline at end of file diff --git a/techblog_cms/management/__init__.py b/techblog_cms/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/techblog_cms/management/commands/__init__.py b/techblog_cms/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/techblog_cms/management/commands/backup_db.py b/techblog_cms/management/commands/backup_db.py new file mode 100644 index 0000000..2a3a67b --- /dev/null +++ b/techblog_cms/management/commands/backup_db.py @@ -0,0 +1,90 @@ +""" +Database backup management command +""" +import os +import subprocess +from datetime import datetime +from django.core.management.base import BaseCommand +from django.conf import settings +from django.db import connection + + +class Command(BaseCommand): + help = 'Create a backup of the database' + + def add_arguments(self, parser): + parser.add_argument( + '--output', + type=str, + default=None, + help='Output file path for the backup' + ) + parser.add_argument( + '--compress', + action='store_true', + help='Compress the backup with gzip' + ) + + def handle(self, *args, **options): + db_settings = connection.settings_dict + + # Generate backup filename + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + backup_dir = os.path.join(settings.BASE_DIR, 'backups') + os.makedirs(backup_dir, exist_ok=True) + + if options['output']: + backup_file = options['output'] + else: + backup_file = os.path.join( + backup_dir, + f"backup_{db_settings['NAME']}_{timestamp}.sql" + ) + + # Build pg_dump command + env = os.environ.copy() + env['PGPASSWORD'] = db_settings['PASSWORD'] + + cmd = [ + 'pg_dump', + '-h', db_settings['HOST'], + '-p', str(db_settings['PORT']), + '-U', db_settings['USER'], + '-d', db_settings['NAME'], + '--no-password', + '--verbose', + ] + + try: + self.stdout.write(f"Creating backup: {backup_file}") + + if options['compress']: + backup_file += '.gz' + with open(backup_file, 'wb') as f: + p1 = subprocess.Popen(cmd, stdout=subprocess.PIPE, env=env) + p2 = subprocess.Popen(['gzip'], stdin=p1.stdout, stdout=f) + p1.stdout.close() + p2.communicate() + else: + with open(backup_file, 'w') as f: + subprocess.run(cmd, stdout=f, env=env, check=True) + + self.stdout.write( + self.style.SUCCESS(f"Successfully created backup: {backup_file}") + ) + + # Get file size + size = os.path.getsize(backup_file) + size_mb = size / (1024 * 1024) + self.stdout.write(f"Backup size: {size_mb:.2f} MB") + + except subprocess.CalledProcessError as e: + self.stdout.write( + self.style.ERROR(f"Backup failed: {e}") + ) + raise + except Exception as e: + self.stdout.write( + self.style.ERROR(f"Unexpected error: {e}") + ) + raise \ No newline at end of file diff --git a/techblog_cms/settings.py b/techblog_cms/settings.py index 24e4df2..4fcc8e0 100644 --- a/techblog_cms/settings.py +++ b/techblog_cms/settings.py @@ -154,3 +154,149 @@ # Default primary key field type DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +# Password validation +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + 'OPTIONS': { + 'min_length': 8, + } + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + +# Internationalization +LANGUAGE_CODE = 'en-us' +TIME_ZONE = 'UTC' +USE_I18N = True +USE_TZ = True + +# Security Settings for Production +if not DEBUG: + SECURE_SSL_REDIRECT = config('SECURE_SSL_REDIRECT', default=True, cast=bool) + SESSION_COOKIE_SECURE = config('SESSION_COOKIE_SECURE', default=True, cast=bool) + CSRF_COOKIE_SECURE = config('CSRF_COOKIE_SECURE', default=True, cast=bool) + SECURE_BROWSER_XSS_FILTER = True + SECURE_CONTENT_TYPE_NOSNIFF = True + X_FRAME_OPTIONS = 'DENY' + SECURE_HSTS_SECONDS = 31536000 + SECURE_HSTS_INCLUDE_SUBDOMAINS = True + SECURE_HSTS_PRELOAD = True + SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') + +# CSRF Settings +CSRF_TRUSTED_ORIGINS = config('CSRF_TRUSTED_ORIGINS', default='', cast=Csv()) + +# Cache Configuration +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.redis.RedisCache', + 'LOCATION': config('REDIS_URL', default='redis://redis:6379/1'), + 'OPTIONS': { + 'CLIENT_CLASS': 'django_redis.client.DefaultClient', + }, + 'KEY_PREFIX': 'techblog', + 'TIMEOUT': 300, + } +} + +# Session Configuration +SESSION_ENGINE = 'django.contrib.sessions.backends.cache' +SESSION_CACHE_ALIAS = 'default' +SESSION_COOKIE_AGE = 86400 # 24 hours +SESSION_COOKIE_NAME = 'techblog_sessionid' + +# Logging Configuration +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'verbose': { + 'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}', + 'style': '{', + }, + 'simple': { + 'format': '{levelname} {message}', + 'style': '{', + }, + }, + 'filters': { + 'require_debug_false': { + '()': 'django.utils.log.RequireDebugFalse', + }, + }, + 'handlers': { + 'console': { + 'level': 'INFO', + 'class': 'logging.StreamHandler', + 'formatter': 'verbose' + }, + 'file': { + 'level': 'ERROR', + 'class': 'logging.handlers.RotatingFileHandler', + 'filename': os.path.join(BASE_DIR, 'logs', 'django.log'), + 'maxBytes': 1024 * 1024 * 15, # 15MB + 'backupCount': 10, + 'formatter': 'verbose', + }, + 'mail_admins': { + 'level': 'ERROR', + 'class': 'django.utils.log.AdminEmailHandler', + 'filters': ['require_debug_false'], + } + }, + 'root': { + 'handlers': ['console'], + 'level': 'INFO', + }, + 'loggers': { + 'django': { + 'handlers': ['console', 'file'], + 'level': 'INFO', + 'propagate': False, + }, + 'django.request': { + 'handlers': ['file', 'mail_admins'], + 'level': 'ERROR', + 'propagate': False, + }, + 'techblog_cms': { + 'handlers': ['console', 'file'], + 'level': 'DEBUG' if DEBUG else 'INFO', + 'propagate': False, + }, + }, +} + +# Media files configuration +MEDIA_URL = '/media/' +MEDIA_ROOT = os.path.join(BASE_DIR, 'media') + +# Email configuration (for production) +if not DEBUG: + EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' + EMAIL_HOST = config('EMAIL_HOST', default='smtp.gmail.com') + EMAIL_PORT = config('EMAIL_PORT', default=587, cast=int) + EMAIL_USE_TLS = config('EMAIL_USE_TLS', default=True, cast=bool) + EMAIL_HOST_USER = config('EMAIL_HOST_USER', default='') + EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD', default='') + DEFAULT_FROM_EMAIL = config('DEFAULT_FROM_EMAIL', default='noreply@techblog.com') + ADMINS = [('Admin', config('ADMIN_EMAIL', default='admin@techblog.com'))] + +# Testing configuration +if 'test' in sys.argv: + DATABASES['default'] = { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': ':memory:', + } + PASSWORD_HASHERS = [ + 'django.contrib.auth.hashers.MD5PasswordHasher', + ] diff --git a/techblog_cms/settings_production.py b/techblog_cms/settings_production.py new file mode 100644 index 0000000..9dca2d7 --- /dev/null +++ b/techblog_cms/settings_production.py @@ -0,0 +1,59 @@ +""" +Production settings for Tech Blog CMS +""" +from .settings import * + +# Override settings for production +DEBUG = False + +# Security Settings - All should be True in production +SECURE_SSL_REDIRECT = True +SESSION_COOKIE_SECURE = True +CSRF_COOKIE_SECURE = True +SECURE_BROWSER_XSS_FILTER = True +SECURE_CONTENT_TYPE_NOSNIFF = True +X_FRAME_OPTIONS = 'DENY' +SECURE_HSTS_SECONDS = 31536000 # 1 year +SECURE_HSTS_INCLUDE_SUBDOMAINS = True +SECURE_HSTS_PRELOAD = True +SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') + +# Content Security Policy +CSP_DEFAULT_SRC = ("'self'",) +CSP_STYLE_SRC = ("'self'", "'unsafe-inline'", "https://fonts.googleapis.com") +CSP_SCRIPT_SRC = ("'self'", "'unsafe-inline'", "'unsafe-eval'") +CSP_FONT_SRC = ("'self'", "https://fonts.gstatic.com") +CSP_IMG_SRC = ("'self'", "data:", "https:") + +# Additional middleware for production +MIDDLEWARE.insert(0, 'django.middleware.security.SecurityMiddleware') + +# Force cookies to be httponly +SESSION_COOKIE_HTTPONLY = True +CSRF_COOKIE_HTTPONLY = True + +# Database connection pooling +DATABASES['default']['CONN_MAX_AGE'] = 60 + +# Disable debug toolbar in production +INTERNAL_IPS = [] + +# Use whitenoise for static files in production (optional) +# MIDDLEWARE.insert(1, 'whitenoise.middleware.WhiteNoiseMiddleware') +# STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' + +# Production logging - only log warnings and above +LOGGING['root']['level'] = 'WARNING' +LOGGING['loggers']['django']['level'] = 'WARNING' +LOGGING['loggers']['techblog_cms']['level'] = 'INFO' + +# Sentry integration (optional) +# import sentry_sdk +# from sentry_sdk.integrations.django import DjangoIntegration +# +# sentry_sdk.init( +# dsn=config('SENTRY_DSN', default=''), +# integrations=[DjangoIntegration()], +# traces_sample_rate=0.1, +# send_default_pii=False +# ) \ No newline at end of file