diff options
author | Daniel Schadt <kingdread@gmx.de> | 2022-07-01 18:21:29 +0200 |
---|---|---|
committer | Daniel Schadt <kingdread@gmx.de> | 2022-07-01 18:23:25 +0200 |
commit | a414ce14920d55a5c48ec4111957c680625d6182 (patch) | |
tree | dbed68a1fd9199b09b6008e891f6b21060c8cf10 | |
parent | befdb3883806a48f9d61af3c91b150432d4c8731 (diff) | |
download | fietsboek-a414ce14920d55a5c48ec4111957c680625d6182.tar.gz fietsboek-a414ce14920d55a5c48ec4111957c680625d6182.tar.bz2 fietsboek-a414ce14920d55a5c48ec4111957c680625d6182.zip |
implement account registration
-rw-r--r-- | development.ini | 2 | ||||
-rw-r--r-- | fietsboek/__init__.py | 4 | ||||
-rw-r--r-- | fietsboek/models/user.py | 4 | ||||
-rw-r--r-- | fietsboek/routes.py | 1 | ||||
-rw-r--r-- | fietsboek/static/fietsboek.js | 7 | ||||
-rw-r--r-- | fietsboek/templates/create_account.jinja2 | 45 | ||||
-rw-r--r-- | fietsboek/templates/layout.jinja2 | 5 | ||||
-rw-r--r-- | fietsboek/views/account.py | 72 | ||||
-rw-r--r-- | fietsboek/views/default.py | 5 |
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: |