Compare commits

..

No commits in common. "73c6fbf165f29252ae17f28495fad3d3a40d01db" and "d75ae864097ef23850507ac04fddc2c0f0e6a352" have entirely different histories.

11 changed files with 47 additions and 231 deletions

View file

View file

@ -1,54 +0,0 @@
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

View file

@ -1,34 +0,0 @@
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]
)

View file

@ -1,37 +0,0 @@
# 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),
),
]

View file

@ -11,13 +11,11 @@ from userausfall import ldap
class MissingUserAttribute(Exception): class MissingUserAttribute(Exception):
"""The user object is missing a required attribute.""" """The user object is missing a required attribute."""
pass pass
class PasswordMismatch(Exception): class PasswordMismatch(Exception):
"""The given password does not match the user's password.""" """The given password does not match the user's password."""
pass pass
@ -36,18 +34,18 @@ class UserManager(BaseUserManager):
return user return user
def create_user(self, email, password=None, **extra_fields): def create_user(self, email, password=None, **extra_fields):
extra_fields.setdefault("is_staff", False) extra_fields.setdefault('is_staff', False)
extra_fields.setdefault("is_superuser", False) extra_fields.setdefault('is_superuser', False)
return self._create_user(email, password, **extra_fields) return self._create_user(email, password, **extra_fields)
def create_superuser(self, email, password, **extra_fields): def create_superuser(self, email, password, **extra_fields):
extra_fields.setdefault("is_staff", True) extra_fields.setdefault('is_staff', True)
extra_fields.setdefault("is_superuser", True) extra_fields.setdefault('is_superuser', True)
if extra_fields.get("is_staff") is not True: if extra_fields.get('is_staff') is not True:
raise ValueError("Superuser must have is_staff=True.") raise ValueError('Superuser must have is_staff=True.')
if extra_fields.get("is_superuser") is not True: if extra_fields.get('is_superuser') is not True:
raise ValueError("Superuser must have is_superuser=True.") raise ValueError('Superuser must have is_superuser=True.')
return self._create_user(email, password, **extra_fields) return self._create_user(email, password, **extra_fields)
@ -56,53 +54,41 @@ class User(AbstractBaseUser, PermissionsMixin):
username_validator = UnicodeUsernameValidator() username_validator = UnicodeUsernameValidator()
username = models.CharField( username = models.CharField(
_("username"), _('username'),
max_length=150, max_length=150,
help_text=_( help_text=_('Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.'),
"Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only."
),
validators=[username_validator], 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, blank=True,
) )
email = models.EmailField(_("email address"), unique=True, blank=True) email = models.EmailField(_('email address'), unique=True, blank=True)
is_staff = models.BooleanField( is_staff = models.BooleanField(
_("staff status"), _('staff status'),
default=False, 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() objects = UserManager()
EMAIL_FIELD = "email" EMAIL_FIELD = 'email'
USERNAME_FIELD = "email" USERNAME_FIELD = 'email'
REQUIRED_FIELDS = [] REQUIRED_FIELDS = []
class Meta: class Meta:
verbose_name = _("user") verbose_name = _('user')
verbose_name_plural = _("users") 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): def clean(self):
super().clean() super().clean()
self.email = self.__class__.objects.normalize_email(self.email) 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): def email_user(self, subject, message, from_email=None, **kwargs):
"""Send an email to this user.""" """Send an email to this user."""
send_mail(subject, message, from_email, [self.email], **kwargs) send_mail(subject, message, from_email, [self.email], **kwargs)
@ -114,7 +100,5 @@ class User(AbstractBaseUser, PermissionsMixin):
if not self.confidant: if not self.confidant:
raise MissingUserAttribute("User is missing a confirmed confidant.") raise MissingUserAttribute("User is missing a confirmed confidant.")
if not self.check_password(raw_password): if not self.check_password(raw_password):
raise PasswordMismatch( raise PasswordMismatch("The given password does not match the user's password.")
"The given password does not match the user's password."
)
return ldap.create_account(self.username, raw_password) return ldap.create_account(self.username, raw_password)

View file

@ -3,19 +3,17 @@ from rest_framework import serializers
from userausfall.models import User from userausfall.models import User
class UserActivationSerializer(serializers.Serializer):
password = serializers.CharField()
class UserSerializer(serializers.ModelSerializer): class UserSerializer(serializers.ModelSerializer):
confidant_email = serializers.EmailField() confidant_email = serializers.EmailField(source="get_confidant_email")
class Meta: class Meta:
model = User model = User
fields = ("pk", "email", "username", "confidant_email") fields = ("email", "username", "confidant_email")
def update(self, instance: User, validated_data): def get_confidant_email(self):
confidant_email = validated_data.pop("confidant_email") return ""
confidant, _ = User.objects.get_or_create(email=confidant_email)
instance.confidant_unconfirmed = confidant def update(self, instance, validated_data):
print(validated_data)
confidant = validated_data.pop("get_confidant_email")
return super().update(instance, validated_data) return super().update(instance, validated_data)

View file

@ -1,14 +1,6 @@
from django.urls import path
from rest_framework import routers from rest_framework import routers
from djeveric.rest_api import ConfirmationView
from userausfall.rest_api.views import UserViewSet from userausfall.rest_api.views import UserViewSet
router = routers.DefaultRouter(trailing_slash=True) router = routers.DefaultRouter(trailing_slash=True)
router.register(r'users', UserViewSet, basename="user") router.register(r'users', UserViewSet, basename="user")
urlpatterns = [
path("confirm", ConfirmationView.as_view())
]
urlpatterns += router.urls

View file

@ -1,25 +1,19 @@
from rest_framework import viewsets, status from rest_framework import viewsets
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.response import Response from rest_framework.response import Response
from userausfall.models import User, MissingUserAttribute, PasswordMismatch from userausfall.models import User
from userausfall.rest_api.serializers import UserSerializer, UserActivationSerializer from userausfall.rest_api.serializers import UserSerializer
class UserViewSet(viewsets.ModelViewSet): class UserViewSet(viewsets.ModelViewSet):
class Meta:
queryset = User.objects.all() queryset = User.objects.all()
serializer_class = UserSerializer
@action(detail=True, methods=["post"]) @action(detail=False, methods=["PATCH"])
def activate(self, request, pk=None): def me(self, request):
"""Create the corresponding LDAP account.""" return Response(
user: User = self.get_object() UserSerializer(
serializer = UserActivationSerializer(data=request.data) instance=request.user, context={"request": request}
if serializer.is_valid(): ).data
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)

View file

@ -35,7 +35,6 @@ INSTALLED_APPS = [
'django.contrib.contenttypes', 'django.contrib.contenttypes',
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.sites',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'userausfall', 'userausfall',
'rest_framework', 'rest_framework',

View file

@ -1,26 +0,0 @@
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()

View file

@ -5,6 +5,6 @@ from userausfall.rest_api import urls as rest_api_urls
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path('api/', include("userausfall.rest_api.urls")), path('api/', include(rest_api_urls.router.urls)),
path("api-auth/", include("rest_framework.urls")), path("api-auth/", include("rest_framework.urls")),
] ]