FossilRepo

fossilrepo / config / settings.py
Source Blame History 297 lines
4ce269c… ragelink 1 import logging
4ce269c… ragelink 2 import os
4ce269c… ragelink 3 from pathlib import Path
4ce269c… ragelink 4
4ce269c… ragelink 5 from django.core.exceptions import ImproperlyConfigured
4ce269c… ragelink 6
4ce269c… ragelink 7 logger = logging.getLogger(__name__)
4ce269c… ragelink 8
4ce269c… ragelink 9 VERSION = "0.1.0"
4ce269c… ragelink 10
4ce269c… ragelink 11 BASE_DIR = Path(__file__).resolve().parent.parent
4ce269c… ragelink 12
4ce269c… ragelink 13
4ce269c… ragelink 14 def env_str(name: str, default: str | None = None) -> str | None:
4ce269c… ragelink 15 return os.getenv(name, default)
4ce269c… ragelink 16
4ce269c… ragelink 17
4ce269c… ragelink 18 def env_bool(name: str, default: bool = False) -> bool:
4ce269c… ragelink 19 return os.getenv(name, str(default)).lower() in ("true", "1", "yes")
4ce269c… ragelink 20
4ce269c… ragelink 21
4ce269c… ragelink 22 def env_int(name: str, default: int = 0) -> int:
4ce269c… ragelink 23 return int(os.getenv(name, str(default)))
4ce269c… ragelink 24
4ce269c… ragelink 25
4ce269c… ragelink 26 # --- Security ---
4ce269c… ragelink 27
4ce269c… ragelink 28 SECRET_KEY = env_str("DJANGO_SECRET_KEY", "change-me-in-production")
4ce269c… ragelink 29 DEBUG = env_bool("DJANGO_DEBUG", False)
4ce269c… ragelink 30 ALLOWED_HOSTS = [h.strip() for h in env_str("DJANGO_ALLOWED_HOSTS", "localhost,127.0.0.1,0.0.0.0").split(",")]
4ce269c… ragelink 31
4ce269c… ragelink 32 if not DEBUG and SECRET_KEY == "change-me-in-production":
4ce269c… ragelink 33 raise ImproperlyConfigured("DJANGO_SECRET_KEY must be set to a unique, unpredictable value when DEBUG is False.")
4ce269c… ragelink 34
4ce269c… ragelink 35 # --- Application ---
4ce269c… ragelink 36
4ce269c… ragelink 37 ROOT_URLCONF = "config.urls"
4ce269c… ragelink 38 WSGI_APPLICATION = "config.wsgi.application"
4ce269c… ragelink 39 DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
4ce269c… ragelink 40 LOGIN_URL = "/auth/login/"
4ce269c… ragelink 41 LOGIN_REDIRECT_URL = "/"
4ce269c… ragelink 42 LOGOUT_REDIRECT_URL = "/auth/login/"
4ce269c… ragelink 43
4ce269c… ragelink 44 INSTALLED_APPS = [
4ce269c… ragelink 45 "django.contrib.admin",
4ce269c… ragelink 46 "django.contrib.auth",
4ce269c… ragelink 47 "django.contrib.contenttypes",
4ce269c… ragelink 48 "django.contrib.sessions",
4ce269c… ragelink 49 "django.contrib.messages",
4ce269c… ragelink 50 "django.contrib.staticfiles",
4ce269c… ragelink 51 "django.contrib.humanize",
4ce269c… ragelink 52 # Third-party
4ce269c… ragelink 53 "import_export",
4ce269c… ragelink 54 "simple_history",
4ce269c… ragelink 55 "django_celery_results",
4ce269c… ragelink 56 "django_celery_beat",
4ce269c… ragelink 57 "corsheaders",
4ce269c… ragelink 58 "constance",
4ce269c… ragelink 59 "constance.backends.database",
4ce269c… ragelink 60 # Project apps
4ce269c… ragelink 61 "core",
c588255… ragelink 62 "accounts",
4ce269c… ragelink 63 "organization",
4ce269c… ragelink 64 "projects",
4ce269c… ragelink 65 "pages",
4ce269c… ragelink 66 "fossil",
4ce269c… ragelink 67 "testdata",
4ce269c… ragelink 68 ]
c588255… ragelink 69
7a60a24… ragelink 70 # Fossil sync pushes can be large (binary artifacts, images, etc.)
6e2907c… ragelink 71 DATA_UPLOAD_MAX_MEMORY_SIZE = 2 * 1024 * 1024 * 1024 # 2 GB
7a60a24… ragelink 72
4ce269c… ragelink 73 MIDDLEWARE = [
4ce269c… ragelink 74 "corsheaders.middleware.CorsMiddleware",
4ce269c… ragelink 75 "django.middleware.security.SecurityMiddleware",
4ce269c… ragelink 76 "whitenoise.middleware.WhiteNoiseMiddleware",
4ce269c… ragelink 77 "django.contrib.sessions.middleware.SessionMiddleware",
4ce269c… ragelink 78 "django.middleware.common.CommonMiddleware",
4ce269c… ragelink 79 "django.middleware.csrf.CsrfViewMiddleware",
4ce269c… ragelink 80 "django.contrib.auth.middleware.AuthenticationMiddleware",
4ce269c… ragelink 81 "django.contrib.messages.middleware.MessageMiddleware",
4ce269c… ragelink 82 "django.middleware.clickjacking.XFrameOptionsMiddleware",
4ce269c… ragelink 83 "simple_history.middleware.HistoryRequestMiddleware",
4ce269c… ragelink 84 "core.middleware.current_user.CurrentUserMiddleware",
4ce269c… ragelink 85 ]
4ce269c… ragelink 86
4ce269c… ragelink 87 TEMPLATES = [
4ce269c… ragelink 88 {
4ce269c… ragelink 89 "BACKEND": "django.template.backends.django.DjangoTemplates",
4ce269c… ragelink 90 "DIRS": [BASE_DIR / "templates"],
4ce269c… ragelink 91 "APP_DIRS": True,
4ce269c… ragelink 92 "OPTIONS": {
4ce269c… ragelink 93 "context_processors": [
4ce269c… ragelink 94 "django.template.context_processors.debug",
4ce269c… ragelink 95 "django.template.context_processors.request",
4ce269c… ragelink 96 "django.contrib.auth.context_processors.auth",
4ce269c… ragelink 97 "django.contrib.messages.context_processors.messages",
4ce269c… ragelink 98 "core.context_processors.sidebar",
4ce269c… ragelink 99 ],
4ce269c… ragelink 100 },
4ce269c… ragelink 101 },
4ce269c… ragelink 102 ]
4ce269c… ragelink 103
4ce269c… ragelink 104 # --- Database ---
4ce269c… ragelink 105
4ce269c… ragelink 106 DATABASES = {
4ce269c… ragelink 107 "default": {
4ce269c… ragelink 108 "ENGINE": "django.db.backends.postgresql",
4ce269c… ragelink 109 "NAME": env_str("POSTGRES_DB", "fossilrepo"),
4ce269c… ragelink 110 "USER": env_str("POSTGRES_USER", "dbadmin"),
4ce269c… ragelink 111 "PASSWORD": env_str("POSTGRES_PASSWORD", "Password123"),
4ce269c… ragelink 112 "HOST": env_str("POSTGRES_HOST", "localhost"),
4ce269c… ragelink 113 "PORT": env_str("POSTGRES_PORT", "5432"),
4ce269c… ragelink 114 }
4ce269c… ragelink 115 }
4ce269c… ragelink 116
4ce269c… ragelink 117 # --- Cache ---
4ce269c… ragelink 118
4ce269c… ragelink 119 REDIS_URL = env_str("REDIS_URL", "redis://localhost:6379/1")
4ce269c… ragelink 120
4ce269c… ragelink 121 CACHES = {
4ce269c… ragelink 122 "default": {
4ce269c… ragelink 123 "BACKEND": "django.core.cache.backends.redis.RedisCache",
4ce269c… ragelink 124 "LOCATION": REDIS_URL,
4ce269c… ragelink 125 }
4ce269c… ragelink 126 }
4ce269c… ragelink 127
4ce269c… ragelink 128 # --- Auth ---
4ce269c… ragelink 129
4ce269c… ragelink 130 AUTH_PASSWORD_VALIDATORS = [
4ce269c… ragelink 131 {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
4ce269c… ragelink 132 {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
4ce269c… ragelink 133 {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
4ce269c… ragelink 134 {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
4ce269c… ragelink 135 ]
4ce269c… ragelink 136
4ce269c… ragelink 137 AUTHENTICATION_BACKENDS = ["django.contrib.auth.backends.ModelBackend"]
4ce269c… ragelink 138 SESSION_ENGINE = "django.contrib.sessions.backends.db"
4ce269c… ragelink 139 SESSION_COOKIE_HTTPONLY = True
4ce269c… ragelink 140 SESSION_COOKIE_AGE = 60 * 60 * 24 * 30 # 30 days
4ce269c… ragelink 141 CSRF_COOKIE_HTTPONLY = True
4ce269c… ragelink 142
4ce269c… ragelink 143 if not DEBUG:
7e1aaf6… ragelink 144 SESSION_COOKIE_SECURE = env_bool("SESSION_COOKIE_SECURE", True)
7e1aaf6… ragelink 145 CSRF_COOKIE_SECURE = env_bool("CSRF_COOKIE_SECURE", True)
7e1aaf6… ragelink 146 SECURE_SSL_REDIRECT = env_bool("SECURE_SSL_REDIRECT", True)
0da8377… ragelink 147 SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
0c354ac… ragelink 148 SECURE_HSTS_SECONDS = 31536000 # 1 year
0c354ac… ragelink 149 SECURE_HSTS_INCLUDE_SUBDOMAINS = True
0c354ac… ragelink 150 SECURE_HSTS_PRELOAD = True
4ce269c… ragelink 151
4ce269c… ragelink 152 # --- i18n ---
4ce269c… ragelink 153
4ce269c… ragelink 154 LANGUAGE_CODE = "en-us"
4ce269c… ragelink 155 TIME_ZONE = "UTC"
4ce269c… ragelink 156 USE_I18N = True
4ce269c… ragelink 157 USE_TZ = True
4ce269c… ragelink 158
4ce269c… ragelink 159 # --- Static ---
4ce269c… ragelink 160
4ce269c… ragelink 161 STATIC_URL = "/static/"
4ce269c… ragelink 162 STATIC_ROOT = BASE_DIR / "assets"
4ce269c… ragelink 163 STATICFILES_DIRS = [BASE_DIR / "static"]
4ce269c… ragelink 164 STORAGES = {
4ce269c… ragelink 165 "staticfiles": {"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage"},
4ce269c… ragelink 166 }
4ce269c… ragelink 167
4ce269c… ragelink 168 # --- Media / S3 ---
4ce269c… ragelink 169
4ce269c… ragelink 170 USE_S3 = env_bool("USE_S3", False)
4ce269c… ragelink 171
4ce269c… ragelink 172 if USE_S3:
4ce269c… ragelink 173 STORAGES["default"] = {"BACKEND": "storages.backends.s3boto3.S3Boto3Storage"}
4ce269c… ragelink 174 AWS_ACCESS_KEY_ID = env_str("AWS_ACCESS_KEY_ID")
4ce269c… ragelink 175 AWS_SECRET_ACCESS_KEY = env_str("AWS_SECRET_ACCESS_KEY")
4ce269c… ragelink 176 AWS_STORAGE_BUCKET_NAME = env_str("AWS_STORAGE_BUCKET_NAME", "fossilrepo")
4ce269c… ragelink 177 AWS_S3_ENDPOINT_URL = env_str("AWS_S3_ENDPOINT_URL", "")
4ce269c… ragelink 178 AWS_S3_FILE_OVERWRITE = False
4ce269c… ragelink 179 AWS_QUERYSTRING_AUTH = True
4ce269c… ragelink 180 else:
0cab6ed… ragelink 181 STORAGES["default"] = {"BACKEND": "django.core.files.storage.FileSystemStorage"}
4ce269c… ragelink 182 MEDIA_URL = "/media/"
4ce269c… ragelink 183 MEDIA_ROOT = BASE_DIR / "media"
4ce269c… ragelink 184
4ce269c… ragelink 185 # --- Email ---
4ce269c… ragelink 186
4ce269c… ragelink 187 EMAIL_BACKEND = env_str("DJANGO_EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")
4ce269c… ragelink 188 EMAIL_HOST = env_str("EMAIL_HOST", "localhost")
4ce269c… ragelink 189 EMAIL_PORT = env_int("EMAIL_PORT", 1025)
4ce269c… ragelink 190 DEFAULT_FROM_EMAIL = env_str("FROM_EMAIL", "[email protected]")
4ce269c… ragelink 191
4ce269c… ragelink 192 # --- Celery ---
4ce269c… ragelink 193
4ce269c… ragelink 194 CELERY_BROKER_URL = env_str("CELERY_BROKER", "redis://localhost:6379/0")
4ce269c… ragelink 195 CELERY_RESULT_BACKEND = "django-db"
4ce269c… ragelink 196 CELERY_TASK_TRACK_STARTED = True
4ce269c… ragelink 197 CELERY_TASK_TIME_LIMIT = 3600
4ce269c… ragelink 198 CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler"
4ce269c… ragelink 199 CELERY_BEAT_SCHEDULE = {
4ce269c… ragelink 200 "fossil-sync-metadata": {
4ce269c… ragelink 201 "task": "fossil.sync_metadata",
4ce269c… ragelink 202 "schedule": 300.0, # every 5 minutes
4ce269c… ragelink 203 },
4ce269c… ragelink 204 "fossil-check-upstream": {
4ce269c… ragelink 205 "task": "fossil.check_upstream",
4ce269c… ragelink 206 "schedule": 900.0, # every 15 minutes
0da8377… ragelink 207 },
0da8377… ragelink 208 "fossil-dispatch-notifications": {
0da8377… ragelink 209 "task": "fossil.dispatch_notifications",
0da8377… ragelink 210 "schedule": 300.0, # every 5 minutes
c588255… ragelink 211 },
c588255… ragelink 212 "fossil-daily-digest": {
c588255… ragelink 213 "task": "fossil.send_digest",
c588255… ragelink 214 "schedule": 86400.0, # daily
c588255… ragelink 215 "kwargs": {"mode": "daily"},
c588255… ragelink 216 },
c588255… ragelink 217 "fossil-weekly-digest": {
c588255… ragelink 218 "task": "fossil.send_digest",
c588255… ragelink 219 "schedule": 604800.0, # weekly
c588255… ragelink 220 "kwargs": {"mode": "weekly"},
2eca4eb… ragelink 221 },
4ce269c… ragelink 222 }
4ce269c… ragelink 223 CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP = True
4ce269c… ragelink 224
4ce269c… ragelink 225 # --- CORS ---
4ce269c… ragelink 226
4ce269c… ragelink 227 CORS_ALLOW_CREDENTIALS = True
4ce269c… ragelink 228 CORS_ALLOWED_ORIGINS = [o.strip() for o in env_str("CORS_ALLOWED_ORIGINS", "http://localhost:8000").split(",")]
4ce269c… ragelink 229
4ce269c… ragelink 230 CSRF_TRUSTED_ORIGINS = [o.strip() for o in env_str("CSRF_TRUSTED_ORIGINS", "http://localhost:8000").split(",")]
4ce269c… ragelink 231
4ce269c… ragelink 232 # --- Rate limiting ---
4ce269c… ragelink 233
4ce269c… ragelink 234 RATELIMIT_VIEW = "django.views.defaults.permission_denied"
4ce269c… ragelink 235
4ce269c… ragelink 236 # --- Constance (runtime feature toggles) ---
4ce269c… ragelink 237
4ce269c… ragelink 238 CONSTANCE_BACKEND = "constance.backends.database.DatabaseBackend"
4ce269c… ragelink 239 CONSTANCE_CONFIG = {
4ce269c… ragelink 240 "SITE_NAME": ("Fossilrepo", "Display name for the site"),
4ce269c… ragelink 241 "FOSSIL_DATA_DIR": ("/data/repos", "Directory where .fossil repository files are stored"),
4ce269c… ragelink 242 "FOSSIL_STORE_IN_DB": (False, "Store binary snapshots of .fossil files via Django file storage"),
4ce269c… ragelink 243 "FOSSIL_S3_TRACKING": (False, "Track S3/Litestream replication keys and versions"),
4ce269c… ragelink 244 "FOSSIL_S3_BUCKET": ("", "S3 bucket name for Fossil repo replication"),
4ce269c… ragelink 245 "FOSSIL_BINARY_PATH": ("fossil", "Path to the fossil binary"),
2eca4eb… ragelink 246 # Git sync settings
2eca4eb… ragelink 247 "GIT_SYNC_MODE": ("disabled", "Default sync mode: disabled, on_change, scheduled, both"),
2eca4eb… ragelink 248 "GIT_SYNC_SCHEDULE": ("*/15 * * * *", "Default cron schedule for Git sync"),
2eca4eb… ragelink 249 "GIT_MIRROR_DIR": ("/data/git-mirrors", "Directory for Git mirror checkouts"),
2eca4eb… ragelink 250 "GIT_SSH_KEY_DIR": ("/data/ssh-keys", "Directory for SSH key storage"),
2eca4eb… ragelink 251 "GITHUB_OAUTH_CLIENT_ID": ("", "GitHub OAuth App Client ID"),
2eca4eb… ragelink 252 "GITHUB_OAUTH_CLIENT_SECRET": ("", "GitHub OAuth App Client Secret"),
2eca4eb… ragelink 253 "GITLAB_OAUTH_CLIENT_ID": ("", "GitLab OAuth App Client ID"),
2eca4eb… ragelink 254 "GITLAB_OAUTH_CLIENT_SECRET": ("", "GitLab OAuth App Client Secret"),
0c354ac… ragelink 255 # Cloudflare Turnstile (optional bot protection on login)
0c354ac… ragelink 256 "TURNSTILE_ENABLED": (False, "Enable Cloudflare Turnstile on the login page"),
0c354ac… ragelink 257 "TURNSTILE_SITE_KEY": ("", "Cloudflare Turnstile site key (public)"),
0c354ac… ragelink 258 "TURNSTILE_SECRET_KEY": ("", "Cloudflare Turnstile secret key (server-side verification)"),
4ce269c… ragelink 259 }
4ce269c… ragelink 260 CONSTANCE_CONFIG_FIELDSETS = {
4ce269c… ragelink 261 "General": ("SITE_NAME",),
4ce269c… ragelink 262 "Fossil Storage": ("FOSSIL_DATA_DIR", "FOSSIL_STORE_IN_DB", "FOSSIL_S3_TRACKING", "FOSSIL_S3_BUCKET", "FOSSIL_BINARY_PATH"),
2eca4eb… ragelink 263 "Git Sync": ("GIT_SYNC_MODE", "GIT_SYNC_SCHEDULE", "GIT_MIRROR_DIR", "GIT_SSH_KEY_DIR"),
2eca4eb… ragelink 264 "GitHub OAuth": ("GITHUB_OAUTH_CLIENT_ID", "GITHUB_OAUTH_CLIENT_SECRET"),
2eca4eb… ragelink 265 "GitLab OAuth": ("GITLAB_OAUTH_CLIENT_ID", "GITLAB_OAUTH_CLIENT_SECRET"),
0c354ac… ragelink 266 "Cloudflare Turnstile": ("TURNSTILE_ENABLED", "TURNSTILE_SITE_KEY", "TURNSTILE_SECRET_KEY"),
4ce269c… ragelink 267 }
4ce269c… ragelink 268
4ce269c… ragelink 269 # --- Sentry ---
4ce269c… ragelink 270
4ce269c… ragelink 271 SENTRY_DSN = env_str("SENTRY_DSN")
4ce269c… ragelink 272 if SENTRY_DSN:
4ce269c… ragelink 273 import sentry_sdk
4ce269c… ragelink 274
4ce269c… ragelink 275 sentry_sdk.init(dsn=SENTRY_DSN, traces_sample_rate=0.1)
4ce269c… ragelink 276
4ce269c… ragelink 277 # --- Logging ---
4ce269c… ragelink 278
4ce269c… ragelink 279 LOGGING = {
4ce269c… ragelink 280 "version": 1,
4ce269c… ragelink 281 "disable_existing_loggers": False,
4ce269c… ragelink 282 "handlers": {"console": {"class": "logging.StreamHandler"}},
4ce269c… ragelink 283 "root": {"handlers": ["console"], "level": "INFO"},
4ce269c… ragelink 284 "loggers": {
4ce269c… ragelink 285 "django": {"handlers": ["console"], "level": "INFO", "propagate": False},
4ce269c… ragelink 286 },
4ce269c… ragelink 287 }
4ce269c… ragelink 288
4ce269c… ragelink 289 # --- Import/Export ---
4ce269c… ragelink 290
4ce269c… ragelink 291 IMPORT_FORMATS = []
4ce269c… ragelink 292 EXPORT_FORMATS = []
4ce269c… ragelink 293
4ce269c… ragelink 294 # --- Admin ---
4ce269c… ragelink 295
4ce269c… ragelink 296 ADMIN_SITE_HEADER = "Fossilrepo"
4ce269c… ragelink 297 ADMIN_SITE_TITLE = f"Fossilrepo Admin {VERSION}"

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button