1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 19:26:48 +00:00

feat: error pages that say what happened and what to do

The 403, 404 and 500 pages said "Oops!" and one generic sentence; a bad
request and a failed CSRF check fell back to Django's plain pages.

Every error page now shares templates/error.html: a card with the status
in a coloured badge with an icon, a heading in plain words, what happened
and what to do, and Back and Home buttons. It uses only theme-defined
Bootstrap classes, so it reads in the light and dark themes alike.

- 400: the request was not accepted; a new address belongs in ALLOWED_HOSTS.
- 403: the account lacks the permission; ask an administrator.
- 403 CSRF: the form's security check failed; reload and submit again,
  and where to look when it happens on every form.
- 404: the path, and that the object may be gone or hidden from the account.
- 500: a libvirt failure on a page says the host's data could not be
  loaded, under libvirt's message; any other failure says it was logged
  and shows the time to give an administrator.
This commit is contained in:
catborise 2026-10-29 17:20:05 +03:00
parent c3c84d780c
commit 10eb0d1c2e
8 changed files with 140 additions and 33 deletions

13
templates/400.html Normal file
View file

@ -0,0 +1,13 @@
{% extends "error.html" %}
{% load i18n %}
{% block title %}{% trans "Bad request" %}{% endblock %}
{% block code %}400{% endblock %}
{% block status_classes %}text-bg-warning{% endblock %}
{% block icon %}bi-exclamation-triangle{% endblock %}
{% block heading %}{% trans "Bad request" %}{% endblock %}
{% block text %}
<p class="mb-3">{% trans "The panel could not accept this request." %}</p>
<p class="text-body-secondary mb-0">{% trans "If the panel was opened at a new address, an administrator must add that address to ALLOWED_HOSTS." %}</p>
{% endblock %}

View file

@ -1,15 +1,12 @@
{% extends "base.html" %}
{% extends "error.html" %}
{% load i18n %}
{% block title %}{% trans "403" %}{% endblock %}
{% block content %}
<div class="row">
<div class="col-12 text-center">
<h1>{% trans 'Oops!'%}</h1>
<p class="lead">{% trans "403 Forbidden" %}</p>
{% block title %}{% trans "Access denied" %}{% endblock %}
{% block code %}403{% endblock %}
{% block status_classes %}text-bg-warning{% endblock %}
{% block icon %}bi-shield-lock{% endblock %}
{% block heading %}{% trans "Access denied" %}{% endblock %}
<p>{% trans "You do not have permission to access this page." %}</p>
<a class="btn btn-success" href="javascript:history.back()">&larr; {% trans 'Back'%}</a>
</div>
</div>
{% block text %}
<p class="mb-0">{% trans "Your account does not have the permission for this page or action. Ask an administrator for it." %}</p>
{% endblock %}

14
templates/403_csrf.html Normal file
View file

@ -0,0 +1,14 @@
{% extends "error.html" %}
{% load i18n %}
{% block title %}{% trans "Form could not be submitted" %}{% endblock %}
{% block code %}403{% endblock %}
{% block status_classes %}text-bg-warning{% endblock %}
{% block icon %}bi-shield-exclamation{% endblock %}
{% block heading %}{% trans "Form could not be submitted" %}{% endblock %}
{% block text %}
<p class="mb-3">{% trans "This submission was rejected because its security check failed. It usually happens when the page was open for a long time, or after signing in or out in another tab." %}</p>
<p class="fw-semibold mb-3">{% trans "Go back, reload the page and submit the form again." %}</p>
<p class="small text-body-secondary border-top pt-3 mb-0">{% trans "If it happens on every form, the browser may block cookies, or the panel's proxy or address settings may be wrong. The reason is written to the panel's log." %}</p>
{% endblock %}

View file

@ -1,15 +1,13 @@
{% extends "base.html" %}
{% extends "error.html" %}
{% load i18n %}
{% block title %}{% trans "404" %}{% endblock %}
{% block content %}
<div class="row">
<div class="col-12 text-center">
<h1>{% trans 'Oops!'%}</h1>
<p class="lead">{% trans "404 Not Found" %}</p>
{% block title %}{% trans "Page not found" %}{% endblock %}
{% block code %}404{% endblock %}
{% block status_classes %}text-bg-secondary{% endblock %}
{% block icon %}bi-file-earmark-x{% endblock %}
{% block heading %}{% trans "Page not found" %}{% endblock %}
<p>{% trans "The requested page was not found on this server." %}</p>
<a class="btn btn-success" href="javascript:history.back()">&larr; {% trans 'Back'%}</a>
</div>
</div>
{% block text %}
<p class="mb-3">{% blocktrans with path=request_path %}Nothing was found at <code>{{ path }}</code>, or your account cannot see it.{% endblocktrans %}</p>
<p class="text-body-secondary mb-0">{% trans "The virtual machine or host may have been deleted, or the link is wrong." %}</p>
{% endblock %}

View file

@ -1,15 +1,18 @@
{% extends "base.html" %}
{% extends "error.html" %}
{% load i18n %}
{% block title %}{% trans "500" %}{% endblock %}
{% block content %}
<div class="row">
<div class="col-12 text-center">
<h1>{% trans 'Oops!' %}</h1>
<p class="lead">{% trans "500 Internal Server Error" %}</p>
{% block title %}{% if libvirt_error %}{% trans "Could not load data from the host" %}{% else %}{% trans "Server error" %}{% endif %}{% endblock %}
{% block code %}500{% endblock %}
{% block status_classes %}text-bg-danger{% endblock %}
{% block icon %}{% if libvirt_error %}bi-hdd-network{% else %}bi-exclamation-octagon{% endif %}{% endblock %}
{% block heading %}{% if libvirt_error %}{% trans "Could not load data from the host" %}{% else %}{% trans "Server error" %}{% endif %}{% endblock %}
<p>{% trans "The server encountered an internal error or misconfiguration and was unable to complete you request." %}</p>
<a class="btn btn-success" href="javascript:history.back()">&larr; {% trans 'Back'%}</a>
</div>
</div>
{% block text %}
{% if libvirt_error %}
<p class="mb-3">{% trans "This page needs data from the host, and libvirt returned the error shown above." %}</p>
<p class="text-body-secondary mb-0">{% trans "Check that the host is running and reachable, then reload the page." %}</p>
{% else %}
<p class="mb-3">{% trans "The panel failed while handling this request. This is a fault in the panel, not something you did, and it was written to the panel's log." %}</p>
<p class="text-body-secondary mb-0">{% now "Y-m-d H:i:s" as now %}{% blocktrans %}Reload the page to try again. If it keeps happening, tell an administrator the time: {{ now }}.{% endblocktrans %}</p>
{% endif %}
{% endblock %}

39
templates/error.html Normal file
View file

@ -0,0 +1,39 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<div class="row justify-content-center py-3 py-sm-4">
<div class="col-12 col-lg-10">
<section class="card shadow-sm" aria-labelledby="error-heading">
<div class="card-body p-4 p-sm-5">
<div class="mb-3">
<span class="badge d-inline-flex align-items-center gap-2 rounded-pill fs-6 px-3 py-2 {% block status_classes %}text-bg-secondary{% endblock %}">
<i class="bi {% block icon %}bi-exclamation-circle{% endblock %}" aria-hidden="true"></i>
<span class="font-monospace">
<span class="visually-hidden">{% trans "HTTP status" %} </span>
{% block code %}{% endblock %}
</span>
</span>
</div>
<h1 id="error-heading" class="h3 mb-3">{% block heading %}{% endblock %}</h1>
<div class="text-break">
{% block text %}{% endblock %}
</div>
<div class="d-grid d-sm-flex gap-2 mt-4">
<a class="btn btn-primary" href="javascript:history.back()">
<i class="bi bi-arrow-left me-1" aria-hidden="true"></i>
{% trans "Back" %}
</a>
<a class="btn btn-secondary" href="{% url 'index' %}">
<i class="bi bi-house me-1" aria-hidden="true"></i>
{% trans "Home" %}
</a>
</div>
</div>
</section>
</div>
</div>
{% endblock %}

View file

@ -66,6 +66,8 @@ class ConnectionErrorPageTests(TestCase):
response, messages = self.handle(self.admin, libvirt_error(libvirt.VIR_FROM_RPC, HOST_DETAIL),
path="/instances/1/", method="get")
self.assertEqual(response.status_code, 500)
self.assertIn(b"Could not load data from the host", response.content)
self.assertIn(HOST_DETAIL.encode(), response.content)
self.assertTrue(messages)
def test_administrators_see_connection_details(self):

View file

@ -0,0 +1,41 @@
"""The error pages say what happened and what to do, in the panel's layout."""
from django.contrib.auth import get_user_model
from django.http import Http404
from django.test import Client, RequestFactory, TestCase
from django.views.defaults import page_not_found, server_error
class ErrorPageTests(TestCase):
def setUp(self):
self.user = get_user_model().objects.create_user("ep-user", password="pw")
self.client.force_login(self.user)
def test_a_bad_request(self):
response = self.client.get("/", HTTP_HOST="unknown.example")
self.assertContains(response, "Bad request", status_code=400)
self.assertContains(response, "ALLOWED_HOSTS", status_code=400)
def test_a_page_the_user_may_not_open(self):
response = self.client.get("/computes/")
self.assertContains(response, "Access denied", status_code=403)
self.assertContains(response, "Ask an administrator", status_code=403)
def test_a_form_that_fails_its_security_check(self):
response = Client(enforce_csrf_checks=True).post("/accounts/login/", {"username": "ep-user", "password": "pw"})
self.assertContains(response, "Form could not be submitted", status_code=403)
self.assertContains(response, "reload the page and submit the form again", status_code=403)
def test_a_page_that_is_not_there_names_its_path(self):
response = self.client.get("/no/such/page/")
self.assertContains(response, "Page not found", status_code=404)
self.assertContains(response, "<code>/no/such/page/</code>", status_code=404)
def test_the_reason_of_a_not_found_is_not_shown(self):
response = page_not_found(RequestFactory().get("/datasource/"), Http404("libvirt detail of the host"))
self.assertNotIn(b"libvirt detail", response.content)
def test_a_server_error_renders_without_a_request_context(self):
response = server_error(RequestFactory().get("/"))
self.assertContains(response, "Server error", status_code=500)
self.assertContains(response, "tell an administrator the time", status_code=500)