Holger Frey
7 years ago
10 changed files with 455 additions and 10 deletions
@ -0,0 +1,44 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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