FossilRepo

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

Keyboard Shortcuts

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