Adds django-browser-reload: runserver already restarts on Python changes, but the browser had to be refreshed by hand for every template or CSS edit. It also watches static/, so a Tailwind rebuild now refreshes the page on its own. Mounted only under DEBUG -- it injects a script into every HTML response and serves an open event stream, neither of which belongs in production; a test holds that line. Its endpoint is exempt from RequireMFAMiddleware, otherwise a not-yet-enrolled staff user has the stream redirected away and live reload dies on the MFA enrolment page, which is exactly a page we are restyling. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
50 lines
1.9 KiB
Python
50 lines
1.9 KiB
Python
"""Force MFA enrolment for privileged users.
|
|
|
|
Anyone who can change other people's data must have a second factor: Django
|
|
staff/superusers, and anyone holding an elevated ``ClubRole`` (ADMIN or EDITOR)
|
|
in *any* club. Regular members may enrol, but aren't forced to.
|
|
|
|
Enrolled users are challenged for their second factor by allauth at login; this
|
|
middleware only handles the other half — a privileged user who has never
|
|
enrolled is redirected to the MFA setup page until they do.
|
|
"""
|
|
|
|
from allauth.mfa.utils import is_mfa_enabled
|
|
from django.conf import settings
|
|
from django.shortcuts import redirect
|
|
from django.urls import reverse
|
|
|
|
from club.models import ClubRole
|
|
|
|
#: Paths a not-yet-enrolled user must still reach (to enrol, or to log out).
|
|
#: ``/__reload__/`` is django-browser-reload's event stream, which only exists
|
|
#: under DEBUG — without it, live reload dies on the enrolment page itself.
|
|
EXEMPT_PREFIXES = ("/accounts/", "/static/", "/media/", "/__reload__/")
|
|
|
|
ELEVATED_ROLES = (ClubRole.Roles.ADMIN, ClubRole.Roles.EDITOR)
|
|
|
|
|
|
def mfa_required_for(user) -> bool:
|
|
"""Privileged users must hold a second factor."""
|
|
if user.is_staff or user.is_superuser:
|
|
return True
|
|
return ClubRole.objects.filter(member__user=user, role__in=ELEVATED_ROLES).exists()
|
|
|
|
|
|
class RequireMFAMiddleware:
|
|
def __init__(self, get_response):
|
|
self.get_response = get_response
|
|
|
|
def __call__(self, request):
|
|
if self.needs_enrolment(request):
|
|
return redirect(reverse(settings.MFA_ENROLMENT_URL_NAME))
|
|
return self.get_response(request)
|
|
|
|
def needs_enrolment(self, request) -> bool:
|
|
user = getattr(request, "user", None)
|
|
if user is None or not user.is_authenticated:
|
|
return False
|
|
if request.path.startswith(EXEMPT_PREFIXES):
|
|
return False
|
|
return mfa_required_for(user) and not is_mfa_enabled(user)
|