Add pw change endpoint in API

This commit is contained in:
Christian Cueni 2019-04-03 15:53:00 +02:00
parent d3edbae71a
commit d64c641661
9 changed files with 336 additions and 1 deletions

View File

@ -0,0 +1,30 @@
<template>
<div class="pw-reset">
<form class="pw-reset__form reset-form">
<label for="current-pw" class="reset-form__current-label">Aktuells Passwort</label>
<input id="current-pw" type="text" class="reset-form__current">
<label for="new-pw" class="reset-form__new-label">Neues Passwort</label>
<input id="new-pw" type="text" class="reset-form__new">
<button class="reset-form__submit">Zurücksetzen</button>
</form>
</div>
</template>
<script>
export default {
components: {
},
data() {
return {
me: []
}
}
}
</script>
<style scoped lang="scss">
@import "@/styles/_variables.scss";
</style>

View File

@ -0,0 +1,28 @@
<template>
<div class="password-reset">
<h1 class="password-reset__header">Passwort Zurücksetzen</h1>
<password-reset />
</div>
</template>
<script>
import PasswordReset from '@/components/PasswordReset'
export default {
components: {
PasswordReset
},
data() {
return {
me: []
}
}
}
</script>
<style scoped lang="scss">
@import "@/styles/_variables.scss";
</style>

View File

@ -0,0 +1,24 @@
<template>
<div class="profile">
<h1 class="profile__heading">Profile</h1>
</div>
</template>
<script>
export default {
components: {
},
data() {
return {
me: []
}
}
}
</script>
<style scoped lang="scss">
@import "@/styles/_variables.scss";
</style>

View File

@ -18,6 +18,8 @@ import start from '@/pages/start'
import submission from '@/pages/studentSubmission'
import portfolio from '@/pages/portfolio'
import project from '@/pages/project'
import profilePage from '@/pages/profile'
import passwordReset from '@/pages/passwordReset'
const routes = [
{path: '/', component: start, meta: {layout: 'blank'}},
@ -62,6 +64,8 @@ const routes = [
{path: 'topic/:topicSlug', component: topic, meta: {subnavigation: true}}
]
},
{path: '/me', name: 'profile', component: profilePage},
{path: '/password-reset', name: 'pw-reset', component: passwordReset},
{path: '*', component: p404}
];

View File

@ -16,6 +16,7 @@ from objectives.schema import ObjectivesQuery
from rooms.mutations import RoomMutations
from rooms.schema import RoomsQuery
from users.schema import UsersQuery
from users.mutations import ProfileMutations
class Query(UsersQuery, RoomsQuery, ObjectivesQuery, BookQuery, AssignmentsQuery, StudentSubmissionQuery,
@ -27,7 +28,7 @@ class Query(UsersQuery, RoomsQuery, ObjectivesQuery, BookQuery, AssignmentsQuery
class Mutation(BookMutations, RoomMutations, AssignmentMutations, ObjectiveMutations, CoreMutations,
graphene.ObjectType):
ProfileMutations, graphene.ObjectType):
if settings.DEBUG:
debug = graphene.Field(DjangoDebug, name='__debug')

View File

@ -0,0 +1,128 @@
from django.test import TestCase, RequestFactory
from graphene.test import Client
from api.schema import schema
from core.factories import UserFactory
from django.contrib.auth import authenticate
class PasswordUpdate(TestCase):
def setUp(self):
self.user = UserFactory(username='aschi')
request = RequestFactory().get('/')
request.user = self.user
self.client = Client(schema=schema, context_value=request)
def make_request(self, new_password, old_password='test'):
mutation = '''
mutation UpdatePassword($input: UpdatePasswordInput!) {
updatePassword(input: $input) {
success
errors {
field
errors {
code
}
}
}
}
'''
return self.client.execute(mutation, variables={
'input': {
'passwordInput': {
'oldPassword': old_password,
'newPassword': new_password
}
}
})
def test_update_password(self):
new_password = 'Abcd123!'
result = self.make_request(new_password)
self.assertTrue(result.get('data').get('updatePassword').get('success'))
user = authenticate(username=self.user.username, password=new_password)
self.assertIsNotNone(user)
def test_update_fails_with_short_password(self):
new_password = 'Ab!d123'
result = self.make_request(new_password)
self.assertFalse(result.get('data').get('updatePassword').get('success'))
self.assertEquals(result.get('data').get('updatePassword').get('errors')[0], {
'field': 'new_password',
'errors': [
{'code': 'min_length'}
]
})
def test_update_fails_with_no_special_character(self):
new_password = 'Abcd1239'
result = self.make_request(new_password)
self.assertFalse(result.get('data').get('updatePassword').get('success'))
self.assertEquals(result.get('data').get('updatePassword').get('errors')[0], {
'field': 'new_password',
'errors': [
{'code': 'invalid'}
]
})
def test_update_fails_with_no_digit(self):
new_password = 'Abcd!asddfg'
result = self.make_request(new_password)
self.assertFalse(result.get('data').get('updatePassword').get('success'))
self.assertEquals(result.get('data').get('updatePassword').get('errors')[0], {
'field': 'new_password',
'errors': [
{'code': 'invalid'}
]
})
def test_update_fails_with_no_lowercase_char(self):
new_password = '45ABDC!AWSWS'
result = self.make_request(new_password)
self.assertFalse(result.get('data').get('updatePassword').get('success'))
self.assertEquals(result.get('data').get('updatePassword').get('errors')[0], {
'field': 'new_password',
'errors': [
{'code': 'invalid'}
]
})
def test_update_fails_with_no_uppercase_char(self):
new_password = '45aswed!aswdef'
result = self.make_request(new_password)
self.assertFalse(result.get('data').get('updatePassword').get('success'))
self.assertEquals(result.get('data').get('updatePassword').get('errors')[0], {
'field': 'new_password',
'errors': [
{'code': 'invalid'}
]
})
def test_update_fails_with_wrong_old_password(self):
new_password = 'Abcd123!'
result = self.make_request(new_password, 'testttt')
self.assertFalse(result.get('data').get('updatePassword').get('success'))
self.assertEquals(result.get('data').get('updatePassword').get('errors')[0], {
'field': 'old_password',
'errors': [
{'code': 'invalid'}
]
})

View File

@ -6,3 +6,8 @@ class SchoolClassInput(InputObjectType):
id = graphene.ID()
name = graphene.String()
year = graphene.Int()
class PasswordUpdateInput(InputObjectType):
old_password = graphene.String()
new_password = graphene.String()

47
server/users/mutations.py Normal file
View File

@ -0,0 +1,47 @@
import graphene
from graphene import relay
from users.inputs import PasswordUpdateInput
from users.serializers import PasswordSerialzer
class FieldError(graphene.ObjectType):
code = graphene.String()
class UpdateError(graphene.ObjectType):
field = graphene.String()
errors = graphene.List(FieldError)
class UpdatePassword(relay.ClientIDMutation):
class Input:
password_input = graphene.Argument(PasswordUpdateInput)
success = graphene.Boolean()
errors = graphene.List(UpdateError)
@classmethod
def mutate_and_get_payload(cls, root, info, **kwargs):
user = info.context.user
password_data = kwargs.get('password_input')
serializer = PasswordSerialzer(data=password_data, context=user)
if serializer.is_valid():
user.set_password(password_data['new_password'])
user.save()
return cls(success=True)
errors = []
for key, value in serializer.errors.items():
error = UpdateError(field=key, errors=[])
for field_error in serializer.errors[key]:
error.errors.append(FieldError(code=field_error.code))
errors.append(error)
return cls(success=False, errors=errors)
class ProfileMutations:
update_password = UpdatePassword.Field()

View File

@ -0,0 +1,68 @@
# -*- coding: utf-8 -*-
#
# ITerativ GmbH
# http://www.iterativ.ch/
#
# Copyright (c) 2019 ITerativ GmbH. All rights reserved.
#
# Created on 2019-04-02
# @author: chrigu <christian.cueni@iterativ.ch>
import re
from django.contrib.auth import get_user_model
from rest_framework import serializers
from rest_framework.fields import CharField
from django.utils.translation import ugettext_lazy as _
MIN_PASSWORD_LENGTH = 8
# For future versions https://docs.djangoproject.com/en/2.1/topics/auth/passwords/#integrating-validation
def validate_old_password(old_password, username):
user = get_user_model().objects.get(username=username)
if user.check_password(old_password):
return old_password
else:
raise serializers.ValidationError(_(u'Das eingegebene Passwort ist falsch'))
def validate_old_new_password(value):
if value.get('old_password') == '' and value.get('new_password') == '':
return value
elif value.get('old_password') == '' and value.get('new_password') != '':
raise serializers.ValidationError(_(u'Das neue Passwort muss gesetzt werden'))
elif value.get('old_password') != '' and value.get('new_password') == '':
raise serializers.ValidationError(_(u'Das alte Passwort muss angegeben werden'))
return value
def validate_strong_email(password):
if len(password) == 0:
return password
has_number = re.search('\d', password)
has_upper = re.search('[A-Z]', password)
has_lower = re.search('[a-z]', password)
has_special = re.search('[!@#$%^&*(),.?":{}|<>\+]', password)
if has_number and has_upper and has_lower and has_special:
return password
else:
raise serializers.ValidationError(_(u'Das Passwort muss Grossbuchstaben, Zahlen und Sonderzeichen beinhalten'))
class PasswordSerialzer(serializers.Serializer):
old_password = CharField(allow_blank=True)
new_password = CharField(allow_blank=True, min_length=MIN_PASSWORD_LENGTH)
def validate_new_password(self, value):
return validate_strong_email(value)
def validate_old_password(self, value):
return validate_old_password(value, self.context.username)
def validate(self, obj):
return validate_old_new_password(obj)