From d48846343d72947928e38e2f13166efe2d5daf5c Mon Sep 17 00:00:00 2001 From: Lova ANDRIARIMALALA <43842786+Xpirix@users.noreply.github.com> Date: Thu, 12 Sep 2024 14:18:36 +0200 Subject: [PATCH 01/17] Create user token model --- qgis-app/api/migrations/0001_initial.py | 30 ++++++++++++++++++++++ qgis-app/api/models.py | 33 ++++++++++++++++++++++++- 2 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 qgis-app/api/migrations/0001_initial.py diff --git a/qgis-app/api/migrations/0001_initial.py b/qgis-app/api/migrations/0001_initial.py new file mode 100644 index 00000000..f2f51797 --- /dev/null +++ b/qgis-app/api/migrations/0001_initial.py @@ -0,0 +1,30 @@ +# Generated by Django 4.2.16 on 2024-09-12 07:16 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('token_blacklist', '0012_alter_outstandingtoken_user'), + ] + + operations = [ + migrations.CreateModel( + name='HubOutstandingToken', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('is_blacklisted', models.BooleanField(default=False)), + ('is_newly_created', models.BooleanField(default=False)), + ('description', models.CharField(blank=True, help_text="Describe this token so that it's easier to remember where you're using it.", max_length=512, null=True, verbose_name='Description')), + ('last_used_on', models.DateTimeField(blank=True, null=True, verbose_name='Last used on')), + ('token', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='token_blacklist.outstandingtoken')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/qgis-app/api/models.py b/qgis-app/api/models.py index 6b202199..c63b5454 100644 --- a/qgis-app/api/models.py +++ b/qgis-app/api/models.py @@ -1 +1,32 @@ -# Create your models here. +from base.models.processing_models import Resource +from django.db import models +from django.utils.translation import gettext_lazy as _ +from rest_framework_simplejwt.token_blacklist.models import OutstandingToken +from django.contrib.auth.models import User + +class HubOutstandingToken(models.Model): + """ + Hub outstanding token + """ + user = models.ForeignKey( + User, + on_delete=models.CASCADE + ) + token = models.ForeignKey( + OutstandingToken, + on_delete=models.CASCADE + ) + is_blacklisted = models.BooleanField(default=False) + is_newly_created = models.BooleanField(default=False) + description = models.CharField( + verbose_name=_("Description"), + help_text=_("Describe this token so that it's easier to remember where you're using it."), + max_length=512, + blank=True, + null=True, + ) + last_used_on = models.DateTimeField( + verbose_name=_("Last used on"), + blank=True, + null=True + ) \ No newline at end of file From f4c7b8981962d72bf15209a344f4aea6a1813765 Mon Sep 17 00:00:00 2001 From: Lova ANDRIARIMALALA <43842786+Xpirix@users.noreply.github.com> Date: Thu, 12 Sep 2024 16:32:18 +0200 Subject: [PATCH 02/17] Token list and create token view --- qgis-app/api/templates/hub_token_list.html | 75 ++++++++++++++ qgis-app/api/urls.py | 20 +++- qgis-app/api/views.py | 110 ++++++++++++++++++++- qgis-app/templates/base.html | 5 +- 4 files changed, 205 insertions(+), 5 deletions(-) create mode 100644 qgis-app/api/templates/hub_token_list.html diff --git a/qgis-app/api/templates/hub_token_list.html b/qgis-app/api/templates/hub_token_list.html new file mode 100644 index 00000000..c5604736 --- /dev/null +++ b/qgis-app/api/templates/hub_token_list.html @@ -0,0 +1,75 @@ +{% extends BASE_TEMPLATE %}{% load i18n %} +{% load local_timezone %} +{% block content %} +

{% trans "Tokens" %}

+
{% csrf_token %} +
+

+ +

+
+
+{% if object_list.count %} +
+ + + + + + + + + + + {% for hub_token in object_list %} + + + + + + + + + {% endfor %} + +
{% trans "Description" %}{% trans "Created at" %}{% trans "Last used at" %}{% trans "Manage" %}
{{ hub_token.token.user }}{{ hub_token.description|default:"-" }} + + {{ hub_token.token.jti }} + + {{ hub_token.token.created_at|local_timezone }}{{ hub_token.last_used_on|default:"-"|local_timezone }} +   + + +
+
+{% else %} +
+ + {% trans "This list is empty!" %} +
+{% endif %} + +{% endblock %} + +{% block extracss %} +{{ block.super }} + +{% endblock %} diff --git a/qgis-app/api/urls.py b/qgis-app/api/urls.py index c2048c34..db44fe6a 100644 --- a/qgis-app/api/urls.py +++ b/qgis-app/api/urls.py @@ -1,9 +1,27 @@ from api.views import ResourceAPIDownload, ResourceAPIList from django.urls import path - +from django.urls import re_path as url +from api.views import HubTokenDetailView, HubTokenListView, hub_token_create urlpatterns = [ path("resources/", ResourceAPIList.as_view(), name="resource-list"), path( "resource//", ResourceAPIDownload.as_view(), name="resource-download" ), + url( + r"^tokens/$", + HubTokenListView.as_view(), + name="hub_token_list", + ), + url( + r"^tokens/(?P\d+)/$", + HubTokenDetailView.as_view(), + name="hub_token_detail", + ), + url( + r"^tokens/create/$", + hub_token_create, + {}, + name="hub_token_create", + ), + ] diff --git a/qgis-app/api/views.py b/qgis-app/api/views.py index f62375d2..e2f02dcc 100644 --- a/qgis-app/api/views.py +++ b/qgis-app/api/views.py @@ -3,12 +3,22 @@ from api.serializers import GeopackageSerializer, ModelSerializer, StyleSerializer, LayerDefinitionSerializer, WavefrontSerializer from base.license import zip_a_file_if_not_zipfile from django.contrib.postgres.search import SearchVector -from django.http import Http404, HttpResponse +from django.http import Http404, HttpResponse, HttpResponseRedirect from django.utils.decorators import method_decorator from django.utils.text import slugify from django.views.decorators.cache import cache_page from drf_multiple_model.pagination import MultipleModelLimitOffsetPagination from drf_multiple_model.views import FlatMultipleModelAPIView +from django.views.decorators.csrf import ensure_csrf_cookie +from django.shortcuts import get_object_or_404, render +from django.contrib.auth.decorators import login_required +from django.db import transaction + +from django.views.generic import ListView, DetailView +from rest_framework_simplejwt.tokens import RefreshToken, api_settings +from django.urls import reverse +from rest_framework_simplejwt.exceptions import InvalidToken, TokenError +import time # models from geopackages.models import Geopackage @@ -18,7 +28,8 @@ from styles.models import Style from layerdefinitions.models import LayerDefinition from wavefronts.models import Wavefront - +from api.models import HubOutstandingToken +from rest_framework_simplejwt.token_blacklist.models import OutstandingToken def filter_resource_type(queryset, request, *args, **kwargs): resource_type = request.query_params["resource_type"] @@ -168,3 +179,98 @@ def get(self, request, *args, **kwargs): slugify(object.name, allow_unicode=True) ) return response + + +class HubTokenDetailView(DetailView): + """ + Hub token detail + """ + model = OutstandingToken + queryset = OutstandingToken.objects.all() + template_name = "hub_token_detail.html" + + @method_decorator(ensure_csrf_cookie) + def dispatch(self, *args, **kwargs): + return super(HubTokenDetailView, self).dispatch(*args, **kwargs) + + def get_context_data(self, **kwargs): + context = super(HubTokenDetailView, self).get_context_data(**kwargs) + token_id = self.kwargs.get('pk') + + outstanding_token = get_object_or_404( + OutstandingToken, + pk=token_id, + user=self.request.user + ) + hub_token = get_object_or_404( + HubOutstandingToken, + token__pk=outstanding_token.pk, + is_blacklisted=False, + is_newly_created=True + ) + try: + token = RefreshToken(outstanding_token.token) + token['refresh_jti'] = token[api_settings.JTI_CLAIM] + except (InvalidToken, TokenError) as e: + context = {} + self.template_name = "hub_token_invalid_or_expired.html" + return context + timestamp_from_last_edit = int(time.time()) + context.update( + { + "access_token": str(token.access_token), + "object": outstanding_token, + 'timestamp_from_last_edit': timestamp_from_last_edit + } + ) + hub_token.is_newly_created = False + hub_token.save() + return context + + +class HubTokenListView(ListView): + """ + Hub token list + """ + model = HubOutstandingToken + queryset = HubOutstandingToken.objects.all().order_by("-token__created_at") + template_name = "hub_token_list.html" + + @method_decorator(ensure_csrf_cookie) + def dispatch(self, *args, **kwargs): + return super(HubTokenListView, self).dispatch(*args, **kwargs) + + def get_filtered_queryset(self, qs): + return qs.filter( + is_blacklisted=False + ) + + def get_queryset(self): + qs = super(HubTokenListView, self).get_queryset() + qs = self.get_filtered_queryset(qs) + return qs + + + +@login_required +@transaction.atomic +def hub_token_create(request): + if request.method == "POST": + user = request.user + refresh = RefreshToken.for_user(user) + + jti = refresh[api_settings.JTI_CLAIM] + + outstanding_token = OutstandingToken.objects.get(jti=jti) + + hub_token = HubOutstandingToken.objects.create( + user=user, + token=outstanding_token, + is_blacklisted=False, + is_newly_created=True + ) + + return HttpResponseRedirect( + reverse("hub_token_detail", args=[hub_token.pk]) + ) + diff --git a/qgis-app/templates/base.html b/qgis-app/templates/base.html index 74b94bd1..75378665 100644 --- a/qgis-app/templates/base.html +++ b/qgis-app/templates/base.html @@ -71,8 +71,9 @@ + {% endfor %} +
  • API
  • + {% if user.is_authenticated %} From 8b57d4c97eb6e8e84e600621f1768e8efa1826e5 Mon Sep 17 00:00:00 2001 From: Lova ANDRIARIMALALA <43842786+Xpirix@users.noreply.github.com> Date: Tue, 17 Sep 2024 13:58:23 +0300 Subject: [PATCH 03/17] Add token detail page, fix token create --- qgis-app/api/templates/hub_token_detail.html | 110 +++++++++++++++++++ qgis-app/api/templates/hub_token_list.html | 9 +- qgis-app/api/views.py | 17 ++- 3 files changed, 120 insertions(+), 16 deletions(-) create mode 100644 qgis-app/api/templates/hub_token_detail.html diff --git a/qgis-app/api/templates/hub_token_detail.html b/qgis-app/api/templates/hub_token_detail.html new file mode 100644 index 00000000..ae60dab5 --- /dev/null +++ b/qgis-app/api/templates/hub_token_detail.html @@ -0,0 +1,110 @@ +{% extends BASE_TEMPLATE %}{% load i18n %} +{% load local_timezone %} +{% block content %} +

    {% trans "Token for" %} {{ hub.name }}

    +
    + + To enhance the security of your token, + it will be displayed only once. Please ensure + to save it in a secure location. If the token + is lost, you can generate a new one at any time. +
    +
    +
    {% trans "User"%}
    +
    + {{ object.user }} +
    +
    {% trans "Created at"%}
    +
    + {{ object.created_at|local_timezone }} +
    +
    {% trans "Expires at"%}
    +
    + {{ object.expires_at|local_timezone }} +
    +
    {% trans "Access token"%}
    +
    + +
    + +
    + +
    + +
    +
    + {% trans "Back to the list" %} + {% comment %} {% trans "Edit description" %} {% endcomment %} +
    +{% endblock %} +{% block extracss %} +{{ block.super }} + +{% endblock %} + +{% block extrajs %} + +{% endblock %} \ No newline at end of file diff --git a/qgis-app/api/templates/hub_token_list.html b/qgis-app/api/templates/hub_token_list.html index c5604736..6bc8b162 100644 --- a/qgis-app/api/templates/hub_token_list.html +++ b/qgis-app/api/templates/hub_token_list.html @@ -29,20 +29,15 @@

    {{ hub_token.token.user }} {{ hub_token.description|default:"-" }} - - - {{ hub_token.token.jti }} - - {{ hub_token.token.created_at|local_timezone }} {{ hub_token.last_used_on|default:"-"|local_timezone }} -   - + {% endcomment %} {% endfor %} diff --git a/qgis-app/api/views.py b/qgis-app/api/views.py index e2f02dcc..1a936695 100644 --- a/qgis-app/api/views.py +++ b/qgis-app/api/views.py @@ -196,18 +196,17 @@ def dispatch(self, *args, **kwargs): def get_context_data(self, **kwargs): context = super(HubTokenDetailView, self).get_context_data(**kwargs) token_id = self.kwargs.get('pk') - - outstanding_token = get_object_or_404( - OutstandingToken, - pk=token_id, - user=self.request.user - ) hub_token = get_object_or_404( HubOutstandingToken, - token__pk=outstanding_token.pk, + pk=token_id, is_blacklisted=False, is_newly_created=True ) + outstanding_token = get_object_or_404( + OutstandingToken, + pk=hub_token.token.pk, + user=self.request.user + ) try: token = RefreshToken(outstanding_token.token) token['refresh_jti'] = token[api_settings.JTI_CLAIM] @@ -223,8 +222,8 @@ def get_context_data(self, **kwargs): 'timestamp_from_last_edit': timestamp_from_last_edit } ) - hub_token.is_newly_created = False - hub_token.save() + # hub_token.is_newly_created = False + # hub_token.save() return context From 2f5fe2a017ec2d4c818b1bf449f94548c38dbddb Mon Sep 17 00:00:00 2001 From: Lova ANDRIARIMALALA <43842786+Xpirix@users.noreply.github.com> Date: Tue, 17 Sep 2024 16:14:05 +0300 Subject: [PATCH 04/17] CRUD of user token --- qgis-app/api/forms.py | 14 +++ qgis-app/api/migrations/0001_initial.py | 2 +- qgis-app/api/models.py | 2 +- qgis-app/api/templates/user_token_delete.html | 9 ++ ...ken_detail.html => user_token_detail.html} | 16 +-- qgis-app/api/templates/user_token_form.html | 26 ++++ ...b_token_list.html => user_token_list.html} | 23 ++-- qgis-app/api/urls.py | 26 ++-- qgis-app/api/views.py | 114 ++++++++++++++---- qgis-app/templates/base.html | 2 +- 10 files changed, 175 insertions(+), 59 deletions(-) create mode 100644 qgis-app/api/forms.py create mode 100644 qgis-app/api/templates/user_token_delete.html rename qgis-app/api/templates/{hub_token_detail.html => user_token_detail.html} (78%) create mode 100644 qgis-app/api/templates/user_token_form.html rename qgis-app/api/templates/{hub_token_list.html => user_token_list.html} (67%) diff --git a/qgis-app/api/forms.py b/qgis-app/api/forms.py new file mode 100644 index 00000000..1431e358 --- /dev/null +++ b/qgis-app/api/forms.py @@ -0,0 +1,14 @@ +from django.forms import CharField, ModelForm +from api.models import UserOutstandingToken + + +class UserTokenForm(ModelForm): + """ + Form for token description editing + """ + + class Meta: + model = UserOutstandingToken + fields = ( + "description", + ) \ No newline at end of file diff --git a/qgis-app/api/migrations/0001_initial.py b/qgis-app/api/migrations/0001_initial.py index f2f51797..950f36bf 100644 --- a/qgis-app/api/migrations/0001_initial.py +++ b/qgis-app/api/migrations/0001_initial.py @@ -16,7 +16,7 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='HubOutstandingToken', + name='UserOutstandingToken', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('is_blacklisted', models.BooleanField(default=False)), diff --git a/qgis-app/api/models.py b/qgis-app/api/models.py index c63b5454..6db86a94 100644 --- a/qgis-app/api/models.py +++ b/qgis-app/api/models.py @@ -4,7 +4,7 @@ from rest_framework_simplejwt.token_blacklist.models import OutstandingToken from django.contrib.auth.models import User -class HubOutstandingToken(models.Model): +class UserOutstandingToken(models.Model): """ Hub outstanding token """ diff --git a/qgis-app/api/templates/user_token_delete.html b/qgis-app/api/templates/user_token_delete.html new file mode 100644 index 00000000..481c1ca4 --- /dev/null +++ b/qgis-app/api/templates/user_token_delete.html @@ -0,0 +1,9 @@ +{% extends BASE_TEMPLATE %}{% load i18n %} +{% block content %} +

    Delete token of "{{ username }}"

    +
    {% csrf_token %} +

    {% trans "You asked to delete a token.
    It will be permanently deleted and this action cannot be undone.
    Please confirm." %}

    +

    {% trans "Cancel" %}

    +
    + +{% endblock %} diff --git a/qgis-app/api/templates/hub_token_detail.html b/qgis-app/api/templates/user_token_detail.html similarity index 78% rename from qgis-app/api/templates/hub_token_detail.html rename to qgis-app/api/templates/user_token_detail.html index ae60dab5..2735de0a 100644 --- a/qgis-app/api/templates/hub_token_detail.html +++ b/qgis-app/api/templates/user_token_detail.html @@ -10,18 +10,6 @@

    {% trans "Token for" %} {{ hub.name }}

    is lost, you can generate a new one at any time.
    -
    {% trans "User"%}
    -
    - {{ object.user }} -
    -
    {% trans "Created at"%}
    -
    - {{ object.created_at|local_timezone }} -
    -
    {% trans "Expires at"%}
    -
    - {{ object.expires_at|local_timezone }} -
    {% trans "Access token"%}