From 73c6fbf165f29252ae17f28495fad3d3a40d01db Mon Sep 17 00:00:00 2001 From: Robert Date: Fri, 21 May 2021 11:06:33 +0200 Subject: [PATCH] feat: Allow to confirm confidant --- djeveric/__init__.py | 0 djeveric/rest_api.py | 54 +++++++++++++++++++++++++++++ djeveric/signals.py | 34 ++++++++++++++++++ userausfall/rest_api/serializers.py | 2 ++ userausfall/rest_api/urls.py | 8 +++++ userausfall/settings.py | 1 + userausfall/signals.py | 26 ++++++++++++++ userausfall/urls.py | 2 +- 8 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 djeveric/__init__.py create mode 100644 djeveric/rest_api.py create mode 100644 djeveric/signals.py diff --git a/djeveric/__init__.py b/djeveric/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/djeveric/rest_api.py b/djeveric/rest_api.py new file mode 100644 index 0000000..87dc333 --- /dev/null +++ b/djeveric/rest_api.py @@ -0,0 +1,54 @@ +from django.contrib.auth import get_user_model +from django.contrib.auth.tokens import default_token_generator +from django.contrib.contenttypes.models import ContentType +from django.db import models +from django.utils.http import urlsafe_base64_decode +from rest_framework import serializers, status +from rest_framework.response import Response +from rest_framework.views import APIView + +from djeveric.signals import user_confirmed, AlreadyConfirmed + + +class ConfirmationSerializer(serializers.Serializer): + uid = serializers.CharField() + token = serializers.CharField() + type_id = serializers.CharField() + obj_id = serializers.CharField() + + +class ConfirmationView(APIView): + def post(self, request, format=None): + serializer = ConfirmationSerializer(data=request.data) + if serializer.is_valid(): + user = self.get_user(serializer.validated_data["uid"]) + token = serializer.validated_data["token"] + if user is not None and default_token_generator.check_token(user, token): + obj = self.get_obj(serializer.validated_data["type_id"], serializer.validated_data["obj_id"]) + if obj is not None: + try: + user_confirmed.send(sender=self.__class__, user=user, instance=obj, key=None) + except AlreadyConfirmed as e: + return Response({"message": str(e)}, status=status.HTTP_400_BAD_REQUEST) + return Response(serializer.data, status=status.HTTP_204_NO_CONTENT) + + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def get_obj(self, type_id, obj_id): + try: + # urlsafe_base64_decode() decodes to bytestring + type_pk = urlsafe_base64_decode(type_id).decode() + obj_pk = urlsafe_base64_decode(obj_id).decode() + obj = ContentType.objects.get_for_id(type_pk).get_object_for_this_type(pk=obj_pk) + except (TypeError, ValueError, OverflowError, models.Model.DoesNotExist): + obj = None + return obj + + def get_user(self, uid): + try: + # urlsafe_base64_decode() decodes to bytestring + uid = urlsafe_base64_decode(uid).decode() + user = get_user_model().objects.get(pk=uid) + except (TypeError, ValueError, OverflowError, models.Model.DoesNotExist): + user = None + return user diff --git a/djeveric/signals.py b/djeveric/signals.py new file mode 100644 index 0000000..86b2012 --- /dev/null +++ b/djeveric/signals.py @@ -0,0 +1,34 @@ +from django.contrib.auth.tokens import default_token_generator +from django.contrib.contenttypes.models import ContentType +from django.contrib.sites.models import Site +from django.core.mail import send_mail +from django.dispatch import Signal +from django.utils.encoding import force_bytes +from django.utils.http import urlsafe_base64_encode + +user_confirmed = Signal(providing_args=["user", "instance", "key"]) + + +class AlreadyConfirmed(Exception): + """The given resource has already been confirmed.""" + pass + + +def encode_pk(obj): + return urlsafe_base64_encode(force_bytes(obj.pk)) + + +def request_confirmation(user, instance, key=None): + site = Site.objects.first() + uid = encode_pk(user) + token = default_token_generator.make_token(user) + obj_type = ContentType.objects.get_for_model(instance) + type_id = encode_pk(obj_type) + obj_id = encode_pk(instance) + confirmation_url = f"https://{site.domain}/confirm/{uid}/{token}/{type_id}/{obj_id}" + send_mail( + f"{site.name}: Bestätigung der Anfrage", + f"Bitte bestätige, dass du deine E-Mail-Adresse auf der Seite {site.name} ({site.domain}) eingegeben hast. " + f"Kopiere dazu folgende URL in deinen Webbrowser:\n\n{confirmation_url}", + None, [user.email] + ) diff --git a/userausfall/rest_api/serializers.py b/userausfall/rest_api/serializers.py index 05eea7b..c06ecf3 100644 --- a/userausfall/rest_api/serializers.py +++ b/userausfall/rest_api/serializers.py @@ -8,6 +8,8 @@ class UserActivationSerializer(serializers.Serializer): class UserSerializer(serializers.ModelSerializer): + confidant_email = serializers.EmailField() + class Meta: model = User fields = ("pk", "email", "username", "confidant_email") diff --git a/userausfall/rest_api/urls.py b/userausfall/rest_api/urls.py index 3094cd0..f784bcd 100644 --- a/userausfall/rest_api/urls.py +++ b/userausfall/rest_api/urls.py @@ -1,6 +1,14 @@ +from django.urls import path from rest_framework import routers +from djeveric.rest_api import ConfirmationView from userausfall.rest_api.views import UserViewSet router = routers.DefaultRouter(trailing_slash=True) router.register(r'users', UserViewSet, basename="user") + +urlpatterns = [ + path("confirm", ConfirmationView.as_view()) +] + +urlpatterns += router.urls diff --git a/userausfall/settings.py b/userausfall/settings.py index 3486771..786acf6 100644 --- a/userausfall/settings.py +++ b/userausfall/settings.py @@ -35,6 +35,7 @@ INSTALLED_APPS = [ 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', + 'django.contrib.sites', 'django.contrib.staticfiles', 'userausfall', 'rest_framework', diff --git a/userausfall/signals.py b/userausfall/signals.py index e69de29..8b09cfe 100644 --- a/userausfall/signals.py +++ b/userausfall/signals.py @@ -0,0 +1,26 @@ +from django.core.exceptions import PermissionDenied +from django.db.models.signals import post_save +from django.dispatch import receiver +from rest_framework.exceptions import PermissionDenied + +from djeveric.signals import request_confirmation, user_confirmed, AlreadyConfirmed +from userausfall.models import User + + +@receiver(post_save, sender=User) +def user_saved(sender, instance: User, **kwargs): + if (instance.confidant_unconfirmed is not None) and (instance.confidant_unconfirmed != instance.confidant): + request_confirmation(instance.confidant_unconfirmed, instance) + + +@receiver(user_confirmed) +def user_confirmed(sender, user: User, instance: User, **kwargs): + if user == instance.confidant_unconfirmed: + if instance.confidant_unconfirmed != instance.confidant: + # confirm the confidant + instance.confidant = instance.confidant_unconfirmed + instance.save() + else: + raise AlreadyConfirmed("The confidant has already been confirmed.") + else: + raise PermissionDenied() diff --git a/userausfall/urls.py b/userausfall/urls.py index 0175b96..64efbbd 100644 --- a/userausfall/urls.py +++ b/userausfall/urls.py @@ -5,6 +5,6 @@ from userausfall.rest_api import urls as rest_api_urls urlpatterns = [ path('admin/', admin.site.urls), - path('api/', include(rest_api_urls.router.urls)), + path('api/', include("userausfall.rest_api.urls")), path("api-auth/", include("rest_framework.urls")), ]