diff --git a/apps/accounts/middleware.py b/apps/accounts/middleware.py new file mode 100644 index 00000000..372cf116 --- /dev/null +++ b/apps/accounts/middleware.py @@ -0,0 +1,35 @@ +import base64 +import binascii + +from django.contrib.auth import authenticate +from django.http import JsonResponse +from django.utils.deprecation import MiddlewareMixin +from django.utils.encoding import smart_str + + +class BasicAuthenticationMiddleware(MiddlewareMixin): + @staticmethod + def process_request(request): + if "HTTP_AUTHORIZATION" in request.META: + auth_parts = request.META.get("HTTP_AUTHORIZATION", "").split() + auth_error = "Unauthorized" + + if not auth_parts or len(auth_parts) != 2 or auth_parts[0].lower() != "basic": + return JsonResponse({"error": "Unauthorized"}, status=401) + else: + username_password = smart_str(auth_parts[1]) + try: + username_password = base64.b64decode(username_password).decode("utf8") + username, password = username_password.split(":", 1) + user = authenticate(request, username=username, password=password) + except binascii.Error: + auth_error = ( + "Invalid basic header: Username and password must be base64 encoded with a colon delimiter " + "(e.g. 'username:password')." + ) + user = None + + if user is not None: + request.user = user + else: + return JsonResponse({"error": auth_error}, status=401) diff --git a/apps/base/views.py b/apps/base/views.py index 16d4c24f..0a9e7515 100644 --- a/apps/base/views.py +++ b/apps/base/views.py @@ -1,5 +1,13 @@ +import json + +from django.contrib.auth.mixins import UserPassesTestMixin +from django.http import JsonResponse from django.shortcuts import render +from django.utils.decorators import method_decorator from django.views import generic +from django.views.decorators.csrf import csrf_exempt + +from maintenance_mode.core import set_maintenance_mode class IndexView(generic.TemplateView): @@ -12,3 +20,25 @@ def http_500(request): def http_404(request): return render(request, "404.html") + + +class ToggleMaintenanceModeView(UserPassesTestMixin, generic.View): + raise_exception = True + # curl -X POST -H "Content-Type: application/json" -d '{"maintenance_mode": "off"}' -u "email:password" http://127.0.0.1:8000/maintenance-mode/ + + def test_func(self): + return self.request.user.is_superuser + + @method_decorator(csrf_exempt) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) + + def post(self, request, *args, **kwargs): + data = json.loads(self.request.body.decode("utf-8")) + target_mode = data.get("maintenance_mode", None) + if target_mode is None or target_mode not in ("on", "off"): + return JsonResponse({"error": "Invalid request: maintenance_mode must be 'on' or 'off'"}, status=400) + + target_mode = True if target_mode == "on" else False + set_maintenance_mode(target_mode) + return JsonResponse({"maintenance_mode": target_mode}) diff --git a/config/settings/_base.py b/config/settings/_base.py index 261d14d8..4d853d10 100644 --- a/config/settings/_base.py +++ b/config/settings/_base.py @@ -66,6 +66,7 @@ "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", + "apps.accounts.middleware.BasicAuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", "maintenance_mode.middleware.MaintenanceModeMiddleware", @@ -299,3 +300,4 @@ # MAINTENANCE MODE SETTINGS MAINTENANCE_MODE_STATE_BACKEND = "maintenance_mode.backends.CacheBackend" MAINTENANCE_MODE_STATE_BACKEND_FALLBACK_VALUE = True +MAINTENANCE_MODE_IGNORE_URLS = ("/maintenance-mode/",) diff --git a/config/urls.py b/config/urls.py index 45f56009..84942c53 100755 --- a/config/urls.py +++ b/config/urls.py @@ -6,7 +6,7 @@ from django.views.generic import TemplateView from apps.accounts.views import NameChange -from apps.base.views import http_404, http_500 +from apps.base.views import ToggleMaintenanceModeView, http_404, http_500 # Includes urlpatterns: list[Union[URLResolver, URLPattern]] = [path(r"admin/", admin.site.urls)] @@ -19,6 +19,7 @@ path("404/", http_404), path("accounts/name/", NameChange.as_view(), name="account_change_name"), path("accounts/", include("allauth.urls")), + path("maintenance-mode/", ToggleMaintenanceModeView.as_view(), name="maintenance_mode"), ] # Debug/Development URLs