Compare commits
2 commits
d75ae86409
...
73c6fbf165
Author | SHA1 | Date | |
---|---|---|---|
73c6fbf165 | |||
2bc9b5ba85 |
11 changed files with 231 additions and 47 deletions
0
djeveric/__init__.py
Normal file
0
djeveric/__init__.py
Normal file
54
djeveric/rest_api.py
Normal file
54
djeveric/rest_api.py
Normal file
|
@ -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
|
34
djeveric/signals.py
Normal file
34
djeveric/signals.py
Normal file
|
@ -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]
|
||||||
|
)
|
37
userausfall/migrations/0006_auto_20210521_0805.py
Normal file
37
userausfall/migrations/0006_auto_20210521_0805.py
Normal file
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -11,11 +11,13 @@ 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
|
||||||
|
|
||||||
|
|
||||||
|
@ -34,18 +36,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)
|
||||||
|
|
||||||
|
@ -54,41 +56,53 @@ 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=_('Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.'),
|
help_text=_(
|
||||||
|
"Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only."
|
||||||
|
),
|
||||||
validators=[username_validator],
|
validators=[username_validator],
|
||||||
error_messages={
|
error_messages={"unique": _("A user with that username already exists.")},
|
||||||
'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)
|
||||||
|
@ -100,5 +114,7 @@ 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("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)
|
return ldap.create_account(self.username, raw_password)
|
||||||
|
|
|
@ -3,17 +3,19 @@ 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(source="get_confidant_email")
|
confidant_email = serializers.EmailField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = User
|
||||||
fields = ("email", "username", "confidant_email")
|
fields = ("pk", "email", "username", "confidant_email")
|
||||||
|
|
||||||
def get_confidant_email(self):
|
def update(self, instance: User, validated_data):
|
||||||
return ""
|
confidant_email = validated_data.pop("confidant_email")
|
||||||
|
confidant, _ = User.objects.get_or_create(email=confidant_email)
|
||||||
def update(self, instance, validated_data):
|
instance.confidant_unconfirmed = confidant
|
||||||
print(validated_data)
|
|
||||||
confidant = validated_data.pop("get_confidant_email")
|
|
||||||
return super().update(instance, validated_data)
|
return super().update(instance, validated_data)
|
||||||
|
|
|
@ -1,6 +1,14 @@
|
||||||
|
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
|
||||||
|
|
|
@ -1,19 +1,25 @@
|
||||||
from rest_framework import viewsets
|
from rest_framework import viewsets, status
|
||||||
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
|
from userausfall.models import User, MissingUserAttribute, PasswordMismatch
|
||||||
from userausfall.rest_api.serializers import UserSerializer
|
from userausfall.rest_api.serializers import UserSerializer, UserActivationSerializer
|
||||||
|
|
||||||
|
|
||||||
class UserViewSet(viewsets.ModelViewSet):
|
class UserViewSet(viewsets.ModelViewSet):
|
||||||
class Meta:
|
queryset = User.objects.all()
|
||||||
queryset = User.objects.all()
|
serializer_class = UserSerializer
|
||||||
|
|
||||||
@action(detail=False, methods=["PATCH"])
|
@action(detail=True, methods=["post"])
|
||||||
def me(self, request):
|
def activate(self, request, pk=None):
|
||||||
return Response(
|
"""Create the corresponding LDAP account."""
|
||||||
UserSerializer(
|
user: User = self.get_object()
|
||||||
instance=request.user, context={"request": request}
|
serializer = UserActivationSerializer(data=request.data)
|
||||||
).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)
|
||||||
|
|
|
@ -35,6 +35,7 @@ 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',
|
||||||
|
|
|
@ -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()
|
|
@ -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(rest_api_urls.router.urls)),
|
path('api/', include("userausfall.rest_api.urls")),
|
||||||
path("api-auth/", include("rest_framework.urls")),
|
path("api-auth/", include("rest_framework.urls")),
|
||||||
]
|
]
|
||||||
|
|
Reference in a new issue