Skip to content

Commit

Permalink
Added a password reset token apiview and a signal for it
Browse files Browse the repository at this point in the history
  • Loading branch information
anx-ckreuzberger committed Jan 18, 2017
1 parent 2b7d549 commit 959605d
Show file tree
Hide file tree
Showing 8 changed files with 154 additions and 4 deletions.
7 changes: 6 additions & 1 deletion django_rest_multitokenauth/admin.py
Original file line number Diff line number Diff line change
@@ -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')
32 changes: 32 additions & 0 deletions django_rest_multitokenauth/migrations/0003_resetpasswordtoken.py
Original file line number Diff line number Diff line change
@@ -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',
},
),
]
48 changes: 48 additions & 0 deletions django_rest_multitokenauth/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
5 changes: 5 additions & 0 deletions django_rest_multitokenauth/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from rest_framework import serializers


class EmailSerializer(serializers.Serializer):
email = serializers.EmailField()
3 changes: 3 additions & 0 deletions django_rest_multitokenauth/signals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import django.dispatch

reset_password_token_created = django.dispatch.Signal(providing_args=["reset_password_token"])
4 changes: 3 additions & 1 deletion django_rest_multitokenauth/urls.py
Original file line number Diff line number Diff line change
@@ -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)
]
57 changes: 56 additions & 1 deletion django_rest_multitokenauth/views.py
Original file line number Diff line number Diff line change
@@ -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"""
Expand Down Expand Up @@ -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()
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down

0 comments on commit 959605d

Please sign in to comment.