
18 changed files with 571 additions and 67 deletions
@ -1,3 +1,40 @@
@@ -1,3 +1,40 @@
|
||||
.alert a { |
||||
color:inherit; |
||||
font-weight:bold; |
||||
} |
||||
|
||||
.o3-login-card { |
||||
margin-top:7em; |
||||
} |
||||
|
||||
|
||||
.o3-registration-card { |
||||
margin-top:1em; |
||||
margin-bottom:2em; |
||||
} |
||||
|
||||
|
||||
.o3-registration-card .alert-danger { |
||||
font-size: 80%; |
||||
text-align: center; |
||||
} |
||||
|
||||
.o3-registration-card .help-block { |
||||
color: #6c757d!important; |
||||
font-size: 80%; |
||||
font-weight: 400; |
||||
display: block; |
||||
margin-top: .25rem; |
||||
} |
||||
|
||||
.o3-pwd-dots .bi-eye-slash, .o3-pwd-text .bi-eye { |
||||
display:inline; |
||||
} |
||||
|
||||
.o3-pwd-dots .bi-eye, .o3-pwd-text .bi-eye-slash { |
||||
display:none; |
||||
} |
||||
|
||||
.form-group.deform-form-buttons { |
||||
margin-top:2em; |
||||
} |
||||
|
@ -0,0 +1,51 @@
@@ -0,0 +1,51 @@
|
||||
{% extends "ordr3:templates/layout_small.jinja2" %} |
||||
|
||||
{% block subtitle %} Log In {% endblock subtitle %} |
||||
|
||||
{% block content %} |
||||
|
||||
<div class="container"> |
||||
<div class="row o3-registration-card"> |
||||
<div class="col"></div> |
||||
<div class="col"> |
||||
<div class="card"> |
||||
<div class="card-header bg-dark text-light"> |
||||
<h2 class="card-title text-center pt-1">Ordr</h5> |
||||
</div> |
||||
<div class="card-body"> |
||||
<h6>Why is Ordr telling me my password is compromised?</h6> |
||||
<p> |
||||
Ordr itself has not suffered a breach. This is a protective measure to |
||||
reduce the risk of <a href="https://www.owasp.org/index.php/Credential_stuffing" target="_blank" rel="noopener">credential stuffing</a> |
||||
attacks against Ordr and its users. |
||||
</p> |
||||
<p> |
||||
Each time a user supplies a password while registering or updating their |
||||
password — Ordr securely checks whether that password has appeared in |
||||
public data breaches. |
||||
</p> |
||||
<p> |
||||
During each of these processes, Ordr generates a SHA-1 hash of the |
||||
supplied password and uses the first five (5) characters of the hash to |
||||
check the <a href="https://haveibeenpwned.com/API/v2#SearchingPwnedPasswordsByRange" target="_blank" rel="noopener">Have I Been Pwned API</a> |
||||
and determine if the password has been previously compromised. |
||||
The plaintext password is never stored by Ordr or submitted to the |
||||
Have I Been Pwned API. |
||||
</p> |
||||
<p> |
||||
If you receive an error message saying that |
||||
"This password appears in a breach or has been compromised.", |
||||
you should change it all other places that you use it as soon as possible. |
||||
</p> |
||||
<p class="small text-secondary"> |
||||
This text originally appeared on the <a href="https://pypi.org/help/#compromised-password" target="_blank" rel="noopener">PyPI FAQ</a>. |
||||
<p> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div class="col"></div> |
||||
</div> |
||||
</div> |
||||
|
||||
|
||||
{% endblock content %} |
@ -0,0 +1,25 @@
@@ -0,0 +1,25 @@
|
||||
{% extends "ordr3:templates/layout_small.jinja2" %} |
||||
|
||||
{% block subtitle %} Registration {% endblock subtitle %} |
||||
|
||||
{% block content %} |
||||
|
||||
<div class="container"> |
||||
<div class="row o3-registration-card"> |
||||
<div class="col"></div> |
||||
<div class="col"> |
||||
<div class="card"> |
||||
<div class="card-header bg-dark text-light"> |
||||
<h2 class="card-title text-center pt-1">Ordr</h5> |
||||
</div> |
||||
<div class="card-body"> |
||||
<h6 class="card-subtitle mb-2 text-muted text-center mt-1 mb-4">Register a new account</h6> |
||||
{{form.render()|safe}} |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div class="col"></div> |
||||
</div> |
||||
</div> |
||||
|
||||
{% endblock content %} |
@ -0,0 +1,34 @@
@@ -0,0 +1,34 @@
|
||||
{% extends "ordr3:templates/layout_small.jinja2" %} |
||||
|
||||
{% block subtitle %} Registration completed {% endblock subtitle %} |
||||
|
||||
{% block content %} |
||||
|
||||
<div class="container"> |
||||
<div class="row o3-registration-card"> |
||||
<div class="col"></div> |
||||
<div class="col"> |
||||
<div class="card"> |
||||
<div class="card-header bg-dark text-light"> |
||||
<h2 class="card-title text-center pt-1">Ordr</h5> |
||||
</div> |
||||
<div class="card-body"> |
||||
<h6 class="card-subtitle mb-2 text-muted text-center mt-1 mb-4">Registration completed</h6> |
||||
{% for message in request.session.pop_flash("warning") %} |
||||
<div class="alert alert-warning" role="alert"> |
||||
<p class="h6">{{message[0]}}</p> |
||||
<p class="small">{{message[1]|safe}}</p> |
||||
</div> |
||||
{% endfor %} |
||||
|
||||
<p>The registration is completed.<p> |
||||
<p>The account needs to be activated by an administrator.</p> |
||||
<p>You should receive an email as soon as the account is activated.</p> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div class="col"></div> |
||||
</div> |
||||
</div> |
||||
|
||||
{% endblock content %} |
@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
|
||||
<span tal:define="name name|field.name; |
||||
css_class css_class|field.widget.css_class; |
||||
oid oid|field.oid; |
||||
mask mask|field.widget.mask; |
||||
mask_placeholder mask_placeholder|field.widget.mask_placeholder; |
||||
style style|field.widget.style; |
||||
" |
||||
tal:omit-tag=""> |
||||
<input type="email" name="${name}" value="${cstruct}" |
||||
tal:attributes="required string: ${required|'required' or ''}; |
||||
class string: form-control ${css_class or ''} ${'is-invalid' if field.error else ''}; |
||||
style style; |
||||
attributes|field.widget.attributes|{};" |
||||
id="${oid}"/> |
||||
<script tal:condition="mask" type="text/javascript"> |
||||
deform.addCallback( |
||||
'${oid}', |
||||
function (oid) { |
||||
$("#" + oid).mask("${mask}", |
||||
{placeholder:"${mask_placeholder}"}); |
||||
}); |
||||
</script> |
||||
</span> |
@ -0,0 +1,109 @@
@@ -0,0 +1,109 @@
|
||||
<form |
||||
tal:define="style style|field.widget.style; |
||||
css_class css_class|string:${field.widget.css_class or field.css_class or ''}; |
||||
item_template item_template|field.widget.item_template; |
||||
autocomplete autocomplete|field.autocomplete; |
||||
title title|field.title; |
||||
errormsg errormsg|field.errormsg; |
||||
description description|field.description; |
||||
buttons buttons|field.buttons; |
||||
use_ajax use_ajax|field.use_ajax; |
||||
ajax_options ajax_options|field.ajax_options; |
||||
formid formid|field.formid; |
||||
action action|field.action or None; |
||||
method method|field.method;" |
||||
tal:attributes="autocomplete autocomplete; |
||||
style style; |
||||
class css_class; |
||||
action action; |
||||
attributes|field.widget.attributes|{};" |
||||
id="${formid}" |
||||
method="${method}" |
||||
enctype="multipart/form-data" |
||||
accept-charset="utf-8" |
||||
i18n:domain="deform" |
||||
> |
||||
|
||||
<fieldset class="deform-form-fieldset"> |
||||
|
||||
<legend tal:condition="title">${title}</legend> |
||||
|
||||
<input type="hidden" name="_charset_" /> |
||||
<input type="hidden" name="__formid__" value="${formid}"/> |
||||
|
||||
<div class="alert alert-danger" tal:condition="field.error"> |
||||
<div class="error-msg-lbl" i18n:translate="" |
||||
>There was a problem with your submission</div> |
||||
<div class="error-msg-detail" i18n:translate="" |
||||
>Errors have been highlighted below</div> |
||||
<p class="error-msg" tal:condition="field.errormsg">${field.errormsg}</p> |
||||
</div> |
||||
|
||||
<p class="section first" tal:condition="description"> |
||||
${description} |
||||
</p> |
||||
|
||||
<div tal:repeat="child field" |
||||
tal:replace="structure child.render_template(item_template)"/> |
||||
|
||||
<div class="form-group deform-form-buttons"> |
||||
<tal:loop tal:repeat="button buttons"> |
||||
<button |
||||
tal:define="btn_disposition repeat.button.start and 'btn-primary' or 'btn-outline-secondary';" |
||||
tal:attributes="disabled button.disabled if button.disabled else None; |
||||
attributes|button.attributes|{};" |
||||
id="${formid+button.name}" |
||||
name="${button.name}" |
||||
type="${button.type}" |
||||
class="btn ${button.css_class or btn_disposition}" |
||||
value="${button.value}" |
||||
tal:condition="button.type != 'link'"> |
||||
<span tal:condition="button.icon" class="glyphicon glyphicon-${button.icon}"></span> |
||||
${button.title} |
||||
</button> |
||||
<a |
||||
tal:define="btn_disposition repeat.button.start and 'btn-primary' or 'btn-outline-secondary'; |
||||
btn_href button.value|''" |
||||
class="btn ${button.css_class or btn_disposition}" |
||||
id="${field.formid + button.name}" |
||||
href="${btn_href}" |
||||
tal:condition="button.type == 'link'"> |
||||
<span tal:condition="button.icon" class="glyphicon glyphicon-${button.icon}"></span> |
||||
${button.title} |
||||
</a> |
||||
</tal:loop> |
||||
</div> |
||||
|
||||
</fieldset> |
||||
|
||||
<script type="text/javascript" tal:condition="use_ajax"> |
||||
deform.addCallback( |
||||
'${formid}', |
||||
function(oid) { |
||||
var target = '#' + oid; |
||||
var options = { |
||||
target: target, |
||||
replaceTarget: true, |
||||
success: function() { |
||||
deform.processCallbacks(); |
||||
deform.focusFirstInput(target); |
||||
}, |
||||
beforeSerialize: function() { |
||||
// See http://bit.ly/1agBs9Z (hack to fix tinymce-related ajax bug) |
||||
if ('tinymce' in window) { |
||||
$(tinymce.get()).each( |
||||
function(i, el) { |
||||
var content = el.getContent(); |
||||
var editor_input = document.getElementById(el.id); |
||||
editor_input.value = content; |
||||
}); |
||||
} |
||||
} |
||||
}; |
||||
var extra_options = ${ajax_options} || {}; |
||||
$('#' + oid).ajaxForm($.extend(options, extra_options)); |
||||
} |
||||
); |
||||
</script> |
||||
|
||||
</form> |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
<input type="hidden" name="${name|field.name}" value="${cstruct}" |
||||
id="${oid|field.oid}"/> |
||||
|
@ -0,0 +1,48 @@
@@ -0,0 +1,48 @@
|
||||
<div tal:define="error_class error_class|field.widget.error_class; |
||||
description description|field.description; |
||||
title title|field.title; |
||||
oid oid|field.oid; |
||||
hidden hidden|field.widget.hidden; |
||||
category category|field.widget.category; |
||||
structural hidden or category == 'structural'; |
||||
required required|field.required;" |
||||
class="form-group ${field.error and 'has-error' or ''} ${field.widget.item_css_class or ''} ${field.default_item_css_class()}" |
||||
title="${description}" |
||||
id="item-${oid}" |
||||
tal:omit-tag="structural" |
||||
i18n:domain="deform"> |
||||
|
||||
<label for="${oid}" |
||||
class="control-label ${required and 'required' or ''}" |
||||
tal:condition="not structural" |
||||
id="req-${oid}" |
||||
> |
||||
${title} |
||||
</label> |
||||
|
||||
<div tal:define="input_prepend field.widget.input_prepend | None; |
||||
input_append field.widget.input_append | None" |
||||
tal:omit-tag="not (input_prepend or input_append)" |
||||
class="input-group"> |
||||
<span class="input-group-addon" |
||||
tal:condition="input_prepend">${input_prepend}</span |
||||
><span tal:replace="structure field.serialize(cstruct).strip()" |
||||
/><span class="input-group-addon" |
||||
tal:condition="input_append">${input_append}</span> |
||||
</div> |
||||
|
||||
<div class="invalid-feedback" |
||||
tal:define="errstr 'error-%s' % field.oid" |
||||
tal:repeat="msg field.error.messages()" |
||||
i18n:translate="" |
||||
tal:attributes="id repeat.msg.index==0 and errstr or |
||||
('%s-%s' % (errstr, repeat.msg.index))" |
||||
tal:condition="field.error and not field.widget.hidden and not field.typ.__class__.__name__=='Mapping'"> |
||||
${msg} |
||||
</div> |
||||
|
||||
<p tal:condition="field.description and not field.widget.hidden" |
||||
class="help-block" > |
||||
${field.description} |
||||
</p> |
||||
</div> |
@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
|
||||
<span tal:define="name name|field.name; |
||||
css_class css_class|field.widget.css_class; |
||||
oid oid|field.oid; |
||||
mask mask|field.widget.mask; |
||||
mask_placeholder mask_placeholder|field.widget.mask_placeholder; |
||||
style style|field.widget.style; |
||||
" |
||||
tal:omit-tag=""> |
||||
<input type="text" name="${name}" value="${cstruct}" |
||||
tal:attributes="required string: ${required|'required' or ''}; |
||||
class string: form-control ${css_class or ''} ${'is-invalid' if field.error else ''} |
||||
style style; |
||||
attributes|field.widget.attributes|{};" |
||||
id="${oid}"/> |
||||
<script tal:condition="mask" type="text/javascript"> |
||||
deform.addCallback( |
||||
'${oid}', |
||||
function (oid) { |
||||
$("#" + oid).mask("${mask}", |
||||
{placeholder:"${mask_placeholder}"}); |
||||
}); |
||||
</script> |
||||
</span> |
@ -0,0 +1,22 @@
@@ -0,0 +1,22 @@
|
||||
<span tal:define="name name|field.name; |
||||
css_class css_class|field.widget.css_class; |
||||
oid oid|field.oid; |
||||
mask mask|field.widget.mask|None; |
||||
mask_placeholder mask_placeholder|field.widget.mask_placeholder|'_'; |
||||
style style|field.widget.style; |
||||
" |
||||
tal:omit-tag=""> |
||||
<input type="text" name="${name}" value="${cstruct}" |
||||
tal:attributes="class string: form-control ${css_class or ''} ${'is-invalid' if field.error else ''} |
||||
style style" |
||||
id="${oid}" |
||||
readonly="readonly"/> |
||||
<script tal:condition="mask" type="text/javascript"> |
||||
deform.addCallback( |
||||
'${oid}', |
||||
function (oid) { |
||||
$("#" + oid).mask("${mask}", |
||||
{placeholder:"${mask_placeholder}"}); |
||||
}); |
||||
</script> |
||||
</span> |
@ -0,0 +1,29 @@
@@ -0,0 +1,29 @@
|
||||
<div class="input-group"> |
||||
<input |
||||
type="password" |
||||
name="${name|field.name}" |
||||
value="${field.widget.redisplay and cstruct or ''}" |
||||
tal:attributes="required string: ${required|'required' or ''}; |
||||
style style|field.widget.style; |
||||
class string: form-control o3-pwd-field ${css_class|field.widget.css_class or ''} ${'is-invalid' if field.error else ''}; |
||||
attributes|field.widget.attributes|{}; |
||||
" |
||||
id="${oid|field.oid}" |
||||
|
||||
/> |
||||
<div class="input-group-append o3-pwd-visibility o3-pwd-dots"> |
||||
<div class="input-group-text"> |
||||
<svg class="bi bi-eye" width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> |
||||
<path fill-rule="evenodd" d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.134 13.134 0 001.66 2.043C4.12 11.332 5.88 12.5 8 12.5c2.12 0 3.879-1.168 5.168-2.457A13.134 13.134 0 0014.828 8a13.133 13.133 0 00-1.66-2.043C11.879 4.668 10.119 3.5 8 3.5c-2.12 0-3.879 1.168-5.168 2.457A13.133 13.133 0 001.172 8z" clip-rule="evenodd"/> |
||||
<path fill-rule="evenodd" d="M8 5.5a2.5 2.5 0 100 5 2.5 2.5 0 000-5zM4.5 8a3.5 3.5 0 117 0 3.5 3.5 0 01-7 0z" clip-rule="evenodd"/> |
||||
</svg> |
||||
<svg class="bi bi-eye-slash" width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> |
||||
<path d="M13.359 11.238C15.06 9.72 16 8 16 8s-3-5.5-8-5.5a7.028 7.028 0 00-2.79.588l.77.771A5.944 5.944 0 018 3.5c2.12 0 3.879 1.168 5.168 2.457A13.134 13.134 0 0114.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755-.165.165-.337.328-.517.486l.708.709z"/> |
||||
<path d="M11.297 9.176a3.5 3.5 0 00-4.474-4.474l.823.823a2.5 2.5 0 012.829 2.829l.822.822zm-2.943 1.299l.822.822a3.5 3.5 0 01-4.474-4.474l.823.823a2.5 2.5 0 002.829 2.829z"/> |
||||
<path d="M3.35 5.47c-.18.16-.353.322-.518.487A13.134 13.134 0 001.172 8l.195.288c.335.48.83 1.12 1.465 1.755C4.121 11.332 5.881 12.5 8 12.5c.716 0 1.39-.133 2.02-.36l.77.772A7.029 7.029 0 018 13.5C3 13.5 0 8 0 8s.939-1.721 2.641-3.238l.708.709z"/> |
||||
<path fill-rule="evenodd" d="M13.646 14.354l-12-12 .708-.708 12 12-.708.708z" clip-rule="evenodd"/> |
||||
</svg> |
||||
</div> |
||||
</div> |
||||
|
||||
</div> |
@ -0,0 +1,25 @@
@@ -0,0 +1,25 @@
|
||||
<!DOCTYPE html> |
||||
<html lang="en"> |
||||
<head> |
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> |
||||
|
||||
<title>Ordr | {% block subtitle %} Subtitle {% endblock subtitle %}</title> |
||||
|
||||
<link href="{{request.static_url('ordr3:static/favicon.ico')}}" type="image/x-icon" rel="shortcut icon"> |
||||
|
||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous"> |
||||
<link rel="stylesheet" href="{{request.static_url('ordr3:static/style.css')}}" type="text/css" media="screen" /> |
||||
|
||||
<script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script> |
||||
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script> |
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script> |
||||
<script src="{{request.static_url('ordr3:static/script.js')}}"></script> |
||||
</head> |
||||
<body> |
||||
|
||||
{% block content %} |
||||
<p>No content</p> |
||||
{% endblock content %} |
||||
|
||||
</body> |
||||
</html> |
@ -0,0 +1,123 @@
@@ -0,0 +1,123 @@
|
||||
""" static and login pages """ |
||||
|
||||
|
||||
import deform |
||||
from pyramid.view import view_config |
||||
from pyramid.security import forget, remember |
||||
from pyramid.httpexceptions import HTTPFound |
||||
|
||||
from .. import models, security, services |
||||
from ..schemas.account import RegistrationSchema |
||||
|
||||
|
||||
@view_config( |
||||
context="ordr3:resources.Root", |
||||
name="login", |
||||
permission="login", |
||||
request_method="GET", |
||||
renderer="ordr3:templates/account/login.jinja2", |
||||
) |
||||
def login(context, request): |
||||
return {"error": False} |
||||
|
||||
|
||||
@view_config( |
||||
context="ordr3:resources.Root", |
||||
name="login", |
||||
permission="login", |
||||
request_method="POST", |
||||
require_csrf=False, |
||||
renderer="ordr3:templates/account/login.jinja2", |
||||
) |
||||
def check_credentials(context, request): |
||||
username = request.POST.get("username", "") |
||||
password = request.POST.get("password", "") |
||||
|
||||
crypt_context = security.get_passlib_context() |
||||
user = services.verify_credentials( |
||||
request.repo, crypt_context, username, password |
||||
) |
||||
if user is not None and user.is_active: |
||||
headers = remember(request, user.id) |
||||
return HTTPFound( |
||||
request.resource_path(request.root, "orders"), headers=headers |
||||
) |
||||
return {"error": True} |
||||
|
||||
|
||||
@view_config( |
||||
context="ordr3:resources.Root", name="logout", permission="logout" |
||||
) |
||||
def logout(context, request): |
||||
""" logout of a user """ |
||||
return HTTPFound( |
||||
request.resource_path(request.root, "login"), headers=forget(request) |
||||
) |
||||
|
||||
|
||||
@view_config( |
||||
context="ordr3:resources.Root", |
||||
name="registration", |
||||
permission="registration", |
||||
request_method="GET", |
||||
renderer="ordr3:templates/account/registration.jinja2", |
||||
) |
||||
def registration(context, request): |
||||
form = RegistrationSchema.as_form(request) |
||||
return {"form": form} |
||||
|
||||
|
||||
@view_config( |
||||
context="ordr3:resources.Root", |
||||
name="registration", |
||||
permission="registration", |
||||
request_method="POST", |
||||
renderer="ordr3:templates/account/registration.jinja2", |
||||
) |
||||
def register_new_user(context, request): |
||||
if "Cancel" in request.POST: |
||||
return HTTPFound(request.resource_path(request.root)) |
||||
|
||||
form = RegistrationSchema.as_form(request) |
||||
data = request.POST.items() |
||||
try: |
||||
appstruct = form.validate(data) |
||||
except deform.ValidationFailure: |
||||
return {"form": form} |
||||
|
||||
account = models.User( |
||||
id=None, |
||||
password=None, |
||||
username=appstruct["user_name"], |
||||
first_name=appstruct["first_name"], |
||||
last_name=appstruct["last_name"], |
||||
email=appstruct["email"], |
||||
role=models.UserRole.NEW, |
||||
) |
||||
warnings = services.set_new_password(account, appstruct["password"]) |
||||
request.repo.add_user(account) |
||||
|
||||
for message in warnings: |
||||
request.flash("warning", message.message, message.description) |
||||
|
||||
return HTTPFound(request.resource_path(request.root, "registered")) |
||||
|
||||
|
||||
@view_config( |
||||
context="ordr3:resources.Root", |
||||
name="registered", |
||||
permission="view", |
||||
renderer="ordr3:templates/account/registration_complete.jinja2", |
||||
) |
||||
def registration_complete(context, request): |
||||
return {} |
||||
|
||||
|
||||
@view_config( |
||||
context="ordr3:resources.Root", |
||||
name="breached", |
||||
permission="view", |
||||
renderer="ordr3:templates/account/breached_password.jinja2", |
||||
) |
||||
def breached_password(context, request): |
||||
return {} |
Loading…
Reference in new issue