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

18
ordr2/schemas/account.py

@ -47,3 +47,21 @@ class RegistrationSchema(CSRFSchema): @@ -47,3 +47,21 @@ class RegistrationSchema(CSRFSchema):
}
settings.update(override)
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 @@ @@ -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 @@ @@ -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 @@ @@ -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_ @@ -9,7 +9,7 @@ from sqlalchemy import or_
from ordr2.events import CompleteRegistration, PasswordReset
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
@ -20,7 +20,6 @@ PROPOSED_PASSWORD_LENGTH = 12 @@ -20,7 +20,6 @@ PROPOSED_PASSWORD_LENGTH = 12
@view_config(
context='ordr2:resources.account.AccountResource',
name='login',
request_method='POST',
permission='login',
renderer='ordr2:templates/account/login.jinja2'
)
@ -195,3 +194,50 @@ def forgot_password_form_processing(context, request): @@ -195,3 +194,50 @@ def forgot_password_form_processing(context, request):
def forgot_password_email_sent(context, request):
''' password reset link was sent '''
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): @@ -25,15 +25,6 @@ def assert_user_login_failed(response, username):
# 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):
''' check if user login works '''
testapp.reset()

50
tests/_functional/reset_password.py

@ -0,0 +1,50 @@ @@ -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): @@ -147,3 +147,5 @@ def test_account_resource_getitem_token_expired(dbsession):
with pytest.raises(KeyError) as excinfo:
resource = account[token.hash]
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(): @@ -285,3 +285,68 @@ def test_forgot_password_email_sent():
result = forgot_password_email_sent(None, None)
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)