aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--development.ini3
-rw-r--r--fietsboek/static/fietsboek.js35
-rw-r--r--fietsboek/templates/password_reset.jinja212
-rw-r--r--fietsboek/templates/profile.jinja212
-rw-r--r--fietsboek/util.py25
-rw-r--r--fietsboek/views/default.py9
-rw-r--r--fietsboek/views/profile.py11
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"]