diff --git a/ordr3/resources.py b/ordr3/resources.py index ac77bb9..7ea69b5 100644 --- a/ordr3/resources.py +++ b/ordr3/resources.py @@ -56,7 +56,12 @@ class Root(BaseResource): def __acl__(self): """ access controll list """ - return [(Allow, Everyone, "view")] + return [ + (Allow, Everyone, "login"), + (Allow, Everyone, "logout"), + (Allow, Everyone, "registration"), + (Allow, Everyone, "view"), + ] def includeme(config): diff --git a/ordr3/static/style.css b/ordr3/static/style.css index f459932..46a2ef6 100644 --- a/ordr3/static/style.css +++ b/ordr3/static/style.css @@ -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; + } diff --git a/ordr3/templates/account/breached_password.jinja2 b/ordr3/templates/account/breached_password.jinja2 new file mode 100644 index 0000000..04cae9b --- /dev/null +++ b/ordr3/templates/account/breached_password.jinja2 @@ -0,0 +1,51 @@ +{% extends "ordr3:templates/layout_small.jinja2" %} + +{% block subtitle %} Log In {% endblock subtitle %} + +{% block content %} + +
+
+
+
+
+
+

Ordr

+
+
+
Why is Ordr telling me my password is compromised?
+

+ Ordr itself has not suffered a breach. This is a protective measure to + reduce the risk of credential stuffing + attacks against Ordr and its users. +

+

+ 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. +

+

+ 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 Have I Been Pwned API + 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. +

+

+ 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. +

+

+ This text originally appeared on the PyPI FAQ. +

+

+
+
+
+
+
+ + +{% endblock content %} diff --git a/ordr3/templates/root/login.jinja2 b/ordr3/templates/account/login.jinja2 similarity index 72% rename from ordr3/templates/root/login.jinja2 rename to ordr3/templates/account/login.jinja2 index 913344f..14a7717 100644 --- a/ordr3/templates/root/login.jinja2 +++ b/ordr3/templates/account/login.jinja2 @@ -1,21 +1,13 @@ - - - - +{% extends "ordr3:templates/layout_small.jinja2" %} - Ordr - Log In +{% block subtitle %} Log In {% endblock subtitle %} - - - - - - +{% block content %}
- @@ -45,5 +37,5 @@
- - + +{% endblock content %} diff --git a/ordr3/templates/account/registration.jinja2 b/ordr3/templates/account/registration.jinja2 new file mode 100644 index 0000000..c488484 --- /dev/null +++ b/ordr3/templates/account/registration.jinja2 @@ -0,0 +1,25 @@ +{% extends "ordr3:templates/layout_small.jinja2" %} + +{% block subtitle %} Registration {% endblock subtitle %} + +{% block content %} + +
+
+
+
+
+
+

Ordr

+
+
+
Register a new account
+ {{form.render()|safe}} +
+
+
+
+
+
+ +{% endblock content %} diff --git a/ordr3/templates/account/registration_complete.jinja2 b/ordr3/templates/account/registration_complete.jinja2 new file mode 100644 index 0000000..4b64e1b --- /dev/null +++ b/ordr3/templates/account/registration_complete.jinja2 @@ -0,0 +1,34 @@ +{% extends "ordr3:templates/layout_small.jinja2" %} + +{% block subtitle %} Registration completed {% endblock subtitle %} + +{% block content %} + +
+
+
+
+
+
+

Ordr

+
+
+
Registration completed
+ {% for message in request.session.pop_flash("warning") %} + + {% endfor %} + +

The registration is completed.

+

The account needs to be activated by an administrator.

+

You should receive an email as soon as the account is activated.

+
+
+
+
+
+
+ +{% endblock content %} diff --git a/ordr3/templates/deform/email.pt b/ordr3/templates/deform/email.pt new file mode 100644 index 0000000..5c3103a --- /dev/null +++ b/ordr3/templates/deform/email.pt @@ -0,0 +1,23 @@ + + + + diff --git a/ordr3/templates/deform/form.pt b/ordr3/templates/deform/form.pt new file mode 100644 index 0000000..156e0a4 --- /dev/null +++ b/ordr3/templates/deform/form.pt @@ -0,0 +1,109 @@ +
+ +
+ + ${title} + + + + +
+
There was a problem with your submission
+
Errors have been highlighted below
+

${field.errormsg}

+
+ +

+ ${description} +

+ +
+ +
+ + + + + ${button.title} + + +
+ +
+ + + +
diff --git a/ordr3/templates/deform/hidden.pt b/ordr3/templates/deform/hidden.pt new file mode 100644 index 0000000..9c73066 --- /dev/null +++ b/ordr3/templates/deform/hidden.pt @@ -0,0 +1,3 @@ + + diff --git a/ordr3/templates/deform/mapping_item.pt b/ordr3/templates/deform/mapping_item.pt new file mode 100644 index 0000000..242065b --- /dev/null +++ b/ordr3/templates/deform/mapping_item.pt @@ -0,0 +1,48 @@ +
+ + + +
+ ${input_prepend}${input_append} +
+ +
+ ${msg} +
+ +

+ ${field.description} +

+
diff --git a/ordr3/templates/deform/textinput.pt b/ordr3/templates/deform/textinput.pt new file mode 100644 index 0000000..664fd40 --- /dev/null +++ b/ordr3/templates/deform/textinput.pt @@ -0,0 +1,23 @@ + + + + diff --git a/ordr3/templates/deform/textinput_disabled.pt b/ordr3/templates/deform/textinput_disabled.pt new file mode 100644 index 0000000..5166524 --- /dev/null +++ b/ordr3/templates/deform/textinput_disabled.pt @@ -0,0 +1,22 @@ + + + + diff --git a/ordr3/templates/deform/viewable_password.pt b/ordr3/templates/deform/viewable_password.pt new file mode 100644 index 0000000..bf65ebb --- /dev/null +++ b/ordr3/templates/deform/viewable_password.pt @@ -0,0 +1,29 @@ +
+ +
+
+ + + + + + + + + + +
+
+ +
diff --git a/ordr3/templates/layout.jinja2 b/ordr3/templates/layout_full.jinja2 similarity index 50% rename from ordr3/templates/layout.jinja2 rename to ordr3/templates/layout_full.jinja2 index 5a4d309..ad8e4e3 100644 --- a/ordr3/templates/layout.jinja2 +++ b/ordr3/templates/layout_full.jinja2 @@ -9,6 +9,10 @@ + + + + diff --git a/ordr3/templates/layout_small.jinja2 b/ordr3/templates/layout_small.jinja2 new file mode 100644 index 0000000..c4884d7 --- /dev/null +++ b/ordr3/templates/layout_small.jinja2 @@ -0,0 +1,25 @@ + + + + + + Ordr | {% block subtitle %} Subtitle {% endblock subtitle %} + + + + + + + + + + + + + +{% block content %} +

No content

+{% endblock content %} + + + diff --git a/ordr3/views/__init__.py b/ordr3/views/__init__.py index 37fe4e6..b987ab8 100644 --- a/ordr3/views/__init__.py +++ b/ordr3/views/__init__.py @@ -3,15 +3,13 @@ some view helpers are defined here """ -from collections import namedtuple -# a message for session.flash() -FlashMessage = namedtuple("FlashMessage", ["message", "description"]) +from .. import models def flash(request, channel, message, description=""): """ small wrapper around request.session.flash """ - msg = FlashMessage(message, description) + msg = models.FlashMessage(message, description) request.session.flash(msg, channel, allow_duplicate=False) diff --git a/ordr3/views/account.py b/ordr3/views/account.py new file mode 100644 index 0000000..cfdb2fd --- /dev/null +++ b/ordr3/views/account.py @@ -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 {} diff --git a/ordr3/views/root.py b/ordr3/views/root.py index e41f46c..e51e0da 100644 --- a/ordr3/views/root.py +++ b/ordr3/views/root.py @@ -2,11 +2,8 @@ from pyramid.view import view_config -from pyramid.security import forget, remember from pyramid.httpexceptions import HTTPFound -from .. import security, services - @view_config( context="ordr3:resources.Root", permission="view", @@ -16,47 +13,3 @@ def root(context, request): return HTTPFound(request.resource_path(request.root, "orders")) else: return HTTPFound(request.resource_path(request.root, "login")) - - -@view_config( - context="ordr3:resources.Root", - name="login", - permission="view", - request_method="GET", - renderer="ordr3:templates/root/login.jinja2", -) -def login(context, request): - return {"error": False} - - -@view_config( - context="ordr3:resources.Root", - name="login", - permission="view", - request_method="POST", - require_csrf=False, - renderer="ordr3:templates/root/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="view") -def logout(context, request): - """ logout of a user """ - headers = forget(request) - return HTTPFound( - request.resource_path(request.root, "login"), headers=headers - )