Holger Frey
7 years ago
10 changed files with 455 additions and 10 deletions
@ -0,0 +1,44 @@ |
|||||||
|
{% extends "ordr:templates/layout.jinja2" %} |
||||||
|
|
||||||
|
{% block content %} |
||||||
|
<div class="row justify-content-md-center mt-3"> |
||||||
|
<div class="col-6"> |
||||||
|
<h1>Forgot Your Password?</h1> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="row justify-content-md-center mt-3"> |
||||||
|
<div class="col-2"> |
||||||
|
<p class="text-primary"> |
||||||
|
Step 1: Validate Account |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
<div class="col-2"> |
||||||
|
<p class="text-secondary"> |
||||||
|
Step 2: Change Password |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
<div class="col-2"> |
||||||
|
<p class="text-secondary"> |
||||||
|
Step 3: Finished |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="row justify-content-md-center mt-3"> |
||||||
|
<div class="col-6 mt-3"> |
||||||
|
<p>Please enter your mail address or your username to reset your password.</p> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="row justify-content-md-center"> |
||||||
|
<div class="col-6"> |
||||||
|
<form action="{{request.resource_url(context)}}" method="POST"> |
||||||
|
<div class="form-group form-row mt-3"> |
||||||
|
<input type="hidden" name="csrf_token" value="{{get_csrf_token()}}"> |
||||||
|
<input type="text" class="form-control {% if loginerror %}is-invalid{% endif %}" id="input-username" placeholder="Mail Address or Username" name="identifier" autofocus="autofocus"> |
||||||
|
</div> |
||||||
|
<div class="form-group form-row mt-5"> |
||||||
|
<button type="submit" name="send_mail" class="btn btn-primary mr-1">Send Reset Link</button> |
||||||
|
<button type="submit" name="cancel" class="btn btn-outline-secondary">Cancel</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{% endblock content %} |
@ -0,0 +1,35 @@ |
|||||||
|
{% extends "ordr:templates/layout.jinja2" %} |
||||||
|
|
||||||
|
{% block title %} Ordr | Registration {% endblock title %} |
||||||
|
|
||||||
|
{% block content %} |
||||||
|
<div class="row justify-content-md-center mt-3"> |
||||||
|
<div class="col-6"> |
||||||
|
<h1>Forgot Your Password?</h1> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="row justify-content-md-center mt-3"> |
||||||
|
<div class="col-2"> |
||||||
|
<p class="text-primary"> |
||||||
|
Step 1: Validate Account |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
<div class="col-2"> |
||||||
|
<p class="text-secondary"> |
||||||
|
Step 2: Change Password |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
<div class="col-2"> |
||||||
|
<p class="text-secondary"> |
||||||
|
Step 3: Finished |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="row justify-content-md-center mt-3"> |
||||||
|
<div class="col-6"> |
||||||
|
<h3>Verify Your Email Address</h3> |
||||||
|
<p class="mt-3">To continue the process, an email has been sent to you.</p> |
||||||
|
<p>Please follow the link in the email to verify your account.</p> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{% endblock content %} |
@ -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.username }}" 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> |
@ -0,0 +1,103 @@ |
|||||||
|
import deform |
||||||
|
|
||||||
|
from pyramid.httpexceptions import HTTPFound |
||||||
|
from pyramid.view import view_config |
||||||
|
from sqlalchemy import func, or_ |
||||||
|
|
||||||
|
from ordr.models.account import User, Role, TokenSubject |
||||||
|
from ordr.events import PasswordResetNotification |
||||||
|
|
||||||
|
# below this password length a warning is displayed |
||||||
|
MIN_PW_LENGTH = 12 |
||||||
|
|
||||||
|
|
||||||
|
@view_config( |
||||||
|
context='ordr.resources.account.PasswordResetResource', |
||||||
|
permission='view', |
||||||
|
request_method='GET', |
||||||
|
renderer='ordr:templates/account/forgotten_password_form.jinja2' |
||||||
|
) |
||||||
|
def forgotten_password_form(context, request): |
||||||
|
''' show forgotten password form ''' |
||||||
|
return {'formerror': False} |
||||||
|
|
||||||
|
|
||||||
|
@view_config( |
||||||
|
context='ordr.resources.account.PasswordResetResource', |
||||||
|
permission='view', |
||||||
|
request_method='POST', |
||||||
|
renderer='ordr:templates/account/forgotten_password_form.jinja2' |
||||||
|
) |
||||||
|
def forgotten_password_form_processing(context, request): |
||||||
|
''' process forgotten password form ''' |
||||||
|
if 'cancel' in request.POST: |
||||||
|
return HTTPFound(request.resource_url(request.root)) |
||||||
|
identifier = request.POST.get('identifier', '') |
||||||
|
account = ( |
||||||
|
request.dbsession |
||||||
|
.query(User) |
||||||
|
.filter(or_( |
||||||
|
func.lower(User.username) == identifier.lower(), |
||||||
|
func.lower(User.email) == identifier.lower() |
||||||
|
) |
||||||
|
) |
||||||
|
.first() |
||||||
|
) |
||||||
|
if account is None or not account.is_active: |
||||||
|
return {'formerror': True} |
||||||
|
|
||||||
|
# create a verify-new-account token and send email |
||||||
|
token = account.issue_token(request, TokenSubject.RESET_PASSWORD) |
||||||
|
notification = PasswordResetNotification( |
||||||
|
request, |
||||||
|
account, |
||||||
|
{'token': token} |
||||||
|
) |
||||||
|
request.registry.notify(notification) |
||||||
|
|
||||||
|
return HTTPFound(request.resource_url(context, 'verify')) |
||||||
|
|
||||||
|
|
||||||
|
@view_config( |
||||||
|
context='ordr.resources.account.PasswordResetResource', |
||||||
|
name='verify', |
||||||
|
permission='view', |
||||||
|
request_method='GET', |
||||||
|
renderer='ordr:templates/account/forgotten_password_verify.jinja2' |
||||||
|
) |
||||||
|
def verify(context, request): |
||||||
|
''' show email verification text ''' |
||||||
|
return {} |
||||||
|
|
||||||
|
|
||||||
|
@view_config( |
||||||
|
context='ordr.resources.account.PasswordResetTokenResource', |
||||||
|
permission='view', |
||||||
|
request_method='GET', |
||||||
|
renderer='ordr:templates/account/forgotten_password_reset.jinja2' |
||||||
|
) |
||||||
|
def reset_password_form(context, request): |
||||||
|
''' user is verified, show reset password form ''' |
||||||
|
raise NotImplemented() |
||||||
|
|
||||||
|
|
||||||
|
@view_config( |
||||||
|
context='ordr.resources.account.PasswordResetTokenResource', |
||||||
|
permission='view', |
||||||
|
request_method='POST', |
||||||
|
renderer='ordr:templates/account/forgotten_password_reset.jinja2' |
||||||
|
) |
||||||
|
def reset_password_form_processing(context, request): |
||||||
|
''' user is verified, process reset password form ''' |
||||||
|
raise NotImplemented() |
||||||
|
|
||||||
|
|
||||||
|
@view_config( |
||||||
|
context='ordr.resources.account.PasswordResetTokenResource', |
||||||
|
permission='view', |
||||||
|
request_method='get', |
||||||
|
renderer='ordr:templates/account/forgotten_password_reset.jinja2' |
||||||
|
) |
||||||
|
def completed(context, request): |
||||||
|
''' user is verified, process reset password form ''' |
||||||
|
raise NotImplemented() |
@ -0,0 +1,119 @@ |
|||||||
|
import deform |
||||||
|
import pytest |
||||||
|
|
||||||
|
from pyramid.httpexceptions import HTTPFound |
||||||
|
from pyramid.testing import DummyRequest, DummyResource |
||||||
|
|
||||||
|
from .. import ( # noqa: F401 |
||||||
|
app_config, |
||||||
|
dbsession, |
||||||
|
get_example_user, |
||||||
|
get_post_request |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
def test_forgotten_password_form(): |
||||||
|
''' test the view for the forgotten password form ''' |
||||||
|
from ordr.resources.account import PasswordResetResource |
||||||
|
from ordr.views.forgotten_password import forgotten_password_form |
||||||
|
|
||||||
|
request = DummyRequest() |
||||||
|
parent = DummyResource(request=request) |
||||||
|
context = PasswordResetResource(name=None, parent=parent) |
||||||
|
result = forgotten_password_form(context, None) |
||||||
|
|
||||||
|
assert result == {'formerror': False} |
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize( # noqa: F811 |
||||||
|
'identifier', |
||||||
|
['TerryGilliam', 'gilliam@example.com', 'Gilliam@Example.com'] |
||||||
|
) |
||||||
|
def test_forgotten_password_processing_ok(dbsession, identifier): |
||||||
|
''' test the processing of the forgotten password form ''' |
||||||
|
from ordr.models.account import Role, TokenSubject |
||||||
|
from ordr.resources.account import PasswordResetResource |
||||||
|
from ordr.views.forgotten_password import ( |
||||||
|
forgotten_password_form_processing |
||||||
|
) |
||||||
|
|
||||||
|
user = get_example_user(Role.USER) |
||||||
|
dbsession.add(user) |
||||||
|
dbsession.flush() |
||||||
|
|
||||||
|
post_data = { |
||||||
|
'identifier': identifier, |
||||||
|
'send_mail': 'send_mail', |
||||||
|
} |
||||||
|
request = DummyRequest(dbsession=dbsession, POST=post_data) |
||||||
|
parent = DummyResource(request=request) |
||||||
|
context = PasswordResetResource(name=None, parent=parent) |
||||||
|
result = forgotten_password_form_processing(context, request) |
||||||
|
|
||||||
|
assert isinstance(result, HTTPFound) |
||||||
|
assert result.location == 'http://example.com//verify' |
||||||
|
|
||||||
|
# a token should be created |
||||||
|
token = user.tokens[0] |
||||||
|
assert token.subject == TokenSubject.RESET_PASSWORD |
||||||
|
|
||||||
|
# a verification email should be sent |
||||||
|
# this is tested in the functional test since request.registry.notify |
||||||
|
# doesn't know about event subscribers in the unittest |
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize( # noqa: F811 |
||||||
|
'identifier', |
||||||
|
['', 'GrahamChapman', 'unknown@example.com'] |
||||||
|
) |
||||||
|
def test_forgotten_password_processing_not_ok(dbsession, identifier): |
||||||
|
''' test error processing of the forgotten password form ''' |
||||||
|
from ordr.models.account import Role, Token |
||||||
|
from ordr.resources.account import PasswordResetResource |
||||||
|
from ordr.views.forgotten_password import ( |
||||||
|
forgotten_password_form_processing |
||||||
|
) |
||||||
|
|
||||||
|
user = get_example_user(Role.UNVALIDATED) |
||||||
|
dbsession.add(user) |
||||||
|
dbsession.flush() |
||||||
|
|
||||||
|
post_data = { |
||||||
|
'identifier': identifier, |
||||||
|
'send_mail': 'send_mail', |
||||||
|
} |
||||||
|
request = DummyRequest(dbsession=dbsession, POST=post_data) |
||||||
|
parent = DummyResource(request=request) |
||||||
|
context = PasswordResetResource(name=None, parent=parent) |
||||||
|
result = forgotten_password_form_processing(context, request) |
||||||
|
|
||||||
|
assert result == {'formerror': True} |
||||||
|
assert dbsession.query(Token).count() == 0 |
||||||
|
|
||||||
|
|
||||||
|
def test_forgotten_password_processing_cancel(dbsession): |
||||||
|
''' test the canceling of the forgotten password form ''' |
||||||
|
from ordr.models.account import Token |
||||||
|
from ordr.resources.account import PasswordResetResource |
||||||
|
from ordr.views.forgotten_password import ( |
||||||
|
forgotten_password_form_processing |
||||||
|
) |
||||||
|
|
||||||
|
post_data = { |
||||||
|
'identifier': 'TerryGilliam', |
||||||
|
'cancel': 'cancel', |
||||||
|
} |
||||||
|
request = DummyRequest(dbsession=dbsession, POST=post_data) |
||||||
|
parent = DummyResource(request=request) |
||||||
|
context = PasswordResetResource(name=None, parent=parent) |
||||||
|
result = forgotten_password_form_processing(context, request) |
||||||
|
|
||||||
|
assert isinstance(result, HTTPFound) |
||||||
|
assert result.location == 'http://example.com//' |
||||||
|
assert dbsession.query(Token).count() == 0 |
||||||
|
|
||||||
|
|
||||||
|
def test_verify(): |
||||||
|
from ordr.views.forgotten_password import verify |
||||||
|
result = verify(None, None) |
||||||
|
assert result == {} |
Reference in new issue