We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Lock admin panel behind 2FA
### What?
<!-- A clear and concise high-level description of what you want to happen. -->
lock the admin panel behind the 2FA functionality

### Why?
<!-- A clear and concise motivation why we should consider implementing this. -->
Admin panel has sensitive data so it should be protected. So requiring 2FA makes sense.

### How?
<!-- Optionally some guidance, ideas, context. -->
Probably nice to have a decorator to be able to lock other things of the site behind 2FA in the future.



--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `website/thaliawebsite/admin.py`
Content:
```
1 """Settings for the admin site."""
2 from django.contrib import admin
3 from django.utils.translation import gettext_lazy as _
4 
5 admin.site.site_header = _("Thalia administration")
6 admin.site.site_title = _("Thalia")
7
```

Path: `website/thaliawebsite/views.py`
Content:
```
1 """General views for the website."""
2 
3 from django.contrib.admin.views.decorators import staff_member_required
4 from django.contrib.auth.views import LogoutView as BaseLogoutView
5 from django.contrib.auth.views import PasswordResetView
6 from django.core.exceptions import PermissionDenied
7 from django.http import HttpResponse, HttpResponseForbidden
8 from django.shortcuts import redirect
9 from django.utils.decorators import method_decorator
10 from django.views.generic import ListView, TemplateView
11 from django.views.generic.base import View
12 
13 from django_ratelimit.decorators import ratelimit
14 from two_factor.views import LoginView
15 
16 
17 class IndexView(TemplateView):
18     template_name = "index.html"
19 
20 
21 @method_decorator(staff_member_required, "dispatch")
22 class TestCrashView(View):
23     """Test view to intentionally crash to test the error handling."""
24 
25     def dispatch(self, request, *args, **kwargs) -> HttpResponse:
26         if not request.user.is_superuser:
27             return HttpResponseForbidden("This is not for you")
28         raise Exception("Test exception")
29 
30 
31 class PagedView(ListView):
32     """A ListView with automatic pagination."""
33 
34     def get_context_data(self, **kwargs) -> dict:
35         context = super().get_context_data(**kwargs)
36         page = context["page_obj"].number
37         paginator = context["paginator"]
38 
39         # Show the two pages before and after the current page
40         page_range_start = max(1, page - 2)
41         page_range_stop = min(page + 3, paginator.num_pages + 1)
42 
43         # Add extra pages if we show less than 5 pages
44         page_range_start = min(page_range_start, page_range_stop - 5)
45         page_range_start = max(1, page_range_start)
46 
47         # Add extra pages if we still show less than 5 pages
48         page_range_stop = max(page_range_stop, page_range_start + 5)
49         page_range_stop = min(page_range_stop, paginator.num_pages + 1)
50 
51         page_range = range(page_range_start, page_range_stop)
52 
53         querydict = self.request.GET.copy()
54 
55         if "page" in querydict:
56             del querydict["page"]
57 
58         context.update(
59             {
60                 "page_range": page_range,
61                 "base_url": f"{self.request.path}?{querydict.urlencode()}&"
62                 if querydict
63                 else f"{self.request.path}?",
64             }
65         )
66 
67         return context
68 
69 
70 class RateLimitedPasswordResetView(PasswordResetView):
71     @method_decorator(ratelimit(key="ip", rate="5/h"))
72     def post(self, request, *args, **kwargs):
73         return super().post(request, *args, **kwargs)
74 
75 
76 class RateLimitedLoginView(LoginView):
77     @method_decorator(ratelimit(key="ip", rate="30/h"))
78     @method_decorator(ratelimit(key="post:username", rate="30/h"))
79     def post(self, request, *args, **kwargs):
80         return super().post(request, *args, **kwargs)
81 
82 
83 class LogoutView(BaseLogoutView):
84     # Allow GET logout still (this was deprecated in Django 5.0).
85     http_method_names = ["get", "post", "options"]
86 
87     def get(self, request, *args, **kwargs):
88         return self.post(request, *args, **kwargs)
89 
90 
91 def rate_limited_view(request, *args, **kwargs):
92     return HttpResponse("You are rate limited", status=429)
93 
94 
95 def admin_unauthorized_view(request):
96     if not request.member:
97         url = "/user/account/login"
98         args = request.META.get("QUERY_STRING", "")
99         if args:
100             url = f"{url}?{args}"
101         return redirect(url)
102     elif not request.member.is_staff and not request.member.is_superuser:
103         raise PermissionDenied("You are not allowed to access the administration page.")
104     else:
105         return redirect(request.GET.get("next", "/"))
106
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/website/thaliawebsite/admin.py b/website/thaliawebsite/admin.py
--- a/website/thaliawebsite/admin.py
+++ b/website/thaliawebsite/admin.py
@@ -1,6 +1,17 @@
 """Settings for the admin site."""
+
+from django.conf import settings
 from django.contrib import admin
 from django.utils.translation import gettext_lazy as _
 
-admin.site.site_header = _("Thalia administration")
-admin.site.site_title = _("Thalia")
+from django_otp import user_has_device
+
+
+class ThaliaAdminSite(admin.AdminSite):
+    site_header = _("Thalia administration")
+    site_title = _("Thalia")
+
+    def has_permission(self, request):
+        return super().has_permission(request) and (
+            settings.DEBUG or user_has_device(request.user)
+        )
diff --git a/website/thaliawebsite/views.py b/website/thaliawebsite/views.py
--- a/website/thaliawebsite/views.py
+++ b/website/thaliawebsite/views.py
@@ -1,5 +1,6 @@
 """General views for the website."""
 
+from django.contrib import messages
 from django.contrib.admin.views.decorators import staff_member_required
 from django.contrib.auth.views import LogoutView as BaseLogoutView
 from django.contrib.auth.views import PasswordResetView
@@ -10,6 +11,7 @@
 from django.views.generic import ListView, TemplateView
 from django.views.generic.base import View
 
+from django_otp import user_has_device
 from django_ratelimit.decorators import ratelimit
 from two_factor.views import LoginView
 
@@ -58,9 +60,11 @@
         context.update(
             {
                 "page_range": page_range,
-                "base_url": f"{self.request.path}?{querydict.urlencode()}&"
-                if querydict
-                else f"{self.request.path}?",
+                "base_url": (
+                    f"{self.request.path}?{querydict.urlencode()}&"
+                    if querydict
+                    else f"{self.request.path}?"
+                ),
             }
         )
 
@@ -101,5 +105,11 @@
         return redirect(url)
     elif not request.member.is_staff and not request.member.is_superuser:
         raise PermissionDenied("You are not allowed to access the administration page.")
+    elif not user_has_device(request.member):
+        messages.error(
+            request,
+            "You need to set up two-factor authentication to access the administration page.",
+        )
+        return redirect("two_factor:setup")
     else:
         return redirect(request.GET.get("next", "/"))

{"golden_diff": "diff --git a/website/thaliawebsite/admin.py b/website/thaliawebsite/admin.py\n--- a/website/thaliawebsite/admin.py\n+++ b/website/thaliawebsite/admin.py\n@@ -1,6 +1,17 @@\n \"\"\"Settings for the admin site.\"\"\"\n+\n+from django.conf import settings\n from django.contrib import admin\n from django.utils.translation import gettext_lazy as _\n \n-admin.site.site_header = _(\"Thalia administration\")\n-admin.site.site_title = _(\"Thalia\")\n+from django_otp import user_has_device\n+\n+\n+class ThaliaAdminSite(admin.AdminSite):\n+    site_header = _(\"Thalia administration\")\n+    site_title = _(\"Thalia\")\n+\n+    def has_permission(self, request):\n+        return super().has_permission(request) and (\n+            settings.DEBUG or user_has_device(request.user)\n+        )\ndiff --git a/website/thaliawebsite/views.py b/website/thaliawebsite/views.py\n--- a/website/thaliawebsite/views.py\n+++ b/website/thaliawebsite/views.py\n@@ -1,5 +1,6 @@\n \"\"\"General views for the website.\"\"\"\n \n+from django.contrib import messages\n from django.contrib.admin.views.decorators import staff_member_required\n from django.contrib.auth.views import LogoutView as BaseLogoutView\n from django.contrib.auth.views import PasswordResetView\n@@ -10,6 +11,7 @@\n from django.views.generic import ListView, TemplateView\n from django.views.generic.base import View\n \n+from django_otp import user_has_device\n from django_ratelimit.decorators import ratelimit\n from two_factor.views import LoginView\n \n@@ -58,9 +60,11 @@\n         context.update(\n             {\n                 \"page_range\": page_range,\n-                \"base_url\": f\"{self.request.path}?{querydict.urlencode()}&\"\n-                if querydict\n-                else f\"{self.request.path}?\",\n+                \"base_url\": (\n+                    f\"{self.request.path}?{querydict.urlencode()}&\"\n+                    if querydict\n+                    else f\"{self.request.path}?\"\n+                ),\n             }\n         )\n \n@@ -101,5 +105,11 @@\n         return redirect(url)\n     elif not request.member.is_staff and not request.member.is_superuser:\n         raise PermissionDenied(\"You are not allowed to access the administration page.\")\n+    elif not user_has_device(request.member):\n+        messages.error(\n+            request,\n+            \"You need to set up two-factor authentication to access the administration page.\",\n+        )\n+        return redirect(\"two_factor:setup\")\n     else:\n         return redirect(request.GET.get(\"next\", \"/\"))\n", "issue": "Lock admin panel behind 2FA\n### What?\r\n<!-- A clear and concise high-level description of what you want to happen. -->\r\nlock the admin panel behind the 2FA functionality\r\n\r\n### Why?\r\n<!-- A clear and concise motivation why we should consider implementing this. -->\r\nAdmin panel has sensitive data so it should be protected. So requiring 2FA makes sense.\r\n\r\n### How?\r\n<!-- Optionally some guidance, ideas, context. -->\r\nProbably nice to have a decorator to be able to lock other things of the site behind 2FA in the future.\r\n\r\n\n", "before_files": [{"content": "\"\"\"Settings for the admin site.\"\"\"\nfrom django.contrib import admin\nfrom django.utils.translation import gettext_lazy as _\n\nadmin.site.site_header = _(\"Thalia administration\")\nadmin.site.site_title = _(\"Thalia\")\n", "path": "website/thaliawebsite/admin.py"}, {"content": "\"\"\"General views for the website.\"\"\"\n\nfrom django.contrib.admin.views.decorators import staff_member_required\nfrom django.contrib.auth.views import LogoutView as BaseLogoutView\nfrom django.contrib.auth.views import PasswordResetView\nfrom django.core.exceptions import PermissionDenied\nfrom django.http import HttpResponse, HttpResponseForbidden\nfrom django.shortcuts import redirect\nfrom django.utils.decorators import method_decorator\nfrom django.views.generic import ListView, TemplateView\nfrom django.views.generic.base import View\n\nfrom django_ratelimit.decorators import ratelimit\nfrom two_factor.views import LoginView\n\n\nclass IndexView(TemplateView):\n    template_name = \"index.html\"\n\n\n@method_decorator(staff_member_required, \"dispatch\")\nclass TestCrashView(View):\n    \"\"\"Test view to intentionally crash to test the error handling.\"\"\"\n\n    def dispatch(self, request, *args, **kwargs) -> HttpResponse:\n        if not request.user.is_superuser:\n            return HttpResponseForbidden(\"This is not for you\")\n        raise Exception(\"Test exception\")\n\n\nclass PagedView(ListView):\n    \"\"\"A ListView with automatic pagination.\"\"\"\n\n    def get_context_data(self, **kwargs) -> dict:\n        context = super().get_context_data(**kwargs)\n        page = context[\"page_obj\"].number\n        paginator = context[\"paginator\"]\n\n        # Show the two pages before and after the current page\n        page_range_start = max(1, page - 2)\n        page_range_stop = min(page + 3, paginator.num_pages + 1)\n\n        # Add extra pages if we show less than 5 pages\n        page_range_start = min(page_range_start, page_range_stop - 5)\n        page_range_start = max(1, page_range_start)\n\n        # Add extra pages if we still show less than 5 pages\n        page_range_stop = max(page_range_stop, page_range_start + 5)\n        page_range_stop = min(page_range_stop, paginator.num_pages + 1)\n\n        page_range = range(page_range_start, page_range_stop)\n\n        querydict = self.request.GET.copy()\n\n        if \"page\" in querydict:\n            del querydict[\"page\"]\n\n        context.update(\n            {\n                \"page_range\": page_range,\n                \"base_url\": f\"{self.request.path}?{querydict.urlencode()}&\"\n                if querydict\n                else f\"{self.request.path}?\",\n            }\n        )\n\n        return context\n\n\nclass RateLimitedPasswordResetView(PasswordResetView):\n    @method_decorator(ratelimit(key=\"ip\", rate=\"5/h\"))\n    def post(self, request, *args, **kwargs):\n        return super().post(request, *args, **kwargs)\n\n\nclass RateLimitedLoginView(LoginView):\n    @method_decorator(ratelimit(key=\"ip\", rate=\"30/h\"))\n    @method_decorator(ratelimit(key=\"post:username\", rate=\"30/h\"))\n    def post(self, request, *args, **kwargs):\n        return super().post(request, *args, **kwargs)\n\n\nclass LogoutView(BaseLogoutView):\n    # Allow GET logout still (this was deprecated in Django 5.0).\n    http_method_names = [\"get\", \"post\", \"options\"]\n\n    def get(self, request, *args, **kwargs):\n        return self.post(request, *args, **kwargs)\n\n\ndef rate_limited_view(request, *args, **kwargs):\n    return HttpResponse(\"You are rate limited\", status=429)\n\n\ndef admin_unauthorized_view(request):\n    if not request.member:\n        url = \"/user/account/login\"\n        args = request.META.get(\"QUERY_STRING\", \"\")\n        if args:\n            url = f\"{url}?{args}\"\n        return redirect(url)\n    elif not request.member.is_staff and not request.member.is_superuser:\n        raise PermissionDenied(\"You are not allowed to access the administration page.\")\n    else:\n        return redirect(request.GET.get(\"next\", \"/\"))\n", "path": "website/thaliawebsite/views.py"}], "after_files": [{"content": "\"\"\"Settings for the admin site.\"\"\"\n\nfrom django.conf import settings\nfrom django.contrib import admin\nfrom django.utils.translation import gettext_lazy as _\n\nfrom django_otp import user_has_device\n\n\nclass ThaliaAdminSite(admin.AdminSite):\n    site_header = _(\"Thalia administration\")\n    site_title = _(\"Thalia\")\n\n    def has_permission(self, request):\n        return super().has_permission(request) and (\n            settings.DEBUG or user_has_device(request.user)\n        )\n", "path": "website/thaliawebsite/admin.py"}, {"content": "\"\"\"General views for the website.\"\"\"\n\nfrom django.contrib import messages\nfrom django.contrib.admin.views.decorators import staff_member_required\nfrom django.contrib.auth.views import LogoutView as BaseLogoutView\nfrom django.contrib.auth.views import PasswordResetView\nfrom django.core.exceptions import PermissionDenied\nfrom django.http import HttpResponse, HttpResponseForbidden\nfrom django.shortcuts import redirect\nfrom django.utils.decorators import method_decorator\nfrom django.views.generic import ListView, TemplateView\nfrom django.views.generic.base import View\n\nfrom django_otp import user_has_device\nfrom django_ratelimit.decorators import ratelimit\nfrom two_factor.views import LoginView\n\n\nclass IndexView(TemplateView):\n    template_name = \"index.html\"\n\n\n@method_decorator(staff_member_required, \"dispatch\")\nclass TestCrashView(View):\n    \"\"\"Test view to intentionally crash to test the error handling.\"\"\"\n\n    def dispatch(self, request, *args, **kwargs) -> HttpResponse:\n        if not request.user.is_superuser:\n            return HttpResponseForbidden(\"This is not for you\")\n        raise Exception(\"Test exception\")\n\n\nclass PagedView(ListView):\n    \"\"\"A ListView with automatic pagination.\"\"\"\n\n    def get_context_data(self, **kwargs) -> dict:\n        context = super().get_context_data(**kwargs)\n        page = context[\"page_obj\"].number\n        paginator = context[\"paginator\"]\n\n        # Show the two pages before and after the current page\n        page_range_start = max(1, page - 2)\n        page_range_stop = min(page + 3, paginator.num_pages + 1)\n\n        # Add extra pages if we show less than 5 pages\n        page_range_start = min(page_range_start, page_range_stop - 5)\n        page_range_start = max(1, page_range_start)\n\n        # Add extra pages if we still show less than 5 pages\n        page_range_stop = max(page_range_stop, page_range_start + 5)\n        page_range_stop = min(page_range_stop, paginator.num_pages + 1)\n\n        page_range = range(page_range_start, page_range_stop)\n\n        querydict = self.request.GET.copy()\n\n        if \"page\" in querydict:\n            del querydict[\"page\"]\n\n        context.update(\n            {\n                \"page_range\": page_range,\n                \"base_url\": (\n                    f\"{self.request.path}?{querydict.urlencode()}&\"\n                    if querydict\n                    else f\"{self.request.path}?\"\n                ),\n            }\n        )\n\n        return context\n\n\nclass RateLimitedPasswordResetView(PasswordResetView):\n    @method_decorator(ratelimit(key=\"ip\", rate=\"5/h\"))\n    def post(self, request, *args, **kwargs):\n        return super().post(request, *args, **kwargs)\n\n\nclass RateLimitedLoginView(LoginView):\n    @method_decorator(ratelimit(key=\"ip\", rate=\"30/h\"))\n    @method_decorator(ratelimit(key=\"post:username\", rate=\"30/h\"))\n    def post(self, request, *args, **kwargs):\n        return super().post(request, *args, **kwargs)\n\n\nclass LogoutView(BaseLogoutView):\n    # Allow GET logout still (this was deprecated in Django 5.0).\n    http_method_names = [\"get\", \"post\", \"options\"]\n\n    def get(self, request, *args, **kwargs):\n        return self.post(request, *args, **kwargs)\n\n\ndef rate_limited_view(request, *args, **kwargs):\n    return HttpResponse(\"You are rate limited\", status=429)\n\n\ndef admin_unauthorized_view(request):\n    if not request.member:\n        url = \"/user/account/login\"\n        args = request.META.get(\"QUERY_STRING\", \"\")\n        if args:\n            url = f\"{url}?{args}\"\n        return redirect(url)\n    elif not request.member.is_staff and not request.member.is_superuser:\n        raise PermissionDenied(\"You are not allowed to access the administration page.\")\n    elif not user_has_device(request.member):\n        messages.error(\n            request,\n            \"You need to set up two-factor authentication to access the administration page.\",\n        )\n        return redirect(\"two_factor:setup\")\n    else:\n        return redirect(request.GET.get(\"next\", \"/\"))\n", "path": "website/thaliawebsite/views.py"}]}