diff options
-rw-r--r-- | development.ini | 3 | ||||
-rw-r--r-- | fietsboek/static/fietsboek.js | 35 | ||||
-rw-r--r-- | fietsboek/templates/password_reset.jinja2 | 12 | ||||
-rw-r--r-- | fietsboek/templates/profile.jinja2 | 12 | ||||
-rw-r--r-- | fietsboek/util.py | 25 | ||||
-rw-r--r-- | fietsboek/views/default.py | 9 | ||||
-rw-r--r-- | fietsboek/views/profile.py | 11 |
7 files changed, 94 insertions, 13 deletions
diff --git a/development.ini b/development.ini index 3820779..fb46605 100644 --- a/development.ini +++ b/development.ini @@ -7,6 +7,9 @@ use = egg:fietsboek pyramid.reload_templates = true +pyramid.reload_assets = true +pyramid.reload_resources = true +pyramid.reload_all = true pyramid.debug_authorization = false pyramid.debug_notfound = false pyramid.debug_routematch = false diff --git a/fietsboek/static/fietsboek.js b/fietsboek/static/fietsboek.js index 6fc5209..26a0902 100644 --- a/fietsboek/static/fietsboek.js +++ b/fietsboek/static/fietsboek.js @@ -19,6 +19,27 @@ function updateTagList() { } } +function checkPasswordValidity(main, repeat) { + let main_pw = document.querySelector(main); + let repeat_pw = document.querySelector(repeat); + + let form = main_pw.closest('form'); + form.classList.remove('was-validated'); + + /* Check password requirements */ + if (main_pw.value.length != 0 && main_pw.value.length < 8) { + main_pw.setCustomValidity('Too short'); + } else { + main_pw.setCustomValidity(''); + } + + if (main_pw.value != repeat_pw.value) { + repeat_pw.setCustomValidity('Needs to match'); + } else { + repeat_pw.setCustomValidity(''); + } +} + document.addEventListener('DOMContentLoaded', function(event) { /* Enable the "Add tag" button */ let $ = (selector) => document.querySelector(selector); @@ -57,4 +78,18 @@ document.addEventListener('DOMContentLoaded', function(event) { var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) { return new bootstrap.Tooltip(tooltipTriggerEl, {sanitize: false}) }); + + /* Enable Bootstrap form validation */ + const forms = document.querySelectorAll('.needs-validation') + Array.from(forms).forEach(form => { + form.addEventListener('submit', event => { + if (!form.checkValidity()) { + event.preventDefault() + event.stopPropagation() + } + + form.classList.add('was-validated') + }, false) + }) + }); diff --git a/fietsboek/templates/password_reset.jinja2 b/fietsboek/templates/password_reset.jinja2 index 892837f..8ecec84 100644 --- a/fietsboek/templates/password_reset.jinja2 +++ b/fietsboek/templates/password_reset.jinja2 @@ -2,17 +2,23 @@ {% block content %} <div class="container"> <h1>{{ _("page.password_reset.title") }}</h1> - <form method="POST"> + <form method="POST" class="needs-validation" novalidate> <div class="row mb-3"> <label for="inputPassword" class="col-sm-3 col-form-label">{{ _("page.password_reset.password") }}</label> <div class="col-sm-9"> - <input type="password" class="form-control" id="inputPassword" name="password"> + <input type="password" class="form-control" id="inputPassword" name="password" onchange="checkPasswordValidity('#inputPassword', '#repeatPassword')"> + <div class="invalid-feedback"> + {{ _("page.password_reset.password_invalid") }} + </div> </div> </div> <div class="row mb-3"> <label for="repeatPassword" class="col-sm-3 col-form-label">{{ _("page.password_reset.repeat_password") }}</label> <div class="col-sm-9"> - <input type="password" class="form-control" id="repeatPassword" name="repeat-password"> + <input type="password" class="form-control" id="repeatPassword" name="repeat-password" onchange="checkPasswordValidity('#inputPassword', '#repeatPassword')"> + <div class="invalid-feedback"> + {{ _("page.password_reset.password_mismatch") }} + </div> </div> </div> <button type="submit" class="btn btn-primary">{{ _("page.password_reset.reset") }}</button> diff --git a/fietsboek/templates/profile.jinja2 b/fietsboek/templates/profile.jinja2 index 5487b92..65978ac 100644 --- a/fietsboek/templates/profile.jinja2 +++ b/fietsboek/templates/profile.jinja2 @@ -8,7 +8,7 @@ <h2>{{ _("page.my_profile.personal_data") }}</h2> - <form method="POST" action="{{ request.route_path('change-profile') }}"> + <form method="POST" action="{{ request.route_path('change-profile') }}" class="needs-validation" novalidate> <div class="row mb-3"> <label for="inputName" class="col-sm-3 col-form-label">{{ _("page.my_profile.personal_data.name") }}</label> <div class="col-sm-9"> @@ -18,13 +18,19 @@ <div class="row mb-3"> <label for="inputPassword" class="col-sm-3 col-form-label">{{ _("page.my_profile.personal_data.password") }}</label> <div class="col-sm-9"> - <input type="password" class="form-control" id="inputPassword" name="password"> + <input type="password" class="form-control" id="inputPassword" name="password" onchange="checkPasswordValidity('#inputPassword', '#repeatPassword')"> + <div class="invalid-feedback"> + {{ _("page.my_profile.personal_data.password_invalid") }} + </div> </div> </div> <div class="row mb-3"> <label for="repeatPassword" class="col-sm-3 col-form-label">{{ _("page.my_profile.personal_data.repeat_password") }}</label> <div class="col-sm-9"> - <input type="password" class="form-control" id="repeatPassword" name="repeat-password"> + <input type="password" class="form-control" id="repeatPassword" name="repeat-password" onchange="checkPasswordValidity('#inputPassword', '#repeatPassword')"> + <div class="invalid-feedback"> + {{ _("page.my_profile.personal_data.password_must_match") }} + </div> </div> </div> <button type="submit" class="btn btn-primary"><i class="bi bi-save"></i> {{ _("page.my_profile.personal_data.save") }}</button> diff --git a/fietsboek/util.py b/fietsboek/util.py index bdc128d..b56584f 100644 --- a/fietsboek/util.py +++ b/fietsboek/util.py @@ -130,3 +130,28 @@ def parse_badges(badges, params, prefix='badge-'): badge for badge in badges if params.get(f'{prefix}{badge.id}') ] + + +def check_password_constraints(password, repeat_password=None): + """Verifies that the password constraints match for the given password. + + This is usually also verified client-side, but for people that bypass the + client side verification and the API, this is re-implemented here. + + If ``repeat_password`` is given, this also verifies that the two passwords + match. + + :raises ValueError: If the verification of the constraints failed. The + first arg of the error will be a + :class:`~pyramid.i18n.TranslationString` with the message of why the + verification failed. + :param password: The password which to verify. + :type password: str + :param repeat_password: The password repeat. + :type repeat_password: str + """ + if repeat_password is not None: + if repeat_password != password: + raise ValueError(_("password_constraint.mismatch")) + if len(password) < 8: + raise ValueError(_("password_constraint.length")) diff --git a/fietsboek/views/default.py b/fietsboek/views/default.py index 9c417d2..8424b97 100644 --- a/fietsboek/views/default.py +++ b/fietsboek/views/default.py @@ -159,11 +159,14 @@ def use_token(request): if request.method == 'GET' and token.token_type == TokenType.RESET_PASSWORD: return render_to_response('fietsboek:templates/password_reset.jinja2', {}, request) if request.method == 'POST' and token.token_type == TokenType.RESET_PASSWORD: - if request.params["password"] != request.params["repeat-password"]: - request.session.flash(request.localizer.translate(_("flash.password_mismatch"))) + password = request.params["password"] + try: + util.check_password_constraints(password, request.params["repeat-password"]) + except ValueError as exc: + request.session.flash(request.localizer.translate(exc.args[0])) return HTTPFound(request.route_url('use-token', uuid=token.uuid)) - token.user.set_password(request.params["password"]) + token.user.set_password(password) request.dbsession.delete(token) request.session.flash(request.localizer.translate(_("flash.password_updated"))) return HTTPFound(request.route_url("login")) diff --git a/fietsboek/views/profile.py b/fietsboek/views/profile.py index b138bd0..c14b138 100644 --- a/fietsboek/views/profile.py +++ b/fietsboek/views/profile.py @@ -7,7 +7,7 @@ from pyramid.httpexceptions import HTTPFound, HTTPNotFound, HTTPForbidden from sqlalchemy import select -from .. import models +from .. import models, util @view_config(route_name='profile', renderer='fietsboek:templates/profile.jinja2', @@ -43,9 +43,12 @@ def do_change_profile(request): :return: The HTTP response. :rtype: pyramid.response.Response """ - if request.params["password"]: - if request.params["password"] != request.params["repeat-password"]: - request.session.flash(request.localizer.translate(_("flash.password_mismatch"))) + password = request.params["password"] + if password: + try: + util.check_password_constraints(password, request.params["repeat-password"]) + except ValueError as exc: + request.session.flash(request.localizer.translate(exc.args[0])) return HTTPFound(request.route_url('profile')) request.identity.set_password(request.params["password"]) name = request.params["name"] |