Skip to content
This repository has been archived by the owner on Jul 2, 2024. It is now read-only.

Commit

Permalink
[Auth Token] Implement user authentication (#109)
Browse files Browse the repository at this point in the history
  • Loading branch information
shorodilov committed Feb 13, 2024
2 parents daddd4a + 5e22648 commit d8b8cc5
Show file tree
Hide file tree
Showing 10 changed files with 136 additions and 13 deletions.
18 changes: 13 additions & 5 deletions tasks/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,23 @@
"""

from django.urls import include, path
from rest_framework.routers import DefaultRouter
from django.urls import path

from tasks.resources import TaskModelViewSet

router = DefaultRouter()
router.register(r"tasks", TaskModelViewSet, basename="tasks")
tasks_list = TaskModelViewSet.as_view({
"get": "list",
"post": "create",
})
tasks_detail = TaskModelViewSet.as_view({
"get": "retrieve",
"put": "update",
"patch": "partial_update",
"delete": "destroy",
})

app_name = "tasks"
urlpatterns = [
path("", include(router.urls)),
path("tasks/", tasks_list, name="list"),
path("tasks/<uuid:pk>/", tasks_detail, name="detail"),
]
4 changes: 2 additions & 2 deletions tasks/templates/tasks/_actions.html
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{% if object.completed %}
<button class="btn btn-outline-warning mx-3 {{ update_permission }}"
hx-patch="{% url "api:tasks-detail" object.pk %}" hx-swap="none"
hx-patch="{% url "api:tasks:detail" object.pk %}" hx-swap="none"
hx-vals="js:{completed:false}" hx-headers="js:{'X-CSRFToken': getCookieValue('csrftoken')}">
Reopen
</button>
{% else %}
<button class="btn btn-outline-success mx-3 {{ update_permission }}"
hx-patch="{% url "api:tasks-detail" object.pk %}" hx-swap="none"
hx-patch="{% url "api:tasks:detail" object.pk %}" hx-swap="none"
hx-vals="js:{completed:true}" hx-headers="js:{'X-CSRFToken': getCookieValue('csrftoken')}">
Complete
</button>
Expand Down
6 changes: 3 additions & 3 deletions tasks/templates/tasks/_task_tr.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,15 @@
<div class="d-flex flex-row justify-content-between align-items-center">
{% if object.completed %}
<i class="bi bi-arrow-repeat {{ update_permission }}" role="button"
hx-patch="{% url "api:tasks-detail" object.pk %}" hx-swap="none"
hx-patch="{% url "api:tasks:detail" object.pk %}" hx-swap="none"
hx-vals="js:{completed:false}" hx-headers="js:{'X-CSRFToken': getCookieValue('csrftoken')}"></i>
{% else %}
<i class="bi bi-check-lg {{ update_permission }}" role="button"
hx-patch="{% url "api:tasks-detail" object.pk %}" hx-swap="none"
hx-patch="{% url "api:tasks:detail" object.pk %}" hx-swap="none"
hx-vals="js:{completed:true}" hx-headers="js:{'X-CSRFToken': getCookieValue('csrftoken')}"></i>
{% endif %}
<i class="bi bi-trash {{ delete_permission }}" role="button"
hx-delete="{% url "api:tasks-detail" object.pk %}" hx-target="closest tr" hx-swap="outerHTML"
hx-delete="{% url "api:tasks:detail" object.pk %}" hx-target="closest tr" hx-swap="outerHTML"
hx-headers="js:{'X-CSRFToken': getCookieValue('csrftoken')}"></i>
</div>
</td>
Expand Down
4 changes: 2 additions & 2 deletions tasks/tests/unit/test_viewsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ class TestTaskModelViewSet(test.APITestCase):

@classmethod
def setUpTestData(cls) -> None:
cls.url_list = reverse("api:tasks-list")
cls.url_detail = reverse("api:tasks-detail", args=(PK_EXISTS,))
cls.url_list = reverse("api:tasks:list")
cls.url_detail = reverse("api:tasks:detail", args=(PK_EXISTS,))
cls.data = {
"summary": "Test task model view set",
"reporter": 2,
Expand Down
9 changes: 9 additions & 0 deletions tasktracker/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"crispy_forms",
"crispy_bootstrap5",
"rest_framework",
"rest_framework.authtoken",

"tasks.apps.TasksAppConfig",
"users.apps.UsersAppConfig",
Expand Down Expand Up @@ -157,3 +158,11 @@
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"

CRISPY_TEMPLATE_PACK = "bootstrap5"

# Django REST framework settings

REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": (
"rest_framework.authentication.TokenAuthentication",
),
}
8 changes: 7 additions & 1 deletion tasktracker/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,15 @@
from django.contrib import admin
from django.urls import include, path

api_urlpatterns = [
path("", include("users.routes", namespace="users")),
path("", include("tasks.routes", namespace="tasks")),
]
api_routes = (api_urlpatterns, "api")

urlpatterns = [
path("admin/", admin.site.urls),
path("api/", include("tasks.routes", namespace="api")),
path("api/", include(api_routes, namespace="api")),
path("", include("users.urls", namespace="users")),
path("", include("tasks.urls", namespace="tasks")),
]
Expand Down
22 changes: 22 additions & 0 deletions users/resources.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""
Users application API resources
"""

from rest_framework.authtoken.models import Token
from rest_framework.authtoken.views import ObtainAuthToken
from rest_framework.response import Response


class AuthTokenAPIView(ObtainAuthToken):
def post(self, request, *args, **kwargs):
ctx = {"request": request}
serializer = self.serializer_class(data=request.data, context=ctx)
serializer.is_valid(raise_exception=True)
user = serializer.validated_data["user"]
token, _ = Token.objects.get_or_create(user=user)

return Response({
"user_pk": user.pk,
"token": token.key,
})
13 changes: 13 additions & 0 deletions users/routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""
Users application API routes
"""

from django.urls import path

from users.resources import AuthTokenAPIView

app_name = "users"
urlpatterns = [
path("auth-token/", AuthTokenAPIView.as_view(), name="auth-token"),
]
35 changes: 35 additions & 0 deletions users/tests/integration/test_auth_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from http import HTTPStatus

from rest_framework import test
from rest_framework.reverse import reverse


class TestAuthTokenAPIView(test.APITestCase):
fixtures = ["users"]

@classmethod
def setUpTestData(cls) -> None:
cls.url_path = reverse("api:users:auth-token")
cls.credentials = {
"username": "prombery87",
"password": "ieZeiSh5k",
}

def setUp(self) -> None:
self.client = test.APIClient()

def test_valid_credentials(self):
response = self.client.post(self.url_path, self.credentials)
self.assertIn(b"user_pk", response.content)
self.assertIn(b"token", response.content)

def test_invalid_credentials(self):
credentials = self.credentials.copy()
credentials["username"] = "invalid"
response = self.client.post(self.url_path, credentials)
self.assertEqual(response.status_code, HTTPStatus.BAD_REQUEST)

credentials = self.credentials.copy()
credentials["password"] = "invalid"
response = self.client.post(self.url_path, credentials)
self.assertEqual(response.status_code, HTTPStatus.BAD_REQUEST)
30 changes: 30 additions & 0 deletions users/tests/unit/test_auth_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from django.contrib.auth import get_user_model
from rest_framework import test
from rest_framework.authtoken.models import Token
from rest_framework.reverse import reverse

from users.resources import AuthTokenAPIView

UserModel = get_user_model()


class TestAuthTokenAPIView(test.APITestCase):
fixtures = ["users"]

@classmethod
def setUpTestData(cls) -> None:
cls.url_path = reverse("api:users:auth-token")
cls.credentials = {
"username": "prombery87",
"password": "ieZeiSh5k",
}
cls.user = UserModel.objects.get(pk=2)

def setUp(self) -> None:
self.factory = test.APIRequestFactory()
self.view = AuthTokenAPIView.as_view()

def test_auth_token_created(self):
request = self.factory.post(self.url_path, self.credentials)
self.view(request)
self.assertTrue(Token.objects.filter(user=self.user).exists())

0 comments on commit d8b8cc5

Please sign in to comment.