490 lines
19 KiB
Python
490 lines
19 KiB
Python
from __future__ import annotations
|
|
|
|
import base64
|
|
import hashlib
|
|
import ipaddress
|
|
import os
|
|
from pathlib import Path
|
|
from urllib.parse import urlparse
|
|
|
|
from django.core.exceptions import ImproperlyConfigured
|
|
|
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
|
|
|
|
|
def env_bool(name: str, default: bool = False) -> bool:
|
|
return os.getenv(name, "1" if default else "0").lower() in {"1", "true", "yes", "on"}
|
|
|
|
|
|
def env_list(name: str, default: str = "") -> list[str]:
|
|
return [item.strip() for item in os.getenv(name, default).split(",") if item.strip()]
|
|
|
|
|
|
def env_int_range(name: str, default: int, *, minimum: int, maximum: int) -> int:
|
|
try:
|
|
value = int(os.getenv(name, str(default)))
|
|
except ValueError as exc:
|
|
raise ImproperlyConfigured(f"{name} moet een geheel getal zijn.") from exc
|
|
if not minimum <= value <= maximum:
|
|
raise ImproperlyConfigured(f"{name} moet tussen {minimum} en {maximum} liggen.")
|
|
return value
|
|
|
|
|
|
def env_cidrs(name: str, default: str = "") -> list[str]:
|
|
networks: list[str] = []
|
|
for value in env_list(name, default):
|
|
try:
|
|
networks.append(str(ipaddress.ip_network(value, strict=False)))
|
|
except ValueError as exc:
|
|
raise ImproperlyConfigured(f"{name} bevat een ongeldig IP-netwerk: {value}") from exc
|
|
return networks
|
|
|
|
|
|
def merge_unique(*groups: list[str]) -> list[str]:
|
|
result: list[str] = []
|
|
seen: set[str] = set()
|
|
for group in groups:
|
|
for value in group:
|
|
key = value.casefold()
|
|
if key not in seen:
|
|
seen.add(key)
|
|
result.append(value)
|
|
return result
|
|
|
|
|
|
def normalize_public_base_url(value: str) -> tuple[str, str]:
|
|
raw = value.strip().rstrip("/")
|
|
if not raw:
|
|
return "", ""
|
|
parsed = urlparse(raw)
|
|
if parsed.scheme not in {"http", "https"} or not parsed.hostname:
|
|
raise ImproperlyConfigured("PUBLIC_BASE_URL moet een geldige http(s)-URL zijn.")
|
|
if parsed.username or parsed.password or parsed.query or parsed.fragment:
|
|
raise ImproperlyConfigured(
|
|
"PUBLIC_BASE_URL mag geen credentials, query of fragment bevatten."
|
|
)
|
|
if parsed.path not in {"", "/"}:
|
|
raise ImproperlyConfigured("PUBLIC_BASE_URL mag geen applicatiepad bevatten.")
|
|
try:
|
|
port = parsed.port
|
|
except ValueError as exc:
|
|
raise ImproperlyConfigured("PUBLIC_BASE_URL bevat een ongeldige poort.") from exc
|
|
host = parsed.hostname.casefold()
|
|
display_host = f"[{host}]" if ":" in host else host
|
|
default_port = 443 if parsed.scheme == "https" else 80
|
|
port_suffix = f":{port}" if port and port != default_port else ""
|
|
return f"{parsed.scheme}://{display_host}{port_suffix}", host
|
|
|
|
|
|
def configured_public_base_url() -> tuple[str, str]:
|
|
raw = os.getenv("PUBLIC_BASE_URL", "").strip()
|
|
if not raw:
|
|
app_host = os.getenv("APP_HOST", "").strip()
|
|
if app_host:
|
|
scheme = os.getenv("APP_SCHEME", "https").strip().lower() or "https"
|
|
public_port = os.getenv("APP_PUBLIC_PORT", "").strip()
|
|
raw = f"{scheme}://{app_host}{f':{public_port}' if public_port else ''}"
|
|
return normalize_public_base_url(raw)
|
|
|
|
|
|
def _validate_production_security(
|
|
*,
|
|
debug: bool,
|
|
secret_key: str,
|
|
allowed_hosts: list[str],
|
|
csrf_trusted_origins: list[str],
|
|
session_cookie_secure: bool,
|
|
csrf_cookie_secure: bool,
|
|
secure_ssl_redirect: bool,
|
|
public_base_url: str = "",
|
|
demo_mode_enabled: bool = False,
|
|
demo_read_only: bool = True,
|
|
) -> None:
|
|
if debug:
|
|
return
|
|
if secret_key.strip().lower() in {"", "change-me", "dev-only-change-me", "secret", "insecure"}:
|
|
raise ImproperlyConfigured(
|
|
"Onveilige DJANGO_SECRET_KEY in productie. Stel een sterke waarde in via environment."
|
|
)
|
|
if len(secret_key.strip()) < 50:
|
|
raise ImproperlyConfigured("DJANGO_SECRET_KEY is te kort voor een productieomgeving.")
|
|
if not allowed_hosts:
|
|
raise ImproperlyConfigured("ALLOWED_HOSTS mag in productie niet leeg zijn.")
|
|
if any(host.strip() == "*" for host in allowed_hosts):
|
|
raise ImproperlyConfigured("ALLOWED_HOSTS mag in productie geen wildcard bevatten.")
|
|
if not csrf_trusted_origins:
|
|
raise ImproperlyConfigured("CSRF_TRUSTED_ORIGINS is verplicht bij DEBUG=False.")
|
|
if not any(origin.lower().startswith("https://") for origin in csrf_trusted_origins):
|
|
raise ImproperlyConfigured("CSRF_TRUSTED_ORIGINS moet HTTPS-origins bevatten in productie.")
|
|
if not session_cookie_secure or not csrf_cookie_secure:
|
|
raise ImproperlyConfigured("Session- en CSRF-cookies moeten Secure=True zijn in productie.")
|
|
if not secure_ssl_redirect:
|
|
raise ImproperlyConfigured("SECURE_SSL_REDIRECT moet True zijn in productie.")
|
|
if public_base_url and not public_base_url.startswith("https://"):
|
|
raise ImproperlyConfigured("PUBLIC_BASE_URL moet HTTPS gebruiken in productie.")
|
|
if demo_mode_enabled and not demo_read_only:
|
|
raise ImproperlyConfigured("Een publieke productiedemo moet alleen-lezen zijn.")
|
|
|
|
|
|
SECRET_KEY = os.getenv("DJANGO_SECRET_KEY", "dev-only-change-me")
|
|
DEBUG = env_bool("DJANGO_DEBUG", True)
|
|
PUBLIC_BASE_URL, PUBLIC_HOST = configured_public_base_url()
|
|
ALLOWED_HOSTS = merge_unique(
|
|
env_list("DJANGO_ALLOWED_HOSTS", "localhost,127.0.0.1,testserver"),
|
|
[PUBLIC_HOST] if PUBLIC_HOST else [],
|
|
)
|
|
CSRF_TRUSTED_ORIGINS = merge_unique(
|
|
env_list("DJANGO_CSRF_TRUSTED_ORIGINS"),
|
|
[PUBLIC_BASE_URL] if PUBLIC_BASE_URL else [],
|
|
)
|
|
TIME_ZONE = os.getenv("DJANGO_TIME_ZONE", "Europe/Brussels")
|
|
VACATURERADAR_OWNER_NAME = os.getenv("VACATURERADAR_OWNER_NAME", "Jens").strip()[:40] or "Jens"
|
|
VACATURERADAR_VERSION = os.getenv("VACATURERADAR_VERSION", "0.3.17").strip() or "0.3.17"
|
|
LANGUAGE_CODE = "nl-be"
|
|
USE_I18N = True
|
|
USE_TZ = True
|
|
|
|
INSTALLED_APPS = [
|
|
"django.contrib.admin",
|
|
"django.contrib.auth",
|
|
"django.contrib.contenttypes",
|
|
"django.contrib.sessions",
|
|
"django.contrib.messages",
|
|
"django.contrib.staticfiles",
|
|
"apps.core",
|
|
"apps.profiles",
|
|
"apps.sources",
|
|
"apps.jobs",
|
|
"apps.notifications",
|
|
]
|
|
|
|
MIDDLEWARE = [
|
|
"django.middleware.security.SecurityMiddleware",
|
|
"whitenoise.middleware.WhiteNoiseMiddleware",
|
|
"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",
|
|
"apps.core.middleware.DemoReadOnlyMiddleware",
|
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
|
"apps.core.middleware.SecurityHeadersMiddleware",
|
|
]
|
|
|
|
ROOT_URLCONF = "config.urls"
|
|
TEMPLATES = [
|
|
{
|
|
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
|
"DIRS": [BASE_DIR / "templates"],
|
|
"APP_DIRS": True,
|
|
"OPTIONS": {
|
|
"context_processors": [
|
|
"django.template.context_processors.request",
|
|
"django.contrib.auth.context_processors.auth",
|
|
"django.contrib.messages.context_processors.messages",
|
|
"apps.core.context_processors.navigation_context",
|
|
],
|
|
},
|
|
}
|
|
]
|
|
WSGI_APPLICATION = "config.wsgi.application"
|
|
ASGI_APPLICATION = "config.asgi.application"
|
|
|
|
DATABASE_URL = os.getenv("DATABASE_URL", "").strip()
|
|
POSTGRES_DB = os.getenv("POSTGRES_DB", "vacatureradar")
|
|
POSTGRES_USER = os.getenv("POSTGRES_USER", "vacatureradar")
|
|
POSTGRES_PASSWORD = os.getenv("POSTGRES_PASSWORD", "")
|
|
POSTGRES_HOST = os.getenv("POSTGRES_HOST", "postgres")
|
|
POSTGRES_PORT = os.getenv("POSTGRES_PORT", "5432")
|
|
|
|
|
|
def _postgres_database_from_env() -> dict[str, str | int]:
|
|
if POSTGRES_PASSWORD.lower().startswith("change_me") or POSTGRES_PASSWORD == "CHANGE_ME":
|
|
return {}
|
|
if not (POSTGRES_PASSWORD and POSTGRES_HOST and POSTGRES_DB and POSTGRES_USER):
|
|
return {}
|
|
return {
|
|
"ENGINE": "django.db.backends.postgresql",
|
|
"NAME": POSTGRES_DB,
|
|
"USER": POSTGRES_USER,
|
|
"PASSWORD": POSTGRES_PASSWORD,
|
|
"HOST": POSTGRES_HOST,
|
|
"PORT": POSTGRES_PORT,
|
|
"CONN_MAX_AGE": 60,
|
|
"OPTIONS": {"connect_timeout": 10},
|
|
}
|
|
|
|
|
|
if DATABASE_URL:
|
|
parsed = urlparse(DATABASE_URL)
|
|
DATABASES = {
|
|
"default": {
|
|
"ENGINE": "django.db.backends.postgresql",
|
|
"NAME": parsed.path.lstrip("/"),
|
|
"USER": parsed.username,
|
|
"PASSWORD": parsed.password,
|
|
"HOST": parsed.hostname,
|
|
"PORT": parsed.port or 5432,
|
|
"CONN_MAX_AGE": 60,
|
|
"OPTIONS": {"connect_timeout": 10},
|
|
}
|
|
}
|
|
elif postgres_cfg := _postgres_database_from_env():
|
|
DATABASES = {"default": postgres_cfg}
|
|
else:
|
|
DATABASES = {
|
|
"default": {
|
|
"ENGINE": "django.db.backends.sqlite3",
|
|
"NAME": BASE_DIR / "local" / "db.sqlite3",
|
|
}
|
|
}
|
|
|
|
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"},
|
|
]
|
|
|
|
STATIC_URL = "/static/"
|
|
STATIC_ROOT = BASE_DIR / "staticfiles"
|
|
STATICFILES_DIRS = [BASE_DIR / "static"]
|
|
STORAGES = {
|
|
"default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
|
|
"staticfiles": {
|
|
"BACKEND": (
|
|
"whitenoise.storage.CompressedStaticFilesStorage"
|
|
if DEBUG
|
|
else "whitenoise.storage.CompressedManifestStaticFilesStorage"
|
|
)
|
|
},
|
|
}
|
|
MEDIA_URL = "/media/"
|
|
MEDIA_ROOT = BASE_DIR / "media"
|
|
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
|
LOGIN_URL = "login"
|
|
LOGIN_REDIRECT_URL = "dashboard:today"
|
|
LOGOUT_REDIRECT_URL = "login"
|
|
|
|
# Inloggen kan met gebruikersnaam of e-mailadres. ModelBackend blijft als fallback.
|
|
AUTHENTICATION_BACKENDS = [
|
|
"apps.core.auth_backends.EmailOrUsernameModelBackend",
|
|
"django.contrib.auth.backends.ModelBackend",
|
|
]
|
|
PASSWORD_RESET_TIMEOUT = int(os.getenv("PASSWORD_RESET_TIMEOUT", str(3 * 24 * 3600)))
|
|
|
|
# Microsoft Entra ID (Azure AD) SSO. Uit tenzij expliciet geconfigureerd.
|
|
ENTRA_ID_ENABLED = env_bool("ENTRA_ID_ENABLED", False)
|
|
ENTRA_CLIENT_ID = os.getenv("ENTRA_CLIENT_ID", "")
|
|
ENTRA_CLIENT_SECRET = os.getenv("ENTRA_CLIENT_SECRET", "")
|
|
ENTRA_TENANT_ID = os.getenv("ENTRA_TENANT_ID", "")
|
|
ENTRA_REDIRECT_URI = os.getenv("ENTRA_REDIRECT_URI", "")
|
|
ENTRA_SCOPES = env_list("ENTRA_SCOPES", "User.Read")
|
|
ENTRA_ALLOWED_DOMAINS = env_list("ENTRA_ALLOWED_DOMAINS")
|
|
|
|
# Demo-toegang: in ontwikkeling standaard beschikbaar; in productie alleen expliciet.
|
|
# Een publiek gastaccount blijft standaard read-only en krijgt een begrensde sessie.
|
|
DEMO_MODE_ENABLED = env_bool("DEMO_MODE_ENABLED", DEBUG)
|
|
DEMO_READ_ONLY = env_bool("DEMO_READ_ONLY", True)
|
|
DEMO_SESSION_SECONDS = env_int_range("DEMO_SESSION_SECONDS", 3600, minimum=300, maximum=86400)
|
|
DEMO_USERNAME = os.getenv("DEMO_USERNAME", "demo")
|
|
DEMO_EMAIL = os.getenv("DEMO_EMAIL", "demo@vacatureradar.local")
|
|
|
|
EMAIL_BACKEND = os.getenv("EMAIL_BACKEND", "django.core.mail.backends.console.EmailBackend")
|
|
DEFAULT_FROM_EMAIL = os.getenv("DEFAULT_FROM_EMAIL", "VacatureRadar <vacatureradar@localhost>")
|
|
EMAIL_HOST = os.getenv("SMTP_HOST", "")
|
|
EMAIL_PORT = int(os.getenv("SMTP_PORT", "587"))
|
|
EMAIL_HOST_USER = os.getenv("SMTP_USER", "")
|
|
EMAIL_HOST_PASSWORD = os.getenv("SMTP_PASSWORD", "")
|
|
EMAIL_USE_TLS = env_bool("SMTP_USE_TLS", True)
|
|
M365_TENANT_ID = os.getenv("M365_TENANT_ID", "")
|
|
M365_CLIENT_ID = os.getenv("M365_CLIENT_ID", "")
|
|
M365_CLIENT_SECRET = os.getenv("M365_CLIENT_SECRET", "")
|
|
M365_MAILBOX_USER = os.getenv("M365_MAILBOX_USER", EMAIL_HOST_USER)
|
|
M365_EXCHANGE_SCOPE = "https://outlook.office365.com/.default"
|
|
M365_SMTP_TIMEOUT_SECONDS = env_int_range("M365_SMTP_TIMEOUT_SECONDS", 20, minimum=3, maximum=120)
|
|
|
|
SESSION_COOKIE_HTTPONLY = True
|
|
SESSION_COOKIE_SAMESITE = "Lax"
|
|
_PUBLIC_HTTPS = PUBLIC_BASE_URL.startswith("https://")
|
|
SESSION_COOKIE_SECURE = env_bool("SESSION_COOKIE_SECURE", _PUBLIC_HTTPS)
|
|
CSRF_COOKIE_SECURE = env_bool("CSRF_COOKIE_SECURE", _PUBLIC_HTTPS)
|
|
CSRF_COOKIE_SAMESITE = "Lax"
|
|
SECURE_SSL_REDIRECT = env_bool("SECURE_SSL_REDIRECT", _PUBLIC_HTTPS)
|
|
TRUST_PROXY_HEADERS = env_bool("TRUST_PROXY_HEADERS", bool(PUBLIC_BASE_URL))
|
|
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") if TRUST_PROXY_HEADERS else None
|
|
USE_X_FORWARDED_HOST = env_bool("USE_X_FORWARDED_HOST", False)
|
|
TRUSTED_PROXY_CIDRS = env_cidrs("TRUSTED_PROXY_CIDRS")
|
|
# Interne containerprobes blijven HTTP; publieke applicatieroutes worden wel omgeleid.
|
|
SECURE_REDIRECT_EXEMPT = [r"^health/(?:live|ready)/$"]
|
|
SECURE_HSTS_SECONDS = int(os.getenv("SECURE_HSTS_SECONDS", "0"))
|
|
SECURE_HSTS_INCLUDE_SUBDOMAINS = env_bool("SECURE_HSTS_INCLUDE_SUBDOMAINS", True)
|
|
SECURE_HSTS_PRELOAD = env_bool("SECURE_HSTS_PRELOAD", False)
|
|
SECURE_CONTENT_TYPE_NOSNIFF = True
|
|
SECURE_REFERRER_POLICY = "strict-origin-when-cross-origin"
|
|
X_FRAME_OPTIONS = "DENY"
|
|
SEARCH_ENGINE_INDEXING_ENABLED = env_bool("SEARCH_ENGINE_INDEXING_ENABLED", False)
|
|
|
|
REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379/0")
|
|
CACHE_URL = os.getenv("CACHE_URL", "" if DEBUG else REDIS_URL).strip()
|
|
_IS_TESTING = bool(os.getenv("PYTEST_CURRENT_TEST") or "pytest" in os.sys.modules)
|
|
if CACHE_URL and not _IS_TESTING:
|
|
CACHES = {
|
|
"default": {
|
|
"BACKEND": "django.core.cache.backends.redis.RedisCache",
|
|
"LOCATION": CACHE_URL,
|
|
"KEY_PREFIX": "vacatureradar",
|
|
"TIMEOUT": 300,
|
|
}
|
|
}
|
|
else:
|
|
CACHES = {
|
|
"default": {
|
|
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
|
|
"LOCATION": "vacatureradar-local",
|
|
}
|
|
}
|
|
HEALTHCHECK_REQUIRE_CACHE = env_bool("HEALTHCHECK_REQUIRE_CACHE", not DEBUG)
|
|
CELERY_BROKER_URL = REDIS_URL
|
|
CELERY_RESULT_BACKEND = REDIS_URL
|
|
CELERY_TASK_ALWAYS_EAGER = env_bool("CELERY_TASK_ALWAYS_EAGER", False)
|
|
CELERY_TASK_EAGER_PROPAGATES = True
|
|
CELERY_TASK_ACKS_LATE = True
|
|
CELERY_WORKER_PREFETCH_MULTIPLIER = 1
|
|
CELERY_TASK_DEFAULT_QUEUE = "default"
|
|
CELERY_TASK_TIME_LIMIT = 300
|
|
CELERY_TASK_SOFT_TIME_LIMIT = 270
|
|
CELERY_TIMEZONE = TIME_ZONE
|
|
|
|
IMAP_MAX_MESSAGES_PER_POLL = env_int_range(
|
|
"IMAP_MAX_MESSAGES_PER_POLL", 200, minimum=1, maximum=1000
|
|
)
|
|
IMAP_MAX_MESSAGE_BYTES = env_int_range(
|
|
"IMAP_MAX_MESSAGE_BYTES", 1000000, minimum=1024, maximum=10000000
|
|
)
|
|
IMAP_CONNECT_TIMEOUT_SECONDS = env_int_range(
|
|
"IMAP_CONNECT_TIMEOUT_SECONDS", 15, minimum=3, maximum=60
|
|
)
|
|
MAILBOX_CREDENTIAL_KEYS = env_list("MAILBOX_CREDENTIAL_KEYS")
|
|
if DEBUG and not MAILBOX_CREDENTIAL_KEYS:
|
|
MAILBOX_CREDENTIAL_KEYS = [
|
|
base64.urlsafe_b64encode(
|
|
hashlib.sha256(f"vacatureradar-mailbox:{SECRET_KEY}".encode()).digest()
|
|
).decode("ascii")
|
|
]
|
|
|
|
CELERY_BEAT_SCHEDULE = {
|
|
"runtime-heartbeat": {
|
|
"task": "apps.core.tasks.record_runtime_heartbeat",
|
|
"schedule": 60.0,
|
|
},
|
|
"schedule-mailbox-polls": {
|
|
"task": "apps.sources.tasks.schedule_mailbox_polls",
|
|
"schedule": 300.0,
|
|
},
|
|
"schedule-due-sources": {
|
|
"task": "apps.sources.tasks.schedule_due_sources",
|
|
"schedule": 900.0,
|
|
},
|
|
"expire-old-jobs": {
|
|
"task": "apps.jobs.tasks.update_job_lifecycle",
|
|
"schedule": 3600.0,
|
|
},
|
|
"build-daily-digests": {
|
|
"task": "apps.notifications.tasks.build_due_digests",
|
|
"schedule": 900.0,
|
|
},
|
|
"build-due-reminders": {
|
|
"task": "apps.notifications.tasks.build_due_reminders",
|
|
"schedule": 900.0,
|
|
},
|
|
"enforce-source-health": {
|
|
"task": "apps.sources.tasks.enforce_source_health",
|
|
"schedule": 900.0,
|
|
},
|
|
"recover-source-health": {
|
|
"task": "apps.sources.tasks.recover_source_health",
|
|
"schedule": 3600.0,
|
|
},
|
|
"cleanup-raw-documents": {
|
|
"task": "apps.sources.tasks.cleanup_raw_documents",
|
|
"schedule": 3600.0,
|
|
},
|
|
}
|
|
|
|
FETCHER_USER_AGENT = os.getenv("FETCHER_USER_AGENT", "VacatureRadar/0.1 (+local-personal-use)")
|
|
FETCHER_TIMEOUT_SECONDS = float(os.getenv("FETCHER_TIMEOUT_SECONDS", "20"))
|
|
FETCHER_MAX_BYTES = int(os.getenv("FETCHER_MAX_BYTES", "3000000"))
|
|
FETCHER_MAX_REDIRECTS = int(os.getenv("FETCHER_MAX_REDIRECTS", "5"))
|
|
FETCHER_ALLOW_NONSTANDARD_PORTS = env_bool("FETCHER_ALLOW_NONSTANDARD_PORTS", False)
|
|
SOURCE_POLICY_MODE = os.getenv("SOURCE_POLICY_MODE", "strict")
|
|
SOURCE_LEASE_TTL_SECONDS = float(os.getenv("SOURCE_LEASE_TTL_SECONDS", "180"))
|
|
SOURCE_ORIGIN_MIN_INTERVAL_SECONDS = int(os.getenv("SOURCE_ORIGIN_MIN_INTERVAL_SECONDS", "0"))
|
|
SOURCE_SUCCESS_JITTER_SECONDS = int(os.getenv("SOURCE_SUCCESS_JITTER_SECONDS", "120"))
|
|
SOURCE_FAILURE_BACKOFF_BASE_SECONDS = int(os.getenv("SOURCE_FAILURE_BACKOFF_BASE_SECONDS", "60"))
|
|
SOURCE_FAILURE_BACKOFF_MAX_SECONDS = int(os.getenv("SOURCE_FAILURE_BACKOFF_MAX_SECONDS", "3600"))
|
|
SOURCE_FAILURE_TIMEOUT_BASE_SECONDS = int(os.getenv("SOURCE_FAILURE_TIMEOUT_BASE_SECONDS", "120"))
|
|
SOURCE_FAILURE_JITTER_SECONDS = int(os.getenv("SOURCE_FAILURE_JITTER_SECONDS", "45"))
|
|
SOURCE_REVIEW_TTL_DAYS = int(os.getenv("SOURCE_REVIEW_TTL_DAYS", "90"))
|
|
ROBOTS_CACHE_TTL_SECONDS = float(os.getenv("ROBOTS_CACHE_TTL_SECONDS", "3600"))
|
|
ROBOTS_FETCH_TIMEOUT_SECONDS = float(os.getenv("ROBOTS_FETCH_TIMEOUT_SECONDS", "5"))
|
|
ROBOTS_MAX_BYTES = int(os.getenv("ROBOTS_MAX_BYTES", "131072"))
|
|
RAW_DOCUMENT_RETENTION_DAYS = int(os.getenv("RAW_DOCUMENT_RETENTION_DAYS", "7"))
|
|
MANUAL_IMPORT_PASTE_MAX_BYTES = int(os.getenv("MANUAL_IMPORT_PASTE_MAX_BYTES", "12000"))
|
|
DATA_UPLOAD_MAX_MEMORY_SIZE = int(os.getenv("DATA_UPLOAD_MAX_MEMORY_SIZE", str(8 * 1024 * 1024)))
|
|
FILE_UPLOAD_MAX_MEMORY_SIZE = 0
|
|
DATA_UPLOAD_MAX_NUMBER_FILES = 0
|
|
|
|
OLLAMA_ENABLED = env_bool("OLLAMA_ENABLED", False)
|
|
OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434")
|
|
OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "")
|
|
OLLAMA_TIMEOUT_SECONDS = float(os.getenv("OLLAMA_TIMEOUT_SECONDS", "60"))
|
|
|
|
DIGEST_RECIPIENT = os.getenv("DIGEST_RECIPIENT", "")
|
|
|
|
AUTH_LOGIN_RATE_LIMIT_MAX_ATTEMPTS = int(os.getenv("AUTH_LOGIN_RATE_LIMIT_MAX_ATTEMPTS", "8"))
|
|
AUTH_LOGIN_RATE_LIMIT_WINDOW_SECONDS = int(os.getenv("AUTH_LOGIN_RATE_LIMIT_WINDOW_SECONDS", "300"))
|
|
AUTH_LOGIN_RATE_LIMIT_BLOCK_SECONDS = int(os.getenv("AUTH_LOGIN_RATE_LIMIT_BLOCK_SECONDS", "300"))
|
|
MANUAL_IMPORT_RATE_LIMIT_MAX_ATTEMPTS = int(
|
|
os.getenv("MANUAL_IMPORT_RATE_LIMIT_MAX_ATTEMPTS", "12")
|
|
)
|
|
MANUAL_IMPORT_RATE_LIMIT_WINDOW_SECONDS = int(
|
|
os.getenv("MANUAL_IMPORT_RATE_LIMIT_WINDOW_SECONDS", "120")
|
|
)
|
|
MANUAL_IMPORT_RATE_LIMIT_BLOCK_SECONDS = int(
|
|
os.getenv("MANUAL_IMPORT_RATE_LIMIT_BLOCK_SECONDS", "300")
|
|
)
|
|
|
|
_validate_production_security(
|
|
debug=DEBUG,
|
|
secret_key=SECRET_KEY,
|
|
allowed_hosts=ALLOWED_HOSTS,
|
|
csrf_trusted_origins=CSRF_TRUSTED_ORIGINS,
|
|
session_cookie_secure=SESSION_COOKIE_SECURE,
|
|
csrf_cookie_secure=CSRF_COOKIE_SECURE,
|
|
secure_ssl_redirect=SECURE_SSL_REDIRECT,
|
|
public_base_url=PUBLIC_BASE_URL,
|
|
demo_mode_enabled=DEMO_MODE_ENABLED,
|
|
demo_read_only=DEMO_READ_ONLY,
|
|
)
|
|
|
|
LOGGING = {
|
|
"version": 1,
|
|
"disable_existing_loggers": False,
|
|
"formatters": {
|
|
"jsonish": {
|
|
"format": "%(asctime)s %(levelname)s %(name)s %(message)s",
|
|
}
|
|
},
|
|
"handlers": {
|
|
"console": {"class": "logging.StreamHandler", "formatter": "jsonish"},
|
|
},
|
|
"root": {"handlers": ["console"], "level": os.getenv("LOG_LEVEL", "INFO")},
|
|
}
|
|
|
|
# Testomgeving hoeft geen Redis te hebben.
|
|
if os.getenv("PYTEST_CURRENT_TEST") or "pytest" in os.sys.modules:
|
|
CELERY_TASK_ALWAYS_EAGER = True
|
|
EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
|