diff --git a/app b/app new file mode 100644 index 0000000..f7b5e19 --- /dev/null +++ b/app @@ -0,0 +1,106 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: memelord + namespace: memelord-andrei +spec: + replicas: 1 + selector: + matchLabels: + app: memelord + template: + metadata: + labels: + app: memelord + spec: + containers: + - name: memelord + image: ghcr.io/l4rm4nd/memelord:latest + imagePullPolicy: Always + ports: + - name: http + containerPort: 8000 + protocol: TCP + + env: + - name: DOMAIN + value: "memelord-andrei.ee-lte-1.codemowers.io" + - name: DB_ENGINE + value: "postgres" + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: memelord-andrei-database + key: username + - name: POSTGRES_HOST + value: "memelord-andrei-database-rw" + - name: POSTGRES_PORT + value: "5432" + - name: POSTGRES_DB + value: "memelord-andrei" + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: memelord-andrei-database + key: password + - name: REDIS_HOST + value: "memelord-andrei-redis" + - name: REDIS_PORT + value: "6379" + - name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + name: memelord-andrei-redis + key: redis-password + - name: STORAGE_BACKEND + value: "s3" + - name: AWS_STORAGE_BUCKET_NAME + value: "memelord-andrei" + - name: AWS_S3_ENDPOINT_URL + value: "https://minio.ee-lte-1.codemowers.io/" + - name: AWS_S3_REGION_NAME + value: "ee-lte-1" + - name: AWS_ACCESS_KEY_ID + valueFrom: + secretKeyRef: + name: memelord-andrei-bucket + key: accessKey + - name: AWS_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + name: memelord-andrei-bucket + key: secretKey + - name: OIDC_ENABLED + value: "True" + - name: OIDC_CREATE_USER + value: "True" + - name: OIDC_RP_CLIENT_ID + valueFrom: + secretKeyRef: + name: oidc-client-memelord-andrei-owner-secrets + key: OIDC_CLIENT_ID + - name: OIDC_RP_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: oidc-client-memelord-andrei-owner-secrets + key: OIDC_CLIENT_SECRET + - name: OIDC_OP_AUTHORIZATION_ENDPOINT + value: "https://auth.ee-lte-1.codemowers.io/auth" + - name: OIDC_OP_TOKEN_ENDPOINT + value: "http://passmower.passmower.svc.cluster.local/token" + - name: OIDC_OP_USER_ENDPOINT + value: "http://passmower.passmower.svc.cluster.local/me" + - name: OIDC_OP_JWKS_ENDPOINT + value: "http://passmower.passmower.svc.cluster.local/jwks" + - name: OIDC_RP_SIGN_ALGO + value: "RS256" + - name: OIDC_AUTOLOGIN + value: "False" + - name: DEBUG + value: "True" + - name: SECURE_COOKIES + value: "True" + - name: ENABLE_PUBLIC_FEED + value: "True" + - name: AWS_S3_ADDRESSING_STYLE + value: path \ No newline at end of file diff --git a/configmap b/configmap deleted file mode 100644 index bf030b0..0000000 --- a/configmap +++ /dev/null @@ -1,704 +0,0 @@ -""" -Django settings for myproject project. - -Generated by 'django-admin startproject' using Django 3.2.16. - -For more information on this file, see -https://docs.djangoproject.com/en/3.2/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/3.2/ref/settings/ -""" - -from pathlib import Path -from dotenv import load_dotenv -import os -import pytz -import secrets -from django.utils.html import escape -from django.utils.translation import gettext_lazy as _ -from csp.constants import NONE, SELF, UNSAFE_INLINE, UNSAFE_EVAL - -# Load environment variables from .env file -load_dotenv() - -# Build paths inside the project like this: BASE_DIR / 'subdir'. -BASE_DIR = Path(__file__).resolve().parent.parent - -# get debug modus from env -DEBUG = os.environ.get('DEBUG', 'False').lower() in ['true'] - -# get container version from env -VERSION = escape(os.environ.get("VERSION", '')) - -# Enable/disable public meme feed feature (must be after import os) -ENABLE_PUBLIC_FEED = os.environ.get('ENABLE_PUBLIC_FEED', 'False').lower() in ['true', '1', 'yes'] - -# auto-generate a secure secret key or use from env variable -SECRET_KEY = os.environ.get("SECRET_KEY", secrets.token_urlsafe(32)) - -# define allowed hosts and trusted domains via env variables -DOMAIN = "" -ALLOWED_HOSTS = ["127.0.0.1"] -CSRF_TRUSTED_ORIGINS = ["http://127.0.0.1:8000"] - -DOMAIN = str(os.environ.get("DOMAIN", "localhost")) -TRUSTED_PORT = str(os.environ.get("PORT", "8000")) - -if DOMAIN: - domains = DOMAIN.split(',') - for domain in domains: - domain = domain.strip().rstrip('/').replace('http://', '').replace('https://', '') - if domain: - ALLOWED_HOSTS.append(domain) - TRUSTED_USER_DOMAIN_HTTP = f"http://{domain}:{TRUSTED_PORT}" - TRUSTED_USER_DOMAIN_HTTP_80_DEFAULT = f"http://{domain}" - TRUSTED_USER_DOMAIN_HTTPS = f"https://{domain}:{TRUSTED_PORT}" - TRUSTED_USER_DOMAIN_HTTPS_443_DEFAULT = f"https://{domain}" - CSRF_TRUSTED_ORIGINS.extend([TRUSTED_USER_DOMAIN_HTTP, TRUSTED_USER_DOMAIN_HTTPS, TRUSTED_USER_DOMAIN_HTTP_80_DEFAULT, TRUSTED_USER_DOMAIN_HTTPS_443_DEFAULT]) - -#Session Management -CSRF_COOKIE_HTTPONLY = True -SESSION_EXPIRE_AT_BROWSER_CLOSE = os.environ.get('SESSION_EXPIRE_AT_BROWSER_CLOSE', 'True').lower() in ['true'] -SESSION_COOKIE_AGE = int(os.environ.get('SESSION_COOKIE_AGE', '30')) * 60 -SESSION_COOKIE_NAME = 'Session' -SESSION_COOKIE_SAMESITE = 'Lax' - -# ============================================================================= -# REDIS CACHE CONFIGURATION (for sessions and general caching) -# ============================================================================= -# If REDIS_HOST is not set, sessions will use database (backward compatible) -REDIS_HOST = os.environ.get("REDIS_HOST", "") -REDIS_PORT = os.environ.get("REDIS_PORT", "6379") -REDIS_DB = os.environ.get("REDIS_DB", "0") -REDIS_PASSWORD = os.environ.get("REDIS_PASSWORD", "") - -if REDIS_HOST: - # Redis is available - use it for caching and sessions - CACHES = { - "default": { - "BACKEND": "django_redis.cache.RedisCache", - "LOCATION": f"redis://{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB}", - "OPTIONS": { - "CLIENT_CLASS": "django_redis.client.DefaultClient", - "PASSWORD": REDIS_PASSWORD if REDIS_PASSWORD else None, - "SOCKET_CONNECT_TIMEOUT": 5, # seconds - "SOCKET_TIMEOUT": 5, # seconds - "RETRY_ON_TIMEOUT": True, - "CONNECTION_POOL_KWARGS": { - "max_connections": 50, - "retry_on_timeout": True, - }, - "COMPRESSOR": "django_redis.compressors.zlib.ZlibCompressor", - "IGNORE_EXCEPTIONS": True, # Don't crash if Redis is down - }, - "KEY_PREFIX": "memelord", - "TIMEOUT": 300, # 5 minutes default - } - } - - # Use Redis for session storage (cloud-native) - SESSION_ENGINE = "django.contrib.sessions.backends.cache" - SESSION_CACHE_ALIAS = "default" -else: - # Redis not configured - use database sessions (backward compatible) - SESSION_ENGINE = "django.contrib.sessions.backends.db" - # No CACHES configuration - Django will use local memory cache - -SECURE_COOKIES = os.environ.get('SECURE_COOKIES', 'False').lower() in ['true'] - -if SECURE_COOKIES: - # transmit cookies over encrypted https only - SESSION_COOKIE_SECURE = True - CSRF_COOKIE_SECURE = True - # also set hsts response header - SECURE_HSTS_SECONDS = "31536000" - SECURE_HSTS_PRELOAD = True - SECURE_HSTS_INCLUDE_SUBDOMAINS = True -else: - # transmit cookies over unencrypted http - SESSION_COOKIE_SECURE = False - CSRF_COOKIE_SECURE = False - -# http security response headers -SECURE_BROWSER_XSS_FILTER = True -SECURE_CONTENT_TYPE_NOSNIFF = True -X_FRAME_OPTIONS = 'DENY' -REFERRER_POLICY = 'same-origin' - -# Load from environment, default to "'self'" -raw_frame_ancestors = os.environ.get("CSP_FRAME_ANCESTORS", "'none'") -# Split by comma, strip spaces, and keep properly quoted entries -FRAME_ANCESTORS = [item.strip() for item in raw_frame_ancestors.split(',') if item.strip()] - -# Build CSP img-src list dynamically based on storage backend -STORAGE_BACKEND = os.environ.get('STORAGE_BACKEND', 'local').lower() -IMG_SRC_LIST = ["'self'", "data:", "blob:", "https://img.logo.dev"] - -# Add S3 domains to CSP if using S3 storage -if STORAGE_BACKEND == 's3': - AWS_STORAGE_BUCKET_NAME = os.environ.get('AWS_STORAGE_BUCKET_NAME') - AWS_S3_CUSTOM_DOMAIN = os.environ.get('AWS_S3_CUSTOM_DOMAIN') - AWS_S3_REGION_NAME = os.environ.get('AWS_S3_REGION_NAME', 'us-east-1') - AWS_S3_ENDPOINT_URL = os.environ.get('AWS_S3_ENDPOINT_URL') - AWS_S3_USE_ACCELERATE_ENDPOINT = os.environ.get('AWS_S3_USE_ACCELERATE_ENDPOINT', 'False').lower() in ['true'] - - # Always add custom domain if specified (CDN like CloudFront) - if AWS_S3_CUSTOM_DOMAIN: - IMG_SRC_LIST.append(f"https://{AWS_S3_CUSTOM_DOMAIN}") - - # Detect S3 provider based on endpoint URL - if AWS_S3_ENDPOINT_URL: - # S3-compatible service detected - from urllib.parse import urlparse - parsed_url = urlparse(AWS_S3_ENDPOINT_URL) - endpoint_domain = parsed_url.netloc - endpoint_scheme = parsed_url.scheme or 'https' - - # Add the endpoint domain - IMG_SRC_LIST.append(f"{endpoint_scheme}://{endpoint_domain}") - - # Add bucket-based subdomain format if applicable - if endpoint_domain and AWS_STORAGE_BUCKET_NAME: - IMG_SRC_LIST.append(f"{endpoint_scheme}://{AWS_STORAGE_BUCKET_NAME}.{endpoint_domain}") - - # Provider-specific URL patterns - endpoint_lower = endpoint_domain.lower() if endpoint_domain else "" - - # DigitalOcean Spaces: also add CDN domain - if 'digitaloceanspaces.com' in endpoint_lower: - # DigitalOcean Spaces CDN format: bucket-name.region.cdn.digitaloceanspaces.com - if AWS_STORAGE_BUCKET_NAME and AWS_S3_REGION_NAME: - IMG_SRC_LIST.append(f"https://{AWS_STORAGE_BUCKET_NAME}.{AWS_S3_REGION_NAME}.cdn.digitaloceanspaces.com") - - # Cloudflare R2: also add public bucket URL format - elif 'r2.cloudflarestorage.com' in endpoint_lower: - # Cloudflare R2 can use custom domains via public.r2.dev - # Format: https://bucket-name.account-id.r2.dev (if public) - # This is typically set via AWS_S3_CUSTOM_DOMAIN, but we note it here - pass - - # Wasabi: supports path-style and virtual-hosted-style - elif 'wasabisys.com' in endpoint_lower: - # Already covered by endpoint_domain and bucket subdomain - pass - - # Linode Object Storage: supports path-style and virtual-hosted-style - elif 'linodeobjects.com' in endpoint_lower: - # Already covered by endpoint_domain and bucket subdomain - pass - - # Backblaze B2: supports path-style and virtual-hosted-style - elif 'backblazeb2.com' in endpoint_lower: - # Already covered by endpoint_domain and bucket subdomain - pass - - # MinIO: custom deployment, already covered - # Other S3-compatible services: already covered - - else: - # No endpoint URL = Standard AWS S3 - if AWS_STORAGE_BUCKET_NAME: - # Add AWS S3 virtual-hosted-style URLs - IMG_SRC_LIST.append(f"https://{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com") - IMG_SRC_LIST.append(f"https://{AWS_STORAGE_BUCKET_NAME}.s3.{AWS_S3_REGION_NAME}.amazonaws.com") - - # Add path-style URL format (legacy but still supported) - IMG_SRC_LIST.append("https://s3.amazonaws.com") - IMG_SRC_LIST.append(f"https://s3.{AWS_S3_REGION_NAME}.amazonaws.com") - - # Add dual-stack endpoints (IPv6 support) - IMG_SRC_LIST.append(f"https://{AWS_STORAGE_BUCKET_NAME}.s3.dualstack.{AWS_S3_REGION_NAME}.amazonaws.com") - - # Add S3 Transfer Acceleration endpoint if enabled - if AWS_S3_USE_ACCELERATE_ENDPOINT: - IMG_SRC_LIST.append(f"https://{AWS_STORAGE_BUCKET_NAME}.s3-accelerate.amazonaws.com") - IMG_SRC_LIST.append(f"https://{AWS_STORAGE_BUCKET_NAME}.s3-accelerate.dualstack.amazonaws.com") - -# Add Azure domains to CSP if using Azure storage -elif STORAGE_BACKEND == 'azure': - AZURE_ACCOUNT_NAME = os.environ.get('AZURE_ACCOUNT_NAME') - AZURE_CUSTOM_DOMAIN = os.environ.get('AZURE_CUSTOM_DOMAIN') - - if AZURE_CUSTOM_DOMAIN: - IMG_SRC_LIST.append(f"https://{AZURE_CUSTOM_DOMAIN}") - - if AZURE_ACCOUNT_NAME: - IMG_SRC_LIST.append(f"https://{AZURE_ACCOUNT_NAME}.blob.core.windows.net") - -# Add GCS domains to CSP if using GCS storage -elif STORAGE_BACKEND == 'gcs': - GS_BUCKET_NAME = os.environ.get('GS_BUCKET_NAME') - GS_CUSTOM_ENDPOINT = os.environ.get('GS_CUSTOM_ENDPOINT') - - if GS_CUSTOM_ENDPOINT: - IMG_SRC_LIST.append(GS_CUSTOM_ENDPOINT) - - if GS_BUCKET_NAME: - # GCS can use multiple URL formats - IMG_SRC_LIST.append("https://storage.googleapis.com") - IMG_SRC_LIST.append("https://storage.cloud.google.com") - IMG_SRC_LIST.append(f"https://{GS_BUCKET_NAME}.storage.googleapis.com") - -# SFTP/Dropbox typically serve through Django (using 'self' origin) -elif STORAGE_BACKEND == 'sftp': - # SFTP files are retrieved by Django and served through Django views - # Optional: If you have nginx serving the same SFTP directory via HTTP - SFTP_CUSTOM_DOMAIN = os.environ.get('SFTP_CUSTOM_DOMAIN') - if SFTP_CUSTOM_DOMAIN: - IMG_SRC_LIST.append(f"https://{SFTP_CUSTOM_DOMAIN}") - -elif STORAGE_BACKEND == 'dropbox': - # Dropbox files are retrieved by Django and served through Django views - # No additional CSP configuration needed - pass - -CSP_IMG_SRC_EXTRA = os.environ.get('CSP_IMG_SRC_EXTRA', '') -if CSP_IMG_SRC_EXTRA: - extra_domains = [domain.strip() for domain in CSP_IMG_SRC_EXTRA.split(',') if domain.strip()] - IMG_SRC_LIST.extend(extra_domains) - -CONTENT_SECURITY_POLICY = { - "DIRECTIVES": { - "default-src": ["'self'"], - "style-src": ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://cdn.jsdelivr.net"], - "script-src": ["'self'", "'unsafe-inline'", "'unsafe-eval'", "https://cdn.jsdelivr.net"], - "font-src": ["'self'", "https://fonts.googleapis.com", "https://fonts.gstatic.com"], - "img-src": IMG_SRC_LIST, - "object-src": ["'none'"], - "connect-src": ["'self'"], - "frame-ancestors": FRAME_ANCESTORS, - }, -} - -# Application definition -INSTALLED_APPS = [ - 'myapp', - 'django_celery_beat', - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'csp', - 'storages', -] - -MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - #'django.middleware.locale.LocaleMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django_http_referrer_policy.middleware.ReferrerPolicyMiddleware', - 'csp.middleware.CSPMiddleware', -] - -ROOT_URLCONF = 'myproject.urls' - -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [os.path.join(BASE_DIR, 'myapp/templates/registration')], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], - }, - }, -] - -# Database -# https://docs.djangoproject.com/en/3.2/ref/settings/#databases - -DB_ENGINE = os.environ.get("DB_ENGINE", "sqlite3") - -if DB_ENGINE == "sqlite3": - - DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'database', 'db.sqlite3'), - } -} - -elif DB_ENGINE == "postgres": - - DB_HOST = os.environ.get("POSTGRES_HOST", "db") - DB_PORT = os.environ.get("POSTGRES_PORT", "5432") - DB_USER = os.environ.get("POSTGRES_USER", "memelord") - DB_PASSWORD = os.environ.get("POSTGRES_PASSWORD", "memelord") - DB_NAME = os.environ.get("POSTGRES_DB", "memelord") - - DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': DB_NAME, - 'HOST': DB_HOST, - 'PORT': DB_PORT, - 'USER': DB_USER, - 'PASSWORD': DB_PASSWORD, - } - } - -# Password validation -# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators - -AUTH_PASSWORD_VALIDATORS = [ - { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', - }, -] - -# Internationalization -# https://docs.djangoproject.com/en/3.2/topics/i18n/ -LANGUAGE_CODE = 'en-us' -TIME_ZONE = os.environ.get('TZ', 'Europe/Berlin') -USE_I18N = True -USE_L10N = True -USE_TZ = True - -LANGUAGES = [ - ('en', _('English')), - ('de', _('German')), - ('fr', _('French')), - ('it', _('Italian')), -] - -LOCALE_PATHS = [ - os.path.join(BASE_DIR, 'locale') -] - -# Celery configuration -# http://docs.celeryproject.org/en/latest/configuration.html - -LOGS_DIR = os.path.join(BASE_DIR, 'logs') - -# ============================================================================= -# LOGGING CONFIGURATION -# ============================================================================= -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'formatters': { - 'verbose': { - 'format': '{levelname} {asctime} {module} {message}', - 'style': '{', - }, - 'simple': { - 'format': '{levelname} {message}', - 'style': '{', - }, - }, - 'handlers': { - 'console': { - 'class': 'logging.StreamHandler', - 'formatter': 'verbose', - }, - 'file': { - 'class': 'logging.handlers.RotatingFileHandler', - 'filename': os.path.join(LOGS_DIR, 'django.log'), - 'maxBytes': 1024 * 1024 * 15, # 15MB - 'backupCount': 10, - 'formatter': 'verbose', - }, - }, - 'loggers': { - 'django': { - 'handlers': ['console', 'file'], - 'level': os.environ.get('DJANGO_LOG_LEVEL', 'INFO'), - 'propagate': False, - }, - 'myapp': { - 'handlers': ['console', 'file'], - 'level': os.environ.get('DJANGO_LOG_LEVEL', 'INFO'), - 'propagate': False, - }, - 'storages': { - 'handlers': ['console', 'file'], - 'level': 'DEBUG' if DEBUG else 'INFO', - 'propagate': False, - }, - 'boto3': { - 'handlers': ['console', 'file'], - 'level': 'DEBUG' if DEBUG else 'INFO', - 'propagate': False, - }, - 'botocore': { - 'handlers': ['console', 'file'], - 'level': 'DEBUG' if DEBUG else 'INFO', - 'propagate': False, - }, - }, - 'root': { - 'handlers': ['console', 'file'], - 'level': 'INFO', - }, -} - -# Create logs directory if it doesn't exist -os.makedirs(LOGS_DIR, exist_ok=True) - -STATIC_URL = '/static/' -STATIC_ROOT = os.path.join(BASE_DIR, 'myapp', 'static') - -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' - -LOGIN_URL = '/accounts/login/' -LOGIN_REDIRECT_URL = '/' -LOGOUT_REDIRECT_URL = '/post-logout/' -ALLOW_LOGOUT_GET_METHOD = True - -WSGI_APPLICATION = 'myproject.wsgi.application' - -# check if oidc is enabled -OIDC_ENABLED = os.environ.get('OIDC_ENABLED', 'False').lower() in ['true'] -OIDC_AUTOLOGIN = os.environ.get('OIDC_AUTOLOGIN', 'False').lower() in ['true'] - -# Max file upload size in MB (default: 10MB) -# Can be configured via MAX_UPLOAD_SIZE_MB environment variable -MAX_UPLOAD_SIZE_MB = int(os.environ.get('MAX_UPLOAD_SIZE_MB', '10')) * 1024 * 1024 - -# ============================================================================= -# STORAGE BACKEND CONFIGURATION -# ============================================================================= -# IMPORTANT: This must be set BEFORE MEDIA_URL and MEDIA_ROOT -# Configure storage backend via STORAGE_BACKEND environment variable -# Supported backends: local, s3, azure, gcs, sftp, dropbox -# Default: local (filesystem storage) - -STORAGE_BACKEND = os.environ.get('STORAGE_BACKEND', 'local').lower() - -# Django 5.0+ uses STORAGES instead of DEFAULT_FILE_STORAGE -# Set both for compatibility -if STORAGE_BACKEND == 's3': - # Amazon S3 / Compatible S3 Storage (MinIO, DigitalOcean Spaces, etc.) - AWS_ACCESS_KEY_ID = os.environ.get('AWS_ACCESS_KEY_ID') - AWS_SECRET_ACCESS_KEY = os.environ.get('AWS_SECRET_ACCESS_KEY') - AWS_STORAGE_BUCKET_NAME = os.environ.get('AWS_STORAGE_BUCKET_NAME') - AWS_S3_REGION_NAME = os.environ.get('AWS_S3_REGION_NAME', 'us-east-1') - AWS_S3_CUSTOM_DOMAIN = os.environ.get('AWS_S3_CUSTOM_DOMAIN') - AWS_S3_ENDPOINT_URL = os.environ.get('AWS_S3_ENDPOINT_URL') # Only set for S3-compatible services (MinIO, etc.) - AWS_DEFAULT_ACL = os.environ.get('AWS_DEFAULT_ACL', 'private') - AWS_S3_OBJECT_PARAMETERS = { - 'CacheControl': 'max-age=86400', - } - AWS_QUERYSTRING_AUTH = os.environ.get('AWS_QUERYSTRING_AUTH', 'True').lower() in ['true'] - AWS_S3_FILE_OVERWRITE = os.environ.get('AWS_S3_FILE_OVERWRITE', 'False').lower() in ['true'] - AWS_LOCATION = os.environ.get('AWS_LOCATION', 'media') - - # Use signature version 4 (required for all regions) - AWS_S3_SIGNATURE_VERSION = 's3v4' - - # Use virtual-hosted-style URLs as default (bucket-name.s3.region.amazonaws.com) - ALLOWED_S3_ADDRESSING_STYLES = {"virtual", "path"} - AWS_S3_ADDRESSING_STYLE = os.environ.get("AWS_S3_ADDRESSING_STYLE", "virtual").strip().lower() - if AWS_S3_ADDRESSING_STYLE not in ALLOWED_S3_ADDRESSING_STYLES: - raise ValueError( - f"Invalid AWS_S3_ADDRESSING_STYLE={AWS_S3_ADDRESSING_STYLE!r}. " - f"Allowed values: {sorted(ALLOWED_S3_ADDRESSING_STYLES)}" - ) - - # Django 5.0+ STORAGES configuration - STORAGES = { - "default": { - "BACKEND": "storages.backends.s3boto3.S3Boto3Storage", - }, - "staticfiles": { - "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage", - }, - } - - # Legacy setting for older Django versions - DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage' - - # Update MEDIA_URL - if AWS_S3_CUSTOM_DOMAIN: - MEDIA_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/{AWS_LOCATION}/' - elif AWS_STORAGE_BUCKET_NAME: - # Use region-specific virtual-hosted-style URL - if AWS_S3_REGION_NAME == 'us-east-1': - MEDIA_URL = f'https://{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com/{AWS_LOCATION}/' - else: - MEDIA_URL = f'https://{AWS_STORAGE_BUCKET_NAME}.s3.{AWS_S3_REGION_NAME}.amazonaws.com/{AWS_LOCATION}/' - else: - MEDIA_URL = "/media/" - - MEDIA_ROOT = BASE_DIR / "media" - -elif STORAGE_BACKEND == 'azure': - # Microsoft Azure Blob Storage - AZURE_ACCOUNT_NAME = os.environ.get('AZURE_ACCOUNT_NAME') - AZURE_ACCOUNT_KEY = os.environ.get('AZURE_ACCOUNT_KEY') - AZURE_CONTAINER = os.environ.get('AZURE_CONTAINER', 'media') - AZURE_SSL = os.environ.get('AZURE_SSL', 'True').lower() in ['true'] - AZURE_UPLOAD_MAX_CONN = int(os.environ.get('AZURE_UPLOAD_MAX_CONN', '2')) - AZURE_CONNECTION_TIMEOUT_SECS = int(os.environ.get('AZURE_CONNECTION_TIMEOUT_SECS', '20')) - AZURE_BLOB_MAX_MEMORY_SIZE = os.environ.get('AZURE_BLOB_MAX_MEMORY_SIZE', '2MB') - AZURE_URL_EXPIRATION_SECS = int(os.environ.get('AZURE_URL_EXPIRATION_SECS', '3600')) - AZURE_OVERWRITE_FILES = os.environ.get('AZURE_OVERWRITE_FILES', 'False').lower() in ['true'] - AZURE_LOCATION = os.environ.get('AZURE_LOCATION', '') - AZURE_CUSTOM_DOMAIN = os.environ.get('AZURE_CUSTOM_DOMAIN') - - STORAGES = { - "default": { - "BACKEND": "storages.backends.azure_storage.AzureStorage", - }, - "staticfiles": { - "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage", - }, - } - - DEFAULT_FILE_STORAGE = 'storages.backends.azure_storage.AzureStorage' - - if AZURE_CUSTOM_DOMAIN: - MEDIA_URL = f'https://{AZURE_CUSTOM_DOMAIN}/{AZURE_LOCATION}' - elif AZURE_ACCOUNT_NAME: - MEDIA_URL = f'https://{AZURE_ACCOUNT_NAME}.blob.core.windows.net/{AZURE_CONTAINER}/{AZURE_LOCATION}' - else: - MEDIA_URL = "/media/" - - MEDIA_ROOT = BASE_DIR / "media" - -elif STORAGE_BACKEND == 'gcs': - # Google Cloud Storage - GS_BUCKET_NAME = os.environ.get('GS_BUCKET_NAME') - GS_PROJECT_ID = os.environ.get('GS_PROJECT_ID') - GS_CREDENTIALS = os.environ.get('GS_CREDENTIALS') - GS_DEFAULT_ACL = os.environ.get('GS_DEFAULT_ACL', 'private') - GS_FILE_OVERWRITE = os.environ.get('GS_FILE_OVERWRITE', 'False').lower() in ['true'] - GS_LOCATION = os.environ.get('GS_LOCATION', 'media') - GS_CUSTOM_ENDPOINT = os.environ.get('GS_CUSTOM_ENDPOINT') - GS_QUERYSTRING_AUTH = os.environ.get('GS_QUERYSTRING_AUTH', 'True').lower() in ['true'] - - STORAGES = { - "default": { - "BACKEND": "storages.backends.gcloud.GoogleCloudStorage", - }, - "staticfiles": { - "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage", - }, - } - - DEFAULT_FILE_STORAGE = 'storages.backends.gcloud.GoogleCloudStorage' - - if GS_CUSTOM_ENDPOINT: - MEDIA_URL = f'{GS_CUSTOM_ENDPOINT}/{GS_LOCATION}/' - elif GS_BUCKET_NAME: - MEDIA_URL = f'https://storage.googleapis.com/{GS_BUCKET_NAME}/{GS_LOCATION}/' - else: - MEDIA_URL = "/media/" - - MEDIA_ROOT = BASE_DIR / "media" - -elif STORAGE_BACKEND == 'sftp': - # SFTP Storage - SFTP_STORAGE_HOST = os.environ.get('SFTP_STORAGE_HOST') - SFTP_STORAGE_ROOT = os.environ.get('SFTP_STORAGE_ROOT', '/media/') - SFTP_STORAGE_PARAMS = { - 'port': int(os.environ.get('SFTP_STORAGE_PORT', '22')), - 'username': os.environ.get('SFTP_STORAGE_USERNAME'), - 'password': os.environ.get('SFTP_STORAGE_PASSWORD'), - 'pkey': os.environ.get('SFTP_STORAGE_PRIVATE_KEY'), - } - SFTP_STORAGE_INTERACTIVE = os.environ.get('SFTP_STORAGE_INTERACTIVE', 'False').lower() in ['true'] - SFTP_STORAGE_FILE_MODE = os.environ.get('SFTP_STORAGE_FILE_MODE') - SFTP_STORAGE_DIR_MODE = os.environ.get('SFTP_STORAGE_DIR_MODE') - SFTP_STORAGE_UID = os.environ.get('SFTP_STORAGE_UID') - SFTP_STORAGE_GID = os.environ.get('SFTP_STORAGE_GID') - SFTP_KNOWN_HOST_FILE = os.environ.get('SFTP_KNOWN_HOST_FILE') - - STORAGES = { - "default": { - "BACKEND": "storages.backends.sftpstorage.SFTPStorage", - }, - "staticfiles": { - "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage", - }, - } - - DEFAULT_FILE_STORAGE = 'storages.backends.sftpstorage.SFTPStorage' - MEDIA_URL = "/media/" - MEDIA_ROOT = BASE_DIR / "media" - -elif STORAGE_BACKEND == 'dropbox': - # Dropbox Storage - DROPBOX_OAUTH2_TOKEN = os.environ.get('DROPBOX_OAUTH2_TOKEN') - DROPBOX_ROOT_PATH = os.environ.get('DROPBOX_ROOT_PATH', '/media') - DROPBOX_TIMEOUT = int(os.environ.get('DROPBOX_TIMEOUT', '100')) - DROPBOX_WRITE_MODE = os.environ.get('DROPBOX_WRITE_MODE', 'add') - - STORAGES = { - "default": { - "BACKEND": "storages.backends.dropbox.DropBoxStorage", - }, - "staticfiles": { - "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage", - }, - } - - DEFAULT_FILE_STORAGE = 'storages.backends.dropbox.DropBoxStorage' - MEDIA_URL = "/media/" - MEDIA_ROOT = BASE_DIR / "media" - -else: - # Local filesystem storage (default) - STORAGES = { - "default": { - "BACKEND": "django.core.files.storage.FileSystemStorage", - }, - "staticfiles": { - "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage", - }, - } - - DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage' - MEDIA_URL = "/media/" - MEDIA_ROOT = BASE_DIR / "media" - -if OIDC_ENABLED: - # get oidc config from env - OIDC_CREATE_USER = os.environ.get('OIDC_CREATE_USER', 'True').lower() in ['true'] - OIDC_RP_SIGN_ALGO = os.environ.get('OIDC_RP_SIGN_ALGO', 'HS256') - OIDC_OP_JWKS_ENDPOINT = os.environ.get('OIDC_OP_JWKS_ENDPOINT') - OIDC_RP_IDP_SIGN_KEY = os.environ.get('OIDC_RP_IDP_SIGN_KEY') - OIDC_RP_CLIENT_ID = os.environ.get('OIDC_RP_CLIENT_ID') - OIDC_RP_CLIENT_SECRET = os.environ.get('OIDC_RP_CLIENT_SECRET') - OIDC_OP_AUTHORIZATION_ENDPOINT = os.environ.get('OIDC_OP_AUTHORIZATION_ENDPOINT') - OIDC_OP_TOKEN_ENDPOINT = os.environ.get('OIDC_OP_TOKEN_ENDPOINT') - OIDC_OP_USER_ENDPOINT = os.environ.get('OIDC_OP_USER_ENDPOINT') - OIDC_RENEW_ID_TOKEN_EXPIRY_SECONDS = float(os.environ.get('OIDC_RENEW_ID_TOKEN_EXPIRY_SECONDS', 900)) - OIDC_USERNAME_ALGO = 'myapp.utils.generate_username' - - # Add 'mozilla_django_oidc.middleware.SessionRefresh' to INSTALLED_APPS - INSTALLED_APPS.append('mozilla_django_oidc') - - # Add 'mozilla_django_oidc' authentication backend - AUTHENTICATION_BACKENDS = ( - 'django.contrib.auth.backends.ModelBackend', - 'mozilla_django_oidc.auth.OIDCAuthenticationBackend', - ) - - # Add 'mozilla_django_oidc.middleware.SessionRefresh' to MIDDLEWARE - # https://mozilla-django-oidc.readthedocs.io/en/stable/installation.html#validate-id-tokens-by-renewing-them - MIDDLEWARE.append('mozilla_django_oidc.middleware.SessionRefresh') - - # Fix http callback issue in mozilla-django-oidc by forcing https; https://github.com/mozilla/mozilla-django-oidc/issues/417 - # OIDC should only be setup behind a TLS reverse proxy anyways - SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") \ No newline at end of file