From 2bc9b5ba850e6bcaccf3ffa796f742c87026483c Mon Sep 17 00:00:00 2001 From: Robert Date: Fri, 21 May 2021 10:14:12 +0200 Subject: [PATCH] feat: Add api endpoints for activation/ user attrs --- .../migrations/0006_auto_20210521_0805.py | 37 ++++++++++ userausfall/models.py | 68 ++++++++++++------- userausfall/rest_api/serializers.py | 18 ++--- userausfall/rest_api/views.py | 30 ++++---- 4 files changed, 106 insertions(+), 47 deletions(-) create mode 100644 userausfall/migrations/0006_auto_20210521_0805.py diff --git a/userausfall/migrations/0006_auto_20210521_0805.py b/userausfall/migrations/0006_auto_20210521_0805.py new file mode 100644 index 0000000..0a80fb1 --- /dev/null +++ b/userausfall/migrations/0006_auto_20210521_0805.py @@ -0,0 +1,37 @@ +# Generated by Django 2.2.20 on 2021-05-21 08:05 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('userausfall', '0005_delete_accountrequest'), + ] + + operations = [ + migrations.RemoveField( + model_name='user', + name='first_name', + ), + migrations.RemoveField( + model_name='user', + name='is_active', + ), + migrations.RemoveField( + model_name='user', + name='last_name', + ), + migrations.AddField( + model_name='user', + name='confidant_unconfirmed', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='unconfirmed_confidants', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='user', + name='confidant', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='confidants', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/userausfall/models.py b/userausfall/models.py index 1b85750..d20c385 100644 --- a/userausfall/models.py +++ b/userausfall/models.py @@ -11,11 +11,13 @@ from userausfall import ldap class MissingUserAttribute(Exception): """The user object is missing a required attribute.""" + pass class PasswordMismatch(Exception): """The given password does not match the user's password.""" + pass @@ -34,18 +36,18 @@ class UserManager(BaseUserManager): return user def create_user(self, email, password=None, **extra_fields): - extra_fields.setdefault('is_staff', False) - extra_fields.setdefault('is_superuser', False) + extra_fields.setdefault("is_staff", False) + extra_fields.setdefault("is_superuser", False) return self._create_user(email, password, **extra_fields) def create_superuser(self, email, password, **extra_fields): - extra_fields.setdefault('is_staff', True) - extra_fields.setdefault('is_superuser', True) + extra_fields.setdefault("is_staff", True) + extra_fields.setdefault("is_superuser", True) - if extra_fields.get('is_staff') is not True: - raise ValueError('Superuser must have is_staff=True.') - if extra_fields.get('is_superuser') is not True: - raise ValueError('Superuser must have is_superuser=True.') + if extra_fields.get("is_staff") is not True: + raise ValueError("Superuser must have is_staff=True.") + if extra_fields.get("is_superuser") is not True: + raise ValueError("Superuser must have is_superuser=True.") return self._create_user(email, password, **extra_fields) @@ -54,41 +56,53 @@ class User(AbstractBaseUser, PermissionsMixin): username_validator = UnicodeUsernameValidator() username = models.CharField( - _('username'), + _("username"), max_length=150, - help_text=_('Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.'), + help_text=_( + "Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only." + ), validators=[username_validator], - error_messages={ - 'unique': _("A user with that username already exists."), - }, + error_messages={"unique": _("A user with that username already exists.")}, blank=True, ) - email = models.EmailField(_('email address'), unique=True, blank=True) + email = models.EmailField(_("email address"), unique=True, blank=True) is_staff = models.BooleanField( - _('staff status'), + _("staff status"), default=False, - help_text=_('Designates whether the user can log into this admin site.'), + help_text=_("Designates whether the user can log into this admin site."), + ) + date_joined = models.DateTimeField(_("date joined"), default=timezone.now) + confidant = models.ForeignKey( + "User", on_delete=models.SET_NULL, null=True, related_name="confidants" + ) + confidant_unconfirmed = models.ForeignKey( + "User", + on_delete=models.SET_NULL, + null=True, + related_name="unconfirmed_confidants", ) - date_joined = models.DateTimeField(_('date joined'), default=timezone.now) - confidant = models.ForeignKey("User", on_delete=models.SET_NULL, null=True) objects = UserManager() - EMAIL_FIELD = 'email' - USERNAME_FIELD = 'email' + EMAIL_FIELD = "email" + USERNAME_FIELD = "email" REQUIRED_FIELDS = [] class Meta: - verbose_name = _('user') - verbose_name_plural = _('users') + verbose_name = _("user") + verbose_name_plural = _("users") + + @property + def confidant_email(self): + if self.confidant is not None: + return self.confidant.email + else: + return None def clean(self): super().clean() self.email = self.__class__.objects.normalize_email(self.email) - def get_confidant_email(self): - return "" - def email_user(self, subject, message, from_email=None, **kwargs): """Send an email to this user.""" send_mail(subject, message, from_email, [self.email], **kwargs) @@ -100,5 +114,7 @@ class User(AbstractBaseUser, PermissionsMixin): if not self.confidant: raise MissingUserAttribute("User is missing a confirmed confidant.") if not self.check_password(raw_password): - raise PasswordMismatch("The given password does not match the user's password.") + raise PasswordMismatch( + "The given password does not match the user's password." + ) return ldap.create_account(self.username, raw_password) diff --git a/userausfall/rest_api/serializers.py b/userausfall/rest_api/serializers.py index c4acdbd..05eea7b 100644 --- a/userausfall/rest_api/serializers.py +++ b/userausfall/rest_api/serializers.py @@ -3,17 +3,17 @@ from rest_framework import serializers from userausfall.models import User -class UserSerializer(serializers.ModelSerializer): - confidant_email = serializers.EmailField(source="get_confidant_email") +class UserActivationSerializer(serializers.Serializer): + password = serializers.CharField() + +class UserSerializer(serializers.ModelSerializer): class Meta: model = User - fields = ("email", "username", "confidant_email") + fields = ("pk", "email", "username", "confidant_email") - def get_confidant_email(self): - return "" - - def update(self, instance, validated_data): - print(validated_data) - confidant = validated_data.pop("get_confidant_email") + def update(self, instance: User, validated_data): + confidant_email = validated_data.pop("confidant_email") + confidant, _ = User.objects.get_or_create(email=confidant_email) + instance.confidant_unconfirmed = confidant return super().update(instance, validated_data) diff --git a/userausfall/rest_api/views.py b/userausfall/rest_api/views.py index c2ff3d6..bcf1363 100644 --- a/userausfall/rest_api/views.py +++ b/userausfall/rest_api/views.py @@ -1,19 +1,25 @@ -from rest_framework import viewsets +from rest_framework import viewsets, status from rest_framework.decorators import action from rest_framework.response import Response -from userausfall.models import User -from userausfall.rest_api.serializers import UserSerializer +from userausfall.models import User, MissingUserAttribute, PasswordMismatch +from userausfall.rest_api.serializers import UserSerializer, UserActivationSerializer class UserViewSet(viewsets.ModelViewSet): - class Meta: - queryset = User.objects.all() + queryset = User.objects.all() + serializer_class = UserSerializer - @action(detail=False, methods=["PATCH"]) - def me(self, request): - return Response( - UserSerializer( - instance=request.user, context={"request": request} - ).data - ) + @action(detail=True, methods=["post"]) + def activate(self, request, pk=None): + """Create the corresponding LDAP account.""" + user: User = self.get_object() + serializer = UserActivationSerializer(data=request.data) + if serializer.is_valid(): + try: + user.create_ldap_account(serializer.validated_data["password"]) + except (MissingUserAttribute, PasswordMismatch) as e: + return Response({"message": str(e)}, status=status.HTTP_400_BAD_REQUEST) + return Response(status=status.HTTP_204_NO_CONTENT) + else: + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)