aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Schadt <kingdread@gmx.de>2022-07-01 18:21:29 +0200
committerDaniel Schadt <kingdread@gmx.de>2022-07-01 18:23:25 +0200
commita414ce14920d55a5c48ec4111957c680625d6182 (patch)
treedbed68a1fd9199b09b6008e891f6b21060c8cf10
parentbefdb3883806a48f9d61af3c91b150432d4c8731 (diff)
downloadfietsboek-a414ce14920d55a5c48ec4111957c680625d6182.tar.gz
fietsboek-a414ce14920d55a5c48ec4111957c680625d6182.tar.bz2
fietsboek-a414ce14920d55a5c48ec4111957c680625d6182.zip
implement account registration
-rw-r--r--development.ini2
-rw-r--r--fietsboek/__init__.py4
-rw-r--r--fietsboek/models/user.py4
-rw-r--r--fietsboek/routes.py1
-rw-r--r--fietsboek/static/fietsboek.js7
-rw-r--r--fietsboek/templates/create_account.jinja245
-rw-r--r--fietsboek/templates/layout.jinja25
-rw-r--r--fietsboek/views/account.py72
-rw-r--r--fietsboek/views/default.py5
9 files changed, 143 insertions, 2 deletions
diff --git a/development.ini b/development.ini
index 153e684..a93c838 100644
--- a/development.ini
+++ b/development.ini
@@ -27,6 +27,8 @@ retry.attempts = 3
email.from = fietsboek@kingdread.de
email.smtp_url = debug://localhost:1025
+enable_account_registration = true
+
session_key = hklurha7ildshgfljhrbuajelghug
# By default, the toolbar only appears for clients from IP addresses
diff --git a/fietsboek/__init__.py b/fietsboek/__init__.py
index 1688dde..80b6c7d 100644
--- a/fietsboek/__init__.py
+++ b/fietsboek/__init__.py
@@ -4,6 +4,7 @@ For more information, see the README or the included documentation.
"""
from pyramid.config import Configurator
from pyramid.session import SignedCookieSessionFactory
+from pyramid.settings import asbool
from .security import SecurityPolicy
@@ -15,6 +16,9 @@ def main(global_config, **settings):
if 'session_key' not in settings:
raise ValueError("Please set a session signing key (session_key) in your settings!")
+ settings['enable_account_registration'] = asbool(
+ settings.get('enable_account_registration', 'false'))
+
my_session_factory = SignedCookieSessionFactory(settings['session_key'])
with Configurator(settings=settings) as config:
config.include('pyramid_jinja2')
diff --git a/fietsboek/models/user.py b/fietsboek/models/user.py
index 1c6475e..cc06225 100644
--- a/fietsboek/models/user.py
+++ b/fietsboek/models/user.py
@@ -91,8 +91,8 @@ class User(Base):
password = Column(LargeBinary)
salt = Column(LargeBinary)
email = Column(Text)
- is_admin = Column(Boolean)
- is_verified = Column(Boolean)
+ is_admin = Column(Boolean, default=False)
+ is_verified = Column(Boolean, default=False)
tracks = relationship('Track', back_populates='owner')
tagged_tracks = relationship('Track', secondary='track_people_assoc',
diff --git a/fietsboek/routes.py b/fietsboek/routes.py
index 34917b6..b0ee3c4 100644
--- a/fietsboek/routes.py
+++ b/fietsboek/routes.py
@@ -8,6 +8,7 @@ def includeme(config):
config.add_route('password-reset', '/password-reset')
config.add_route('use-token', '/token/{uuid}')
+ config.add_route('create-account', '/create-account')
config.add_route('upload', '/upload')
config.add_route('preview', '/preview/{id}.gpx')
diff --git a/fietsboek/static/fietsboek.js b/fietsboek/static/fietsboek.js
index 26a0902..5a26cbc 100644
--- a/fietsboek/static/fietsboek.js
+++ b/fietsboek/static/fietsboek.js
@@ -40,6 +40,13 @@ function checkPasswordValidity(main, repeat) {
}
}
+function checkNameValidity(name) {
+ let name_field = document.querySelector(name);
+ if (name_field.value.length == 0) {
+ name_field.setCustomValidity('Needs a name');
+ }
+}
+
document.addEventListener('DOMContentLoaded', function(event) {
/* Enable the "Add tag" button */
let $ = (selector) => document.querySelector(selector);
diff --git a/fietsboek/templates/create_account.jinja2 b/fietsboek/templates/create_account.jinja2
new file mode 100644
index 0000000..cf52203
--- /dev/null
+++ b/fietsboek/templates/create_account.jinja2
@@ -0,0 +1,45 @@
+{% extends "layout.jinja2" %}
+{% block content %}
+<div class="container">
+ <h1>{{ _("page.create_account.title") }}</h1>
+ <form method="POST" action="{{ request.route_path('create-account') }}" class="needs-validation" novalidate>
+ <div class="row mb-3">
+ <label for="inputName" class="col-sm-3 col-form-label">{{ _("page.create_account.email") }}</label>
+ <div class="col-sm-9">
+ <input type="email" class="form-control" id="inputEmail" name="email" required>
+ <div class="invalid-feedback">
+ {{ _("page.create_account.email_invalid") }}
+ </div>
+ </div>
+ </div>
+ <div class="row mb-3">
+ <label for="inputName" class="col-sm-3 col-form-label">{{ _("page.create_account.name") }}</label>
+ <div class="col-sm-9">
+ <input type="text" class="form-control" id="inputName" name="name" onchange="checkNameValidity('#inputName')" required>
+ <div class="invalid-feedback">
+ {{ _("page.create_account.name_invalid") }}
+ </div>
+ </div>
+ </div>
+ <div class="row mb-3">
+ <label for="inputPassword" class="col-sm-3 col-form-label">{{ _("page.create_account.password") }}</label>
+ <div class="col-sm-9">
+ <input type="password" class="form-control" id="inputPassword" name="password" onchange="checkPasswordValidity('#inputPassword', '#repeatPassword')" required>
+ <div class="invalid-feedback">
+ {{ _("page.create_account.password_invalid") }}
+ </div>
+ </div>
+ </div>
+ <div class="row mb-3">
+ <label for="repeatPassword" class="col-sm-3 col-form-label">{{ _("page.create_account.repeat_password") }}</label>
+ <div class="col-sm-9">
+ <input type="password" class="form-control" id="repeatPassword" name="repeat-password" onchange="checkPasswordValidity('#inputPassword', '#repeatPassword')" required>
+ <div class="invalid-feedback">
+ {{ _("page.create_account.password_must_match") }}
+ </div>
+ </div>
+ </div>
+ <button type="submit" class="btn btn-primary"><i class="bi bi-person-plus"></i> {{ _("page.create_account.create") }}</button>
+ </form>
+</div>
+{% endblock %}
diff --git a/fietsboek/templates/layout.jinja2 b/fietsboek/templates/layout.jinja2
index 4c6b1cc..6caf666 100644
--- a/fietsboek/templates/layout.jinja2
+++ b/fietsboek/templates/layout.jinja2
@@ -42,6 +42,11 @@
<li class="nav-item">
<a class="nav-link" href="{{ request.route_url('login') }}">{{ _("page.navbar.login") }}</a>
</li>
+ {% if request.registry.settings.get('enable_account_registration') %}
+ <li class="nav-item">
+ <a class="nav-link" href="{{ request.route_url('create-account') }}">{{ _("page.navbar.create_account") }}</a>
+ </li>
+ {% endif %}
{% else %}
<li class="nav-item">
<a class="nav-link" href="{{ request.route_url('logout') }}">{{ _("page.navbar.logout") }}</a>
diff --git a/fietsboek/views/account.py b/fietsboek/views/account.py
new file mode 100644
index 0000000..e7b1efd
--- /dev/null
+++ b/fietsboek/views/account.py
@@ -0,0 +1,72 @@
+"""Account related endpoints."""
+from pyramid.view import view_config
+from pyramid.i18n import TranslationString as _
+from pyramid.httpexceptions import HTTPForbidden, HTTPFound
+
+from .. import models, util, email
+from ..models.user import TokenType
+
+
+@view_config(route_name="create-account", renderer="fietsboek:templates/create_account.jinja2",
+ request_method="GET")
+def create_account(request):
+ """Shows the "create account" page.
+
+ :param request: The Pyramid request.
+ :type request: pyramid.request.Request
+ :return: The HTTP response.
+ :rtype: pyramid.response.Response
+ """
+ # pylint: disable=unused-argument
+ if not request.registry.settings['enable_account_registration']:
+ return HTTPForbidden()
+ return {}
+
+
+@view_config(route_name="create-account", renderer="fietsboek:templates/create_account.jinja2",
+ request_method="POST")
+def do_create_account(request):
+ """Shows the "create account" page.
+
+ :param request: The Pyramid request.
+ :type request: pyramid.request.Request
+ :return: The HTTP response.
+ :rtype: pyramid.response.Response
+ """
+ if not request.registry.settings['enable_account_registration']:
+ return HTTPForbidden()
+ 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('create-account'))
+
+ name = request.params["name"]
+ if not name:
+ request.session.flash(request.localizer.translate(_('flash.invalid_name')))
+ return HTTPFound(request.route_url('create-account'))
+
+ email_addr = request.params["email"]
+ if not email_addr:
+ request.session.flash(request.localizer.translate(_('flash.invalid_email')))
+ return HTTPFound(request.route_url('create-account'))
+
+ user = models.User(name=name, email=email_addr)
+ user.set_password(password)
+ request.dbsession.add(user)
+
+ token = models.Token.generate(user, TokenType.VERIFY_EMAIL)
+ request.dbsession.add(token)
+
+ message = email.prepare_message(
+ request.registry.settings,
+ user.email,
+ request.localizer.translate(_('email.verify_mail.subject')),
+ )
+ message.set_content(request.localizer.translate(_('email.verify.text'))
+ .format(request.route_url('use-token', uuid=token.uuid)))
+ email.send_message(request.registry.settings, message)
+
+ request.session.flash(request.localizer.translate(_("flash.a_confirmation_link_has_been_sent")))
+ return HTTPFound(request.route_url('login'))
diff --git a/fietsboek/views/default.py b/fietsboek/views/default.py
index 8424b97..913266d 100644
--- a/fietsboek/views/default.py
+++ b/fietsboek/views/default.py
@@ -68,6 +68,10 @@ def do_login(request):
request.session.flash(request.localizer.translate(_('flash.invalid_credentials')))
return HTTPFound(request.route_url('login'))
+ if not user.is_verified:
+ request.session.flash(request.localizer.translate(_('flash.account_not_verified')))
+ return HTTPFound(request.route_url('login'))
+
request.session.flash(request.localizer.translate(_('flash.logged_in')))
headers = remember(request, str(user.id))
return HTTPFound('/', headers=headers)
@@ -154,6 +158,7 @@ def use_token(request):
if token.token_type == TokenType.VERIFY_EMAIL:
token.user.is_verified = True
+ request.dbsession.delete(token)
request.session.flash(request.localizer.translate(_('flash.email_verified')))
return HTTPFound(request.route_url('login'))
if request.method == 'GET' and token.token_type == TokenType.RESET_PASSWORD: