Add a branded 403 page instead of Django's bare error page
templates/403.html is picked up automatically by Django's default permission_denied handler -- no urls.py wiring needed. Rendered through the tenant's own skin (base_template, same as maintenance.html), so a permission error still looks like the app and the navbar (sign out, theme toggle, home link) stays reachable instead of leaving the user stuck on a dead end.
This commit is contained in:
@@ -1043,6 +1043,40 @@ class BrandingTests(TestCase):
|
||||
self.assertNotContains(self.login_page("ajax-united.rosterchief.app"), "--color-secondary")
|
||||
|
||||
|
||||
@override_settings(
|
||||
ROSTERCHIEF_BASE_DOMAIN="rosterchief.app",
|
||||
ALLOWED_HOSTS=["rosterchief.app", "ajax-united.rosterchief.app", "testserver"],
|
||||
)
|
||||
class Custom403PageTests(TestCase):
|
||||
"""Django's default 403 handler picks up templates/403.html automatically --
|
||||
branded per tenant (base_template, same as maintenance.html) so a permission
|
||||
error still looks like the app, not a bare Django error page, and the navbar
|
||||
(sign out, theme toggle, home link) stays reachable."""
|
||||
|
||||
def setUp(self):
|
||||
self.club = Club.objects.create(name="Ajax United", slug="ajax-united")
|
||||
|
||||
def test_a_club_subdomain_403_gets_the_club_skin(self):
|
||||
member = get_user_model().objects.create_user(email="member-403@example.com", password="pw-secret-123")
|
||||
self.client.force_login(member)
|
||||
|
||||
response = self.client.get(reverse("management:position_list"), HTTP_HOST="ajax-united.rosterchief.app")
|
||||
|
||||
self.assertEqual(response.status_code, 403)
|
||||
self.assertContains(response, "Access denied", status_code=403)
|
||||
self.assertContains(response, "Ajax United", status_code=403)
|
||||
self.assertContains(response, "Sign out", status_code=403)
|
||||
|
||||
def test_the_base_domain_403_gets_the_platform_skin(self):
|
||||
self.client.force_login(get_user_model().objects.create_user(email="platform-403@example.com", password="pw-secret-123"))
|
||||
|
||||
response = self.client.get(reverse("controlpanel:dashboard"))
|
||||
|
||||
self.assertEqual(response.status_code, 403)
|
||||
self.assertContains(response, "Access denied", status_code=403)
|
||||
self.assertContains(response, "Club & Team Management", status_code=403)
|
||||
|
||||
|
||||
class ClubBrandingModelTests(TestCase):
|
||||
def test_initials_use_the_first_two_words(self):
|
||||
self.assertEqual(Club(name="Ajax United Football Club").initials, "AU")
|
||||
|
||||
26
templates/403.html
Normal file
26
templates/403.html
Normal file
@@ -0,0 +1,26 @@
|
||||
{% extends base_template %}
|
||||
{% load i18n lucide %}
|
||||
|
||||
{% comment %}
|
||||
Django's default 403 handler (django.views.defaults.permission_denied) looks up
|
||||
this exact filename automatically -- no urls.py wiring needed. Rendered through
|
||||
the tenant's own skin (base_template, from club.context_processors.branding),
|
||||
same reasoning as maintenance.html: a club sees its own logo and colours, not a
|
||||
bare error page, and the navbar (sign out, theme toggle, ...) stays reachable
|
||||
since {% block main %} is the only thing this page actually replaces.
|
||||
{% endcomment %}
|
||||
|
||||
{% block head_title %}{% trans "Access denied" %}{% endblock head_title %}
|
||||
|
||||
{% block main %}
|
||||
<div class="flex justify-center">
|
||||
<div class="card w-full max-w-xl bg-base-100 shadow">
|
||||
<div class="card-body items-center text-center">
|
||||
<div class="text-error">{% lucide "shield-x" size=48 %}</div>
|
||||
<h1 class="card-title mt-2">{% trans "Access denied" %}</h1>
|
||||
<p class="opacity-80">{% trans "You don't have permission to view this page." %}</p>
|
||||
<a class="btn btn-primary mt-4 gap-2" href="/">{% lucide "house" size=16 %} {% trans "Go to home" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock main %}
|
||||
Reference in New Issue
Block a user