diff --git a/dune_processes/settings/settings.py b/dune_processes/settings/settings.py
index 98764f5..169bace 100644
--- a/dune_processes/settings/settings.py
+++ b/dune_processes/settings/settings.py
@@ -131,5 +131,8 @@
AUTH_USER_MODEL = "main.User"
+LOGIN_URL = "main:login"
+LOGIN_REDIRECT_URL = "main:index"
+
INSTALLED_APPS += ["django_bootstrap5"]
DJANGO_TABLES2_TEMPLATE = "django_tables2/bootstrap5.html"
diff --git a/main/templates/registration/login.html b/main/templates/registration/login.html
new file mode 100644
index 0000000..417c5cc
--- /dev/null
+++ b/main/templates/registration/login.html
@@ -0,0 +1,37 @@
+{% extends "../main/base.html" %}
+
+{% block content %}
+
+ {% if form.errors %}
+
Your username and password didn't match. Please try again.
+ {% endif %}
+
+ {% if next %}
+ {% if user.is_authenticated %}
+ Your account doesn't have access to this page. To proceed,
+ please login with an account that has access.
+ {% else %}
+ Please login to see this page.
+ {% endif %}
+ {% endif %}
+
+
+
+ {# Assumes you set up the password_reset view in your URLconf #}
+ Lost password?
+
+{% endblock %}
diff --git a/main/urls.py b/main/urls.py
index 2703ba6..8579501 100644
--- a/main/urls.py
+++ b/main/urls.py
@@ -1,12 +1,13 @@
"""Urls module for the main app."""
-from django.urls import path
+from django.urls import include, path
from . import views
app_name = "main"
urlpatterns = [
path("", views.index, name="index"),
+ path("accounts/", include("django.contrib.auth.urls")),
path("restart/", views.restart_process, name="restart"),
path("kill/", views.kill_process, name="kill"),
path("flush/", views.flush_process, name="flush"),
diff --git a/main/views.py b/main/views.py
index 3f9c7ae..4ea9d56 100644
--- a/main/views.py
+++ b/main/views.py
@@ -5,6 +5,8 @@
from enum import Enum
import django_tables2
+from django.contrib.auth.decorators import login_required
+from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
from django.shortcuts import render
from django.urls import reverse, reverse_lazy
@@ -36,6 +38,7 @@ async def get_session_info() -> ProcessInstanceList:
return await pmd.ps(query)
+@login_required
def index(request: HttpRequest) -> HttpResponse:
"""View that renders the index/home page."""
val = asyncio.run(get_session_info())
@@ -96,6 +99,7 @@ async def _process_call(uuid: str, action: ProcessAction) -> None:
await pmd.flush(query)
+@login_required
def restart_process(request: HttpRequest, uuid: uuid.UUID) -> HttpResponse:
"""Restart the process associated to the given UUID.
@@ -111,6 +115,7 @@ def restart_process(request: HttpRequest, uuid: uuid.UUID) -> HttpResponse:
return HttpResponseRedirect(reverse("main:index"))
+@login_required
def kill_process(request: HttpRequest, uuid: uuid.UUID) -> HttpResponse:
"""Kill the process associated to the given UUID.
@@ -125,6 +130,7 @@ def kill_process(request: HttpRequest, uuid: uuid.UUID) -> HttpResponse:
return HttpResponseRedirect(reverse("main:index"))
+@login_required
def flush_process(request: HttpRequest, uuid: uuid.UUID) -> HttpResponse:
"""Flush the process associated to the given UUID.
@@ -154,6 +160,7 @@ async def _get_process_logs(uuid: str) -> list[DecodedResponse]:
return [item async for item in pmd.logs(request)]
+@login_required
def logs(request: HttpRequest, uuid: uuid.UUID) -> HttpResponse:
"""Display the logs of a process.
@@ -181,7 +188,7 @@ async def _boot_process(user: str, data: dict[str, str | int]) -> None:
pass
-class BootProcessView(FormView): # type: ignore [type-arg]
+class BootProcessView(LoginRequiredMixin, FormView): # type: ignore [type-arg]
"""View for the BootProcess form."""
template_name = "main/boot_process.html"
diff --git a/tests/conftest.py b/tests/conftest.py
index cb6cc72..9fbbe5e 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,4 +1,16 @@
+"""Configuration for pytest."""
+
import pytest
+from django.test import Client
+
+
+@pytest.fixture
+def auth_client(django_user_model) -> Client:
+ """Return an authenticated client."""
+ user = django_user_model.objects.create(username="testuser")
+ client = Client()
+ client.force_login(user)
+ return client
@pytest.fixture
diff --git a/tests/test_views.py b/tests/test_views.py
index 8265ef9..7a8c874 100644
--- a/tests/test_views.py
+++ b/tests/test_views.py
@@ -3,42 +3,67 @@
import pytest
from django.urls import reverse
-from pytest_django.asserts import assertContains, assertTemplateUsed
+from pytest_django.asserts import assertContains, assertRedirects, assertTemplateUsed
from main.views import ProcessAction
-def test_index(client, admin_client, mocker):
+def test_index(client, auth_client, admin_client, mocker):
"""Test the index view."""
mocker.patch("main.views.get_session_info")
+
+ # Test with an anonymous client.
+ response = client.get(reverse("main:index"))
+ assert response.status_code == HTTPStatus.FOUND
+ assertRedirects(response, "/accounts/login/?next=/")
+
+ # Test with an authenticated client.
with assertTemplateUsed(template_name="main/index.html"):
- response = client.get(reverse("main:index"))
+ response = auth_client.get(reverse("main:index"))
+ assert response.status_code == HTTPStatus.OK
+
+ # Test with an admin client.
+ with assertTemplateUsed(template_name="main/index.html"):
+ response = admin_client.get(reverse("main:index"))
assert response.status_code == HTTPStatus.OK
assert "table" in response.context
assertContains(response, "Boot")
-def test_logs(client, mocker):
+def test_logs(client, auth_client, mocker):
"""Test the logs view."""
mock = mocker.patch("main.views._get_process_logs")
uuid = uuid4()
+
+ # Test with an anonymous client.
+ response = client.get(reverse("main:logs", kwargs=dict(uuid=uuid)))
+ assert response.status_code == HTTPStatus.FOUND
+ assertRedirects(response, f"/accounts/login/?next=/logs/{uuid}")
+
+ # Test with an authenticated client.
with assertTemplateUsed(template_name="main/logs.html"):
- response = client.get(reverse("main:logs", kwargs=dict(uuid=uuid)))
+ response = auth_client.get(reverse("main:logs", kwargs=dict(uuid=uuid)))
assert response.status_code == HTTPStatus.OK
mock.assert_called_once_with(str(uuid))
assert "log_text" in response.context
-def test_process_flush(client, mocker):
+def test_process_flush(client, auth_client, mocker):
"""Test the process_flush view."""
mock = mocker.patch("main.views._process_call")
uuid = uuid4()
+
+ # Test with an anonymous client.
response = client.get(reverse("main:flush", kwargs=dict(uuid=uuid)))
+ assert response.status_code == HTTPStatus.FOUND
+ assertRedirects(response, f"/accounts/login/?next=/flush/{uuid}")
+ # Test with an authenticated client.
+ response = auth_client.get(reverse("main:flush", kwargs=dict(uuid=uuid)))
assert response.status_code == HTTPStatus.FOUND
assert response.url == reverse("main:index")
mock.assert_called_once_with(str(uuid), ProcessAction.FLUSH)
@@ -49,27 +74,35 @@ class TestBootProcess:
template_name = "main/boot_process.html"
- def test_boot_process_get(self, client):
+ def test_boot_process_get(self, auth_client):
"""Test the GET request for the BootProcess view."""
with assertTemplateUsed(template_name=self.template_name):
- response = client.get(reverse("main:boot_process"))
+ response = auth_client.get(reverse("main:boot_process"))
assert response.status_code == HTTPStatus.OK
assert "form" in response.context
assertContains(response, f'form action="{reverse("main:boot_process")}"')
- def test_boot_process_post_invalid(self, client):
+ def test_boot_process_get_anon(self, client):
+ """Test the GET request for the BootProcess view with an anonymous client."""
+ response = client.get(reverse("main:boot_process"))
+ assert response.status_code == HTTPStatus.FOUND
+ assertRedirects(response, "/accounts/login/?next=/boot_process/")
+
+ def test_boot_process_post_invalid(self, auth_client):
"""Test the POST request for the BootProcess view with invalid data."""
with assertTemplateUsed(template_name=self.template_name):
- response = client.post(reverse("main:boot_process"), data=dict())
+ response = auth_client.post(reverse("main:boot_process"), data=dict())
assert response.status_code == HTTPStatus.OK
assert "form" in response.context
- def test_boot_process_post_valid(self, client, mocker, dummy_session_data):
+ def test_boot_process_post_valid(self, auth_client, mocker, dummy_session_data):
"""Test the POST request for the BootProcess view."""
mock = mocker.patch("main.views._boot_process")
- response = client.post(reverse("main:boot_process"), data=dummy_session_data)
+ response = auth_client.post(
+ reverse("main:boot_process"), data=dummy_session_data
+ )
assert response.status_code == HTTPStatus.FOUND
assert response.url == reverse("main:index")