From 6950cc220e5a18b387f9c4b0f63198908c0d2a96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Bastian?= Date: Tue, 6 Sep 2022 23:39:07 +0200 Subject: [PATCH] API: base implementation of /cards, /card/ Implements #1 Implements #2 --- .gitignore | 1 + api/urls.py | 7 ++++++ api/view_utils.py | 52 ++++++++++++++++++++++++++++++++++++++++++++ api/views.py | 53 +++++++++++++++++++++++++++++++++++++++++++-- catima_sync/urls.py | 3 ++- 5 files changed, 113 insertions(+), 3 deletions(-) create mode 100644 api/urls.py create mode 100644 api/view_utils.py diff --git a/.gitignore b/.gitignore index d1de1e5..dbad747 100644 --- a/.gitignore +++ b/.gitignore @@ -153,3 +153,4 @@ cython_debug/ # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ +_token diff --git a/api/urls.py b/api/urls.py new file mode 100644 index 0000000..825894b --- /dev/null +++ b/api/urls.py @@ -0,0 +1,7 @@ +from django.urls import path +from . import views + +urlpatterns = [ + path("cards/", views.CardsGet.as_view()), + path("card//", views.CardGet.as_view()), +] diff --git a/api/view_utils.py b/api/view_utils.py new file mode 100644 index 0000000..d1d2f22 --- /dev/null +++ b/api/view_utils.py @@ -0,0 +1,52 @@ +""" Utility classes for API views """ + +from .models import AuthToken +import typing as t +from django import http +from django.views.generic.base import View + +if t.TYPE_CHECKING: + _base_view_mixin = View +else: + _base_view_mixin = object + + +class TokenAuthMixin(_base_view_mixin): + """Checks that the user provides a correct token-based auth through headers""" + + auth_token: AuthToken + + def dispatch(self, request, *args, **kwargs) -> http.response.HttpResponseBase: + token_name = request.headers.get("x-token-username", None) + token_secret = request.headers.get("x-token-secret", None) + if not token_name or not token_secret: + return http.JsonResponse( + { + "reason": "No X-Token-Username or X-Token-Secret provided", + }, + status=403, + ) + + token = AuthToken.check_auth(token_name, token_secret) + if token is None: + return http.JsonResponse( + {"reason": "Token authentication failed"}, + status=403, + ) + + self.auth_token = token + return super().dispatch(request, *args, **kwargs) + + +class APIView(View): + """A View part of the API.""" + + # Override to empty list by default -- prevent mistakes + http_method_names: list[str] = [] + + def http_method_not_allowed(self, request, *args, **kwargs) -> http.HttpResponse: + """Called when a bad method is used""" + return http.JsonResponse( + {"reason": f"{request.method} method not allowed for this endpoint."}, + status=405, + ) diff --git a/api/views.py b/api/views.py index 91ea44a..a7e19f7 100644 --- a/api/views.py +++ b/api/views.py @@ -1,3 +1,52 @@ -from django.shortcuts import render +from backend import models as backend_models +from .view_utils import TokenAuthMixin, APIView +from django.views.generic.detail import SingleObjectMixin +from django.views.generic.list import MultipleObjectMixin +from django.db.models.query import QuerySet +from django.http import JsonResponse -# Create your views here. + +class LoyaltyCardAuthenticatedMixin(TokenAuthMixin): + """Restricts the queryset to the LoyaltyCards owned by the authenticated user""" + + def get_queryset(self) -> QuerySet: + return backend_models.LoyaltyCard.objects.filter(owner=self.auth_token.user) + + +class CardsGet(LoyaltyCardAuthenticatedMixin, MultipleObjectMixin, APIView): + """Get the list of cards of this user, mapped to their revision ID""" + + http_method_names = ["get"] + + def get(self, request, *args, **kwargs) -> JsonResponse: + cards = self.get_queryset() + data = {str(card.uuid): card.revision_id for card in cards} + return JsonResponse(data) + + +class CardGet(LoyaltyCardAuthenticatedMixin, SingleObjectMixin, APIView): + """Get the details of a single card""" + + pk_url_kwarg = "uuid" + + http_method_names = ["get"] + + def get(self, request, *args, **kwargs): + card = self.get_object() + data = { + "uuid": str(card.uuid), + "store": card.store, + "note": card.note, + "expiracy": card.expiracy, + "balance": card.balance, + "balance_currency": card.balance_currency, + "card_id": card.card_id, + "barcode_id": card.barcode_id_raw, + "header_color": card.header_color.as_hex(), + "star_status": card.star_status, + "archive_status": card.archive_status, + "last_used": card.last_used, + "zoom_level": card.zoom_level, + "revision_id": card.revision_id, + } + return JsonResponse(data) diff --git a/catima_sync/urls.py b/catima_sync/urls.py index ba8b641..0b60b12 100644 --- a/catima_sync/urls.py +++ b/catima_sync/urls.py @@ -14,8 +14,9 @@ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.urls import path +from django.urls import include, path urlpatterns = [ path("admin/", admin.site.urls), + path("api/", include("api.urls")), ]