Browse Source

password reset is working

master
Holger Frey 7 years ago
parent
commit
b23c214df1
  1. 3
      ordr2/models/account.py
  2. 18
      ordr2/schemas/account.py
  3. 37
      ordr2/templates/account/password_resetted.jinja2
  4. 15
      ordr2/templates/account/reset_password.jinja2
  5. 25
      ordr2/templates/emails/password_reset.jinja2
  6. 50
      ordr2/views/account.py
  7. 9
      tests/_functional/login_logout.py
  8. 50
      tests/_functional/reset_password.py
  9. 2
      tests/resources/account.py
  10. 65
      tests/views/account.py

3
ordr2/models/account.py

@ -164,8 +164,7 @@ class User(Base):
:rtype: :rtype:
(str) unique hash to access the token (str) unique hash to access the token
''' '''
token = Token.issue(request, self, subject, payload) return Token.issue(request, self, subject, payload)
return token.hash
def __str__(self): def __str__(self):
''' string representation ''' ''' string representation '''

18
ordr2/schemas/account.py

@ -47,3 +47,21 @@ class RegistrationSchema(CSRFSchema):
} }
settings.update(override) settings.update(override)
return super().as_form(request, **settings) return super().as_form(request, **settings)
class ResetPasswordSchema(CSRFSchema):
''' reset a password '''
password = colander.SchemaNode(
colander.String(),
widget=deform.widget.CheckedPasswordWidget()
)
@classmethod
def as_form(cls, request, **override):
settings = {
'buttons': ('Create Account', 'Cancel'),
'css_class': 'form reset-password'
}
settings.update(override)
return super().as_form(request, **settings)

37
ordr2/templates/account/password_resetted.jinja2

@ -0,0 +1,37 @@
{% extends "ordr2:templates/layout.jinja2" %}
{% block title %} Ordr | Reset Password {% endblock title %}
{% block content %}
<div class="row">
<div class="col-2"></div>
<div class="col-5">
<h1>Password Reset Successful</h1>
<p>You can now log on with your new password</p>
<form action="{{ request.resource_url(request.root, 'account', 'login') }}" method="POST" id="login-form">
<input type="hidden" name="csrf_token" value="{{ get_csrf_token() }}">
<div class="form-group row">
<label for="username" class="col-2">Username</label>
<div class="col-6">
<input name="username" id="username" type="text" class="form-control">
</div>
</div>
<div class="form-group row">
<label for="password" class="col-2">Password</label>
<div class="col-6">
<input name="password" id="password>" type="password" class="form-control">
</div>
</div>
<div class="form-group row">
<div class="col-2"></div>
<div class="col-6">
<button type="submit" class="btn btn-sm btn-primary">Log in</button>
</div>
</div>
</form>
</div>
</div>
{% endblock content %}

15
ordr2/templates/account/reset_password.jinja2

@ -0,0 +1,15 @@
{% extends "ordr2:templates/layout.jinja2" %}
{% block title %} Ordr | Reset Password {% endblock title %}
{% block content %}
<div class="row">
<div class="col-2"></div>
<div class="col-5">
<h1>Reset Your Password</h1>
{{ form.render()|safe }}
</div>
</div>
{% endblock content %}

25
ordr2/templates/emails/password_reset.jinja2

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>[ordr] reset your password</title>
<link href='http://fonts.googleapis.com/css?family=Anton&subset=latin,latin-ext' rel='stylesheet' type='text/css'>
</head>
<body>
<h1>Hi there!</h1>
<p>
To set a new password for the account "{{ user.user_name }}" follow this link
<a href="{{ request.resource_url(context, data.token.hash) }}">{{ request.resource_url(context, data.token.hash) }}</a>
</p>
<p> The link will expire on {{ data.token.expires.strftime('%d.%m.%y at %H:%M') }}.
<p class="signature">
Regards,
<br/>
<span class="brand">ordr</span>
</p>
<p class="footprint">
<small>Please don't respont to this email! This is an automatically generated notification by the system.</small>
<a href="http://distractedbysquirrels.com/" target="_blank" title="This software was originally written by Sebastian Sebald." class="icon-dbs"></a>
</p>
</body>
</html>

50
ordr2/views/account.py

@ -9,7 +9,7 @@ from sqlalchemy import or_
from ordr2.events import CompleteRegistration, PasswordReset from ordr2.events import CompleteRegistration, PasswordReset
from ordr2.models.account import User, Role, TokenSubject from ordr2.models.account import User, Role, TokenSubject
from ordr2.schemas.account import RegistrationSchema from ordr2.schemas.account import RegistrationSchema, ResetPasswordSchema
PROPOSED_PASSWORD_LENGTH = 12 PROPOSED_PASSWORD_LENGTH = 12
@ -20,7 +20,6 @@ PROPOSED_PASSWORD_LENGTH = 12
@view_config( @view_config(
context='ordr2:resources.account.AccountResource', context='ordr2:resources.account.AccountResource',
name='login', name='login',
request_method='POST',
permission='login', permission='login',
renderer='ordr2:templates/account/login.jinja2' renderer='ordr2:templates/account/login.jinja2'
) )
@ -195,3 +194,50 @@ def forgot_password_form_processing(context, request):
def forgot_password_email_sent(context, request): def forgot_password_email_sent(context, request):
''' password reset link was sent ''' ''' password reset link was sent '''
return {} return {}
@view_config(
context='ordr2:resources.account.ForgottenPasswordToken',
request_method='GET',
permission='reset password',
renderer='ordr2:templates/account/reset_password.jinja2'
)
def reset_password_form(context, request):
form = ResetPasswordSchema.as_form(request)
return {'form': form}
@view_config(
context='ordr2:resources.account.ForgottenPasswordToken',
request_method='POST',
permission='reset password',
renderer='ordr2:templates/account/reset_password.jinja2'
)
def reset_password_form_processing(context, request):
if 'Cancel' in request.POST:
return HTTPFound(request.resource_url(request.root))
# validate the form data
form = ResetPasswordSchema.as_form(request)
data = request.POST.items()
try:
appstruct = form.validate(data)
except deform.ValidationFailure as e:
return {'form': form}
# set the new password
context.model.owner.set_password(appstruct['password'])
# delete the token
request.dbsession.delete(context.model)
# issue a warning on a short password
if len(appstruct['password']) < PROPOSED_PASSWORD_LENGTH:
request.session.flash(
'warning',
'You should really consider a longer password'
)
request.session.flash('OK', 'Your password was changed' )
return HTTPFound(request.resource_url(context.__parent__, 'login'))

9
tests/_functional/login-logout.py → tests/_functional/login_logout.py

@ -25,15 +25,6 @@ def assert_user_login_failed(response, username):
# tests for login and logout of users # tests for login and logout of users
def test_account_login_only_by_post(testapp):
''' test that the login view is not accessibal via get '''
testapp.reset()
response = testapp.get('/account/login', status=404)
assert response.status.startswith('404')
def test_account_login_for_active_users(testapp): def test_account_login_for_active_users(testapp):
''' check if user login works ''' ''' check if user login works '''
testapp.reset() testapp.reset()

50
tests/_functional/reset_password.py

@ -0,0 +1,50 @@
''' tests for the login, logout, registration and account settings'''
import pytest
from pyramid_mailer import get_mailer
from . import testapp, get_token_url
from .. import get_user
@pytest.mark.xfail
def test_reset_password(testapp):
''' test the complete reset password process '''
# submit the registration form
response = testapp.get('/account/forgot-password')
form = response.forms[1]
form['username_or_email'] = 'TerryGilliam'
response = form.submit()
assert response.location == 'http://localhost/account/forgot-password-email'
response = response.follow()
assert 'Password Reset Link' in response
# click the email verification token
mailer = get_mailer(testapp.app.registry)
email = mailer.outbox[-1]
assert email.subject == '[ordr] Password Reset'
token_link = get_token_url(email)
response = testapp.get(token_link)
form = response.forms[1]
form['password'] = 'Nudge Nudge'
form['password-confirm'] = 'Nudge Nudge'
response = form.submit()
assert response.location == 'http://localhost/account/login'
response = response.follow()
assert 'consider a longer password' in response
assert 'Your password was changed' in response
form = response.forms[1]
form['username'] = 'TerryGilliam'
form['password'] = 'Nudge Nudge'
response = form.submit().follow()
assert '<!-- user is logged in -->' in response

2
tests/resources/account.py

@ -147,3 +147,5 @@ def test_account_resource_getitem_token_expired(dbsession):
with pytest.raises(KeyError) as excinfo: with pytest.raises(KeyError) as excinfo:
resource = account[token.hash] resource = account[token.hash]
assert f'Token {token.hash} has expired' in str(excinfo.value) assert f'Token {token.hash} has expired' in str(excinfo.value)

65
tests/views/account.py

@ -285,3 +285,68 @@ def test_forgot_password_email_sent():
result = forgot_password_email_sent(None, None) result = forgot_password_email_sent(None, None)
assert result == {} assert result == {}
def test_reset_password_form():
''' reset password form display '''
from ordr2.views.account import reset_password_form
request = DummyRequest()
result = reset_password_form(None, request)
assert isinstance(result['form'], deform.Form)
def reset_password_form_processing_ok():
''' reset password form processing is ok '''
from ordr2.models.account import TokenSubject
from ordr2.views.account import reset_password_form_processing
account = get_user('user')
token = user.issue_token(request, TokenSubject.RESET_PASSWORD)
dbsession.add(account)
dbsession.flush()
context = DummyResource(model=token)
request = DummyRequest(
dbsession=dbsession,
POST={'password': 'Nudge', 'password-confirmation': 'Nudge'}
)
result = reset_password_form_processing(context, request)
assert isinstance(result, HTTPFound)
assert result.location == 'http://example.com/account/login'
assert account.check_password('Nudge')
assert dbsession.query(Token).count() == 0
assert dbsession.query(User).count() == 1
def reset_password_form_processing_cancel():
''' reset password form processing is canceled '''
from ordr2.views.account import reset_password_form_processing
request = DummyRequest(dbsession=dbsession, POST={'Cancel': 'cancel'})
result = reset_password_form_processing(None, request)
assert isinstance(result, HTTPFound)
assert result.location == 'http://example.com//'
@pytest.mark.parametrize(
'pw, confirm', [
('', ''),
('no', 'match'),
('one is empty', ''),
('', 'one is empty'),
]
)
def reset_password_form_processing_invalid(pw, confirm):
''' validation error in reset password form '''
from ordr2.views.account import reset_password_form_processing
request = DummyRequest(
dbsession=dbsession,
POST={'password': pw, 'password-confirmation': confirm}
)
result = reset_password_form_processing(context, request)
assert isinstance(result['form'], deform.Form)