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 submission from '@/pages/studentSubmission'
import portfolio from '@/pages/portfolio' import portfolio from '@/pages/portfolio'
import project from '@/pages/project' import project from '@/pages/project'
import profilePage from '@/pages/profile'
import passwordReset from '@/pages/passwordReset'
const routes = [ const routes = [
{path: '/', component: start, meta: {layout: 'blank'}}, {path: '/', component: start, meta: {layout: 'blank'}},
@ -62,6 +64,8 @@ const routes = [
{path: 'topic/:topicSlug', component: topic, meta: {subnavigation: true}} {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} {path: '*', component: p404}
]; ];

View File

@ -16,6 +16,7 @@ from objectives.schema import ObjectivesQuery
from rooms.mutations import RoomMutations from rooms.mutations import RoomMutations
from rooms.schema import RoomsQuery from rooms.schema import RoomsQuery
from users.schema import UsersQuery from users.schema import UsersQuery
from users.mutations import ProfileMutations
class Query(UsersQuery, RoomsQuery, ObjectivesQuery, BookQuery, AssignmentsQuery, StudentSubmissionQuery, 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, class Mutation(BookMutations, RoomMutations, AssignmentMutations, ObjectiveMutations, CoreMutations,
graphene.ObjectType): ProfileMutations, graphene.ObjectType):
if settings.DEBUG: if settings.DEBUG:
debug = graphene.Field(DjangoDebug, name='__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() id = graphene.ID()
name = graphene.String() name = graphene.String()
year = graphene.Int() 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)