diff --git a/django_rest_multitokenauth/admin.py b/django_rest_multitokenauth/admin.py index e986b2f..7ff1d02 100644 --- a/django_rest_multitokenauth/admin.py +++ b/django_rest_multitokenauth/admin.py @@ -1,8 +1,13 @@ """ contains basic admin views for MultiToken """ from django.contrib import admin -from django_rest_multitokenauth.models import MultiToken +from django_rest_multitokenauth.models import MultiToken, ResetPasswordToken @admin.register(MultiToken) class MultiTokenAdmin(admin.ModelAdmin): list_display = ('user', 'key', 'user_agent') + + +@admin.register(ResetPasswordToken) +class ResetPasswordTokenAdmin(admin.ModelAdmin): + list_display = ('user', 'key', 'created_at', 'ip_address', 'user_agent') \ No newline at end of file diff --git a/django_rest_multitokenauth/migrations/0003_resetpasswordtoken.py b/django_rest_multitokenauth/migrations/0003_resetpasswordtoken.py new file mode 100644 index 0000000..2ba110d --- /dev/null +++ b/django_rest_multitokenauth/migrations/0003_resetpasswordtoken.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-01-18 15:30 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('django_rest_multitokenauth', '0002_rename_ip_address_20160426'), + ] + + operations = [ + migrations.CreateModel( + name='ResetPasswordToken', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='When was this token generated')), + ('key', models.CharField(max_length=64, primary_key=True, serialize=False, verbose_name='Key')), + ('ip_address', models.GenericIPAddressField(default='127.0.0.1', verbose_name='The IP address of this session')), + ('user_agent', models.CharField(default='', max_length=256, verbose_name='HTTP User Agent')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='password_reset_tokens', to=settings.AUTH_USER_MODEL, verbose_name='The User which is associated to this password reset token')), + ], + options={ + 'verbose_name': 'Password Reset Token', + 'verbose_name_plural': 'Password Reset Tokens', + }, + ), + ] diff --git a/django_rest_multitokenauth/models.py b/django_rest_multitokenauth/models.py index c800f49..f0bf5d9 100644 --- a/django_rest_multitokenauth/models.py +++ b/django_rest_multitokenauth/models.py @@ -67,3 +67,51 @@ def generate_key(): def __str__(self): return self.key + " (user " + str(self.user) + " with IP " + self.last_known_ip + \ " and user agent " + self.user_agent + ")" + + +@python_2_unicode_compatible +class ResetPasswordToken(models.Model): + class Meta: + verbose_name = _("Password Reset Token") + verbose_name_plural = _("Password Reset Tokens") + + @staticmethod + def generate_key(): + """ generates a pseudo random code using os.urandom and binascii.hexlify """ + return binascii.hexlify(os.urandom(32)).decode() + + user = models.ForeignKey( + AUTH_USER_MODEL, + related_name='password_reset_tokens', + on_delete=models.CASCADE, + verbose_name=_("The User which is associated to this password reset token") + ) + + created_at = models.DateTimeField( + auto_now_add=True, + verbose_name=_("When was this token generated") + ) + + key = models.CharField( + _("Key"), + max_length=64, + primary_key=True + ) + + ip_address = models.GenericIPAddressField( + _("The IP address of this session"), + default="127.0.0.1" + ) + user_agent = models.CharField( + max_length=256, + verbose_name=_("HTTP User Agent"), + default="" + ) + + def save(self, *args, **kwargs): + if not self.key: + self.key = self.generate_key() + return super(ResetPasswordToken, self).save(*args, **kwargs) + + def __str__(self): + return "Password reset token for user {user}".format(user=self.user) diff --git a/django_rest_multitokenauth/serializers.py b/django_rest_multitokenauth/serializers.py new file mode 100644 index 0000000..80183e5 --- /dev/null +++ b/django_rest_multitokenauth/serializers.py @@ -0,0 +1,5 @@ +from rest_framework import serializers + + +class EmailSerializer(serializers.Serializer): + email = serializers.EmailField() \ No newline at end of file diff --git a/django_rest_multitokenauth/signals.py b/django_rest_multitokenauth/signals.py new file mode 100644 index 0000000..89ef30e --- /dev/null +++ b/django_rest_multitokenauth/signals.py @@ -0,0 +1,3 @@ +import django.dispatch + +reset_password_token_created = django.dispatch.Signal(providing_args=["reset_password_token"]) diff --git a/django_rest_multitokenauth/urls.py b/django_rest_multitokenauth/urls.py index 04f5ed5..15783f7 100644 --- a/django_rest_multitokenauth/urls.py +++ b/django_rest_multitokenauth/urls.py @@ -1,9 +1,11 @@ """ URL Configuration for core auth """ from django.conf.urls import url, include -from django_rest_multitokenauth.views import login_and_obtain_auth_token, logout_and_delete_auth_token +from django_rest_multitokenauth.views import login_and_obtain_auth_token, logout_and_delete_auth_token, reset_password_request_token, reset_password_confirm urlpatterns = [ url(r'^login', login_and_obtain_auth_token), # normal login with session url(r'^logout', logout_and_delete_auth_token), + url(r'^reset_password', reset_password_request_token), + url(r'^reset_password/confirm', reset_password_confirm) ] diff --git a/django_rest_multitokenauth/views.py b/django_rest_multitokenauth/views.py index 6d4378c..cde0a59 100644 --- a/django_rest_multitokenauth/views.py +++ b/django_rest_multitokenauth/views.py @@ -1,11 +1,18 @@ +from django.contrib.auth.models import User +from django.http import Http404 +from django.utils.translation import ugettext_lazy as _ + from rest_framework import parsers, renderers from rest_framework.authtoken.serializers import AuthTokenSerializer from rest_framework.response import Response from rest_framework.views import APIView from rest_framework.authentication import get_authorization_header +from django.core.exceptions import ValidationError from django_rest_multitokenauth.models import MultiToken - +from django_rest_multitokenauth.serializers import EmailSerializer +from django_rest_multitokenauth.models import ResetPasswordToken +from django_rest_multitokenauth.signals import reset_password_token_created class LogoutAndDeleteAuthToken(APIView): """ Custom API View for logging out""" @@ -50,5 +57,53 @@ def post(self, request, *args, **kwargs): return Response({'error': 'not logged in'}) +class ResetPasswordConfirm(APIView): + pass + + +class ResetPasswordRequestToken(APIView): + """ + An Api View which provides a method to request a password reset token based on an e-mail address + + Sends a signal reset_password_token_created when a reset token was created + """ + throttle_classes = () + permission_classes = () + parser_classes = (parsers.FormParser, parsers.MultiPartParser, parsers.JSONParser,) + renderer_classes = (renderers.JSONRenderer,) + serializer_class = EmailSerializer + + def post(self, request, *args, **kwargs): + serializer = self.serializer_class(data=request.data) + serializer.is_valid(raise_exception=True) + email = serializer.validated_data['email'] + + # find a user by email address + users = User.objects.filter(email=email) + + active_user_found = False + + for user in users: + if user.is_active: + active_user_found = True + + if not active_user_found: + raise ValidationError({ + 'email': ValidationError(_("There is no active user associated with this e-mail address"), code='invalid')}) + + for user in users: + if user.is_active: + token = ResetPasswordToken.objects.create( + user=user, + user_agent=request.META['HTTP_USER_AGENT'], + ip_address=request.META['REMOTE_ADDR'] + ) + # send a signal that the password token was created, let whoever receives this signal handle sending the email + reset_password_token_created.send(sender=self.__class__, reset_password_token=token) + return Response({'status': 'OK'}) + + login_and_obtain_auth_token = LoginAndObtainAuthToken.as_view() logout_and_delete_auth_token = LogoutAndDeleteAuthToken.as_view() +reset_password_confirm = ResetPasswordConfirm.as_view() +reset_password_request_token = ResetPasswordRequestToken.as_view() diff --git a/setup.py b/setup.py index 6209e68..1da7d44 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( name='django-rest-multitokenauth', - version='0.1.1', + version='0.1.2', packages=find_packages(), include_package_data=True, license='BSD License',