From 1052750f7337db28119cc18dbd009421c61284b8 Mon Sep 17 00:00:00 2001
From: Daniel Schadt <kingdread@gmx.de>
Date: Mon, 29 May 2023 18:30:51 +0200
Subject: [wip] add ability to re-send the verification mail

---
 fietsboek/routes.py                            |  1 +
 fietsboek/templates/login.jinja2               |  2 +
 fietsboek/templates/resend_verification.jinja2 | 22 +++++++++++
 fietsboek/views/default.py                     | 51 ++++++++++++++++++++++++++
 4 files changed, 76 insertions(+)
 create mode 100644 fietsboek/templates/resend_verification.jinja2

diff --git a/fietsboek/routes.py b/fietsboek/routes.py
index 8f109d9..60a566d 100644
--- a/fietsboek/routes.py
+++ b/fietsboek/routes.py
@@ -14,6 +14,7 @@ def includeme(config):
     config.add_route("track-archive", "/track/archive")
 
     config.add_route("password-reset", "/password-reset")
+    config.add_route("resend-verification", "/resend-verification")
     config.add_route("use-token", "/token/{uuid}")
     config.add_route("create-account", "/create-account")
 
diff --git a/fietsboek/templates/login.jinja2 b/fietsboek/templates/login.jinja2
index 7a9bf07..bdf2c99 100644
--- a/fietsboek/templates/login.jinja2
+++ b/fietsboek/templates/login.jinja2
@@ -41,6 +41,8 @@
     <div class="row justify-content-center">
       <div class="col-auto mb-3">
         <a href="{{ request.route_url("password-reset") }}">{{ _("page.login.forgot_password") }}</a>
+        &bull;
+        <a href="{{ request.route_url("resend-verification") }}">{{ _("page.login.resend_verification") }}</a>
       </div>
     </div>
   </form>
diff --git a/fietsboek/templates/resend_verification.jinja2 b/fietsboek/templates/resend_verification.jinja2
new file mode 100644
index 0000000..cc56854
--- /dev/null
+++ b/fietsboek/templates/resend_verification.jinja2
@@ -0,0 +1,22 @@
+{% extends "layout.jinja2" %}
+{% import "util.jinja2" as util with context %}
+{% block content %}
+<div class="container">
+  <h1>{{ _("page.resend_verification.title") }}</h1>
+  <p>{{ _("page.resend_verification.info") }}</p>
+  <form method="POST">
+    <div class="row align-items-center">
+      <div class="col-lg-5">
+        <div class="form-floating">
+          <input type="email" id="resendEmail" name="email" class="form-control" placeholder="x">
+          <label for="resendEmail">{{ _("page.resend_verification.email") }}</label>
+        </div>
+      </div>
+      {{ util.hidden_csrf_input() }}
+      <div class="col-lg-4">
+        <button class="btn btn-primary">{{ _("page.resend_verification.request") }}</button>
+      </div>
+    </div>
+  </form>
+</div>
+{% endblock %}
diff --git a/fietsboek/views/default.py b/fietsboek/views/default.py
index b8b1835..397f9ca 100644
--- a/fietsboek/views/default.py
+++ b/fietsboek/views/default.py
@@ -196,6 +196,57 @@ def do_password_reset(request: Request) -> Response:
     return HTTPFound(request.route_url("password-reset"))
 
 
+@view_config(
+    route_name="resend-verification",
+    request_method="GET",
+    renderer="fietsboek:templates/resend_verification.jinja2",
+)
+def resend_verification(request: Request) -> Response:
+    """Form to request a new verification mail.
+
+    :param request: The Pyramid request.
+    :return: The HTTP response.
+    """
+    return {}
+
+
+@view_config(route_name="resend-verification", request_method="POST")
+def do_resend_verification(request: Request) -> Response:
+    """Endpoint for the verification resend form.
+
+    :param request: The Pyramid request.
+    :return: The HTTP response.
+    """
+    query = models.User.query_by_email(request.params["email"])
+    user = request.dbsession.execute(query).scalar_one_or_none()
+    if user is None or user.is_verified:
+        request.session.flash(request.localizer.translate(_("flash.resend_verification_email")))
+        return HTTPFound(request.route_url("resend-verification"))
+
+    token = models.Token.generate(user, TokenType.VERIFY_EMAIL)
+    request.dbsession.add(token)
+    request.session.flash(request.localizer.translate(_("flash.verification_token_generated")))
+
+    mail = email.prepare_message(
+        request.config.email_from,
+        user.email,
+        request.localizer.translate(_("page.password_reset.email.subject")),
+    )
+    mail.set_content(
+        request.localizer.translate(_("page.password_reset.email.body")).format(
+            request.route_url("use-token", uuid=token.uuid)
+        )
+    )
+    email.send_message(
+        request.config.email_smtp_url,
+        request.config.email_username,
+        request.config.email_password.get_secret_value(),
+        mail,
+    )
+
+    return HTTPFound(request.route_url("password-reset"))
+
+
 @view_config(route_name="use-token")
 def use_token(request: Request) -> Response:
     """Endpoint with which a user can use a token for a password reset or email
-- 
cgit v1.2.3


From c87903ca4d01382623fb1ac3ce076456c279b841 Mon Sep 17 00:00:00 2001
From: Daniel Schadt <kingdread@gmx.de>
Date: Tue, 30 May 2023 19:02:34 +0200
Subject: verification: use right mail text and subject

---
 fietsboek/views/default.py | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/fietsboek/views/default.py b/fietsboek/views/default.py
index 397f9ca..b3cad40 100644
--- a/fietsboek/views/default.py
+++ b/fietsboek/views/default.py
@@ -220,7 +220,8 @@ def do_resend_verification(request: Request) -> Response:
     query = models.User.query_by_email(request.params["email"])
     user = request.dbsession.execute(query).scalar_one_or_none()
     if user is None or user.is_verified:
-        request.session.flash(request.localizer.translate(_("flash.resend_verification_email")))
+        request.session.flash(
+            request.localizer.translate(_("flash.resend_verification_email_fail")))
         return HTTPFound(request.route_url("resend-verification"))
 
     token = models.Token.generate(user, TokenType.VERIFY_EMAIL)
@@ -230,10 +231,10 @@ def do_resend_verification(request: Request) -> Response:
     mail = email.prepare_message(
         request.config.email_from,
         user.email,
-        request.localizer.translate(_("page.password_reset.email.subject")),
+        request.localizer.translate(_("email.verify_mail.subject")),
     )
     mail.set_content(
-        request.localizer.translate(_("page.password_reset.email.body")).format(
+        request.localizer.translate(_("email.verify.text")).format(
             request.route_url("use-token", uuid=token.uuid)
         )
     )
@@ -244,7 +245,7 @@ def do_resend_verification(request: Request) -> Response:
         mail,
     )
 
-    return HTTPFound(request.route_url("password-reset"))
+    return HTTPFound(request.route_url("login"))
 
 
 @view_config(route_name="use-token")
-- 
cgit v1.2.3


From 42c99034be468f6968274700cd6c5abbd7da40ff Mon Sep 17 00:00:00 2001
From: Daniel Schadt <kingdread@gmx.de>
Date: Tue, 30 May 2023 19:45:09 +0200
Subject: de-duplicate verification token code

---
 fietsboek/actions.py       | 43 ++++++++++++++++++++++++++++++++++++++++++-
 fietsboek/views/account.py | 23 ++---------------------
 fietsboek/views/default.py | 27 +++++----------------------
 3 files changed, 49 insertions(+), 44 deletions(-)

diff --git a/fietsboek/actions.py b/fietsboek/actions.py
index b9221c3..395843f 100644
--- a/fietsboek/actions.py
+++ b/fietsboek/actions.py
@@ -12,15 +12,17 @@ from typing import List, Optional
 
 import brotli
 import gpxpy
+from pyramid.i18n import TranslationString as _
 from pyramid.request import Request
 from sqlalchemy import select
 from sqlalchemy.orm.session import Session
 
-from . import models
+from . import email, models
 from . import transformers as mod_transformers
 from . import util
 from .data import DataManager, TrackDataDir
 from .models.track import TrackType, Visibility
+from .models.user import TokenType
 
 LOGGER = logging.getLogger(__name__)
 
@@ -219,3 +221,42 @@ def execute_transformers(request: Request, track: models.Track) -> Optional[gpxp
     track.ensure_cache(gpx)
     request.dbsession.add(track.cache)
     return gpx
+
+
+def send_verification_token(request: Request, user: models.User):
+    """Creates a verification token and sends it to the user.
+
+    If no verification token exists yet, a fresh one is created.
+
+    If a token already exists, a fresh one is still created. The old token
+    stays valid until its expiry date.
+
+    Note that this function does not provide the user with feedback other than
+    the email. You may want to show a flash message or similar to show that
+    something has happened.
+
+    :param request: The request.
+    :param user: The user who to generate a verification token for.
+    """
+    # Some of this code appears in the password reset form as well, but that's
+    # fine for now.
+    # pylint: disable=duplicate-code
+    token = models.Token.generate(user, TokenType.VERIFY_EMAIL)
+    request.dbsession.add(token)
+
+    message = email.prepare_message(
+        request.config.email_from,
+        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.config.email_smtp_url,
+        request.config.email_username,
+        request.config.email_password.get_secret_value(),
+        message,
+    )
diff --git a/fietsboek/views/account.py b/fietsboek/views/account.py
index 39a62e5..5400f0a 100644
--- a/fietsboek/views/account.py
+++ b/fietsboek/views/account.py
@@ -3,8 +3,7 @@ from pyramid.httpexceptions import HTTPForbidden, HTTPFound
 from pyramid.i18n import TranslationString as _
 from pyramid.view import view_config
 
-from .. import email, models, util
-from ..models.user import TokenType
+from .. import actions, models, util
 
 
 @view_config(
@@ -63,25 +62,7 @@ def do_create_account(request):
     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.config.email_from,
-        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.config.email_smtp_url,
-        request.config.email_username,
-        request.config.email_password.get_secret_value(),
-        message,
-    )
+    actions.send_verification_token(request, user)
 
     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 b3cad40..f9942c3 100644
--- a/fietsboek/views/default.py
+++ b/fietsboek/views/default.py
@@ -12,7 +12,7 @@ from sqlalchemy import select
 from sqlalchemy.exc import NoResultFound
 from sqlalchemy.orm import aliased
 
-from .. import email, models, summaries, util
+from .. import actions, email, models, summaries, util
 from ..models.track import TrackType, TrackWithMetadata
 from ..models.user import TOKEN_LIFETIME, PasswordMismatch, TokenType
 
@@ -201,7 +201,7 @@ def do_password_reset(request: Request) -> Response:
     request_method="GET",
     renderer="fietsboek:templates/resend_verification.jinja2",
 )
-def resend_verification(request: Request) -> Response:
+def resend_verification(_request: Request) -> Response:
     """Form to request a new verification mail.
 
     :param request: The Pyramid request.
@@ -221,30 +221,13 @@ def do_resend_verification(request: Request) -> Response:
     user = request.dbsession.execute(query).scalar_one_or_none()
     if user is None or user.is_verified:
         request.session.flash(
-            request.localizer.translate(_("flash.resend_verification_email_fail")))
+            request.localizer.translate(_("flash.resend_verification_email_fail"))
+        )
         return HTTPFound(request.route_url("resend-verification"))
 
-    token = models.Token.generate(user, TokenType.VERIFY_EMAIL)
-    request.dbsession.add(token)
+    actions.send_verification_token(request, user)
     request.session.flash(request.localizer.translate(_("flash.verification_token_generated")))
 
-    mail = email.prepare_message(
-        request.config.email_from,
-        user.email,
-        request.localizer.translate(_("email.verify_mail.subject")),
-    )
-    mail.set_content(
-        request.localizer.translate(_("email.verify.text")).format(
-            request.route_url("use-token", uuid=token.uuid)
-        )
-    )
-    email.send_message(
-        request.config.email_smtp_url,
-        request.config.email_username,
-        request.config.email_password.get_secret_value(),
-        mail,
-    )
-
     return HTTPFound(request.route_url("login"))
 
 
-- 
cgit v1.2.3


From 3151722f364b0fc1b27b4fd545b80e7f44d0395d Mon Sep 17 00:00:00 2001
From: Daniel Schadt <kingdread@gmx.de>
Date: Wed, 31 May 2023 20:42:28 +0200
Subject: add test for verification mail resending

---
 tests/integration/test_register.py | 40 ++++++++++++++++++++++++++++++++------
 1 file changed, 34 insertions(+), 6 deletions(-)

diff --git a/tests/integration/test_register.py b/tests/integration/test_register.py
index af1e313..dc7b00a 100644
--- a/tests/integration/test_register.py
+++ b/tests/integration/test_register.py
@@ -1,19 +1,28 @@
 import re
 
+import pytest
+
 import fietsboek.email
 from fietsboek import models
 
 VERIFICATION_LINK_PATTERN = re.compile("http://example.com(/token/[A-Za-z0-9-]+)")
 
 
-def test_registration_working(testapp, dbsession, route_path, monkeypatch):
-    """Ensures that a user can register, including using the verification link."""
+@pytest.fixture
+def mailcatcher(monkeypatch):
+    """Monkeypatches the send mail functionality.
+
+    Returns the list of mails sent.
+    """
     mails = []
-    def send_message(server_url, username, password, message):
+    def send_message(_server_url, _username, _passwords, message):
         mails.append(message)
-
     monkeypatch.setattr(fietsboek.email, "send_message", send_message)
+    yield mails
 
+
+def test_registration_working(testapp, dbsession, route_path, mailcatcher):
+    """Ensures that a user can register, including using the verification link."""
     registration = testapp.get(route_path('create-account'))
     form = registration.form
     form['email'] = 'foo-new@bar.com'
@@ -23,18 +32,37 @@ def test_registration_working(testapp, dbsession, route_path, monkeypatch):
     response = form.submit().maybe_follow()
 
     assert b'A confirmation link has been sent' in response.body
-    assert len(mails) == 1
+    assert len(mailcatcher) == 1
 
     user = dbsession.execute(models.User.query_by_email('foo-new@bar.com')).scalar_one()
     assert not user.is_verified
 
-    body = mails[0].get_body().get_content()
+    body = mailcatcher[0].get_body().get_content()
     token_path = VERIFICATION_LINK_PATTERN.search(body).group(1)
     testapp.get(token_path)
 
     assert user.is_verified
 
 
+def test_resend_verification_mail(testapp, dbsession, route_path, mailcatcher):
+    """Ensures that the verification link re-sending works."""
+    registration = testapp.get(route_path('create-account'))
+    form = registration.form
+    form['email'] = 'foo-new@bar.com'
+    form['name'] = 'The new Foo'
+    form['password'] = 'foobarpassword'
+    form['repeat-password'] = 'foobarpassword'
+    form.submit().maybe_follow()
+
+    req = testapp.get(route_path('resend-verification'))
+    req.form['email'] = 'foo-new@bar.com'
+    req.form.submit().maybe_follow()
+
+    assert len(mailcatcher) == 2
+    assert VERIFICATION_LINK_PATTERN.search(mailcatcher[0].get_body().get_content())
+    assert VERIFICATION_LINK_PATTERN.search(mailcatcher[1].get_body().get_content())
+
+
 def test_registration_short_password(testapp, route_path):
     """Ensures that passwords that are too short are rejected."""
     registration = testapp.get(route_path('create-account'))
-- 
cgit v1.2.3


From 0eb281e89241095c82c6e6b9f8b60f0771a8cd8a Mon Sep 17 00:00:00 2001
From: Daniel Schadt <kingdread@gmx.de>
Date: Wed, 31 May 2023 21:02:49 +0200
Subject: add translations for verification resending

---
 fietsboek/locale/de/LC_MESSAGES/messages.mo | Bin 13807 -> 14476 bytes
 fietsboek/locale/de/LC_MESSAGES/messages.po | 103 ++++++++++++++++++----------
 fietsboek/locale/en/LC_MESSAGES/messages.mo | Bin 12916 -> 13505 bytes
 fietsboek/locale/en/LC_MESSAGES/messages.po | 102 +++++++++++++++++----------
 fietsboek/locale/fietslog.pot               |  96 +++++++++++++++++---------
 5 files changed, 193 insertions(+), 108 deletions(-)

diff --git a/fietsboek/locale/de/LC_MESSAGES/messages.mo b/fietsboek/locale/de/LC_MESSAGES/messages.mo
index 887f9bc..10f468c 100644
Binary files a/fietsboek/locale/de/LC_MESSAGES/messages.mo and b/fietsboek/locale/de/LC_MESSAGES/messages.mo differ
diff --git a/fietsboek/locale/de/LC_MESSAGES/messages.po b/fietsboek/locale/de/LC_MESSAGES/messages.po
index f5528c9..cb62dbb 100644
--- a/fietsboek/locale/de/LC_MESSAGES/messages.po
+++ b/fietsboek/locale/de/LC_MESSAGES/messages.po
@@ -7,7 +7,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PROJECT VERSION\n"
 "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
-"POT-Creation-Date: 2023-05-22 22:40+0200\n"
+"POT-Creation-Date: 2023-05-31 20:46+0200\n"
 "PO-Revision-Date: 2022-07-02 17:35+0200\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language: de\n"
@@ -18,11 +18,22 @@ msgstr ""
 "Content-Transfer-Encoding: 8bit\n"
 "Generated-By: Babel 2.12.1\n"
 
-#: fietsboek/util.py:280
+#: fietsboek/actions.py:250
+msgid "email.verify_mail.subject"
+msgstr "Fietsboek Konto Bestätigung"
+
+#: fietsboek/actions.py:253
+msgid "email.verify.text"
+msgstr ""
+"Um Dein Fietsboek-Konto zu bestätigen, nutze diesen Link: {}\n"
+"\n"
+"Falls Du kein Konto angelegt hast, ignoriere diese E-Mail."
+
+#: fietsboek/util.py:304
 msgid "password_constraint.mismatch"
 msgstr "Passwörter stimmen nicht überein"
 
-#: fietsboek/util.py:282
+#: fietsboek/util.py:306
 msgid "password_constraint.length"
 msgstr "Passwort zu kurz"
 
@@ -478,72 +489,72 @@ msgstr "Absenden"
 msgid "page.upload.form.cancel"
 msgstr "Abbrechen"
 
-#: fietsboek/templates/home.jinja2:5
+#: fietsboek/templates/home.jinja2:6
 msgid "page.home.title"
 msgstr "Startseite"
 
-#: fietsboek/templates/home.jinja2:8
+#: fietsboek/templates/home.jinja2:17
 msgid "page.home.unfinished_uploads"
 msgstr ""
 "Es sind noch nicht abgeschlossene Uploads vorhanden. Klicke auf die "
 "Links, um sie fortzusetzen:"
 
-#: fietsboek/templates/home.jinja2:27 fietsboek/templates/home.jinja2:34
-#: fietsboek/templates/home.jinja2:52
+#: fietsboek/templates/home.jinja2:31 fietsboek/templates/home.jinja2:38
+#: fietsboek/templates/home.jinja2:56
 msgid "page.home.summary.track"
 msgid_plural "page.home.summary.tracks"
 msgstr[0] "%(num)d Strecke"
 msgstr[1] "%(num)d Strecken"
 
-#: fietsboek/templates/home.jinja2:52
+#: fietsboek/templates/home.jinja2:56
 msgid "page.home.total"
 msgstr "Gesamt"
 
-#: fietsboek/templates/layout.jinja2:41
+#: fietsboek/templates/layout.jinja2:43
 msgid "page.navbar.toggle"
 msgstr "Navigation umschalten"
 
-#: fietsboek/templates/layout.jinja2:52
+#: fietsboek/templates/layout.jinja2:54
 msgid "page.navbar.home"
 msgstr "Startseite"
 
-#: fietsboek/templates/layout.jinja2:55
+#: fietsboek/templates/layout.jinja2:57
 msgid "page.navbar.browse"
 msgstr "Stöbern"
 
-#: fietsboek/templates/layout.jinja2:59
+#: fietsboek/templates/layout.jinja2:61
 msgid "page.navbar.upload"
 msgstr "Hochladen"
 
-#: fietsboek/templates/layout.jinja2:68
+#: fietsboek/templates/layout.jinja2:70
 msgid "page.navbar.user"
 msgstr "Nutzer"
 
-#: fietsboek/templates/layout.jinja2:72
+#: fietsboek/templates/layout.jinja2:74
 msgid "page.navbar.welcome_user"
 msgstr "Willkommen, {}!"
 
-#: fietsboek/templates/layout.jinja2:75
+#: fietsboek/templates/layout.jinja2:77
 msgid "page.navbar.logout"
 msgstr "Abmelden"
 
-#: fietsboek/templates/layout.jinja2:78
+#: fietsboek/templates/layout.jinja2:80
 msgid "page.navbar.profile"
 msgstr "Profil"
 
-#: fietsboek/templates/layout.jinja2:81
+#: fietsboek/templates/layout.jinja2:83
 msgid "page.navbar.user_data"
 msgstr "Persönliche Daten"
 
-#: fietsboek/templates/layout.jinja2:85
+#: fietsboek/templates/layout.jinja2:87
 msgid "page.navbar.admin"
 msgstr "Admin"
 
-#: fietsboek/templates/layout.jinja2:91
+#: fietsboek/templates/layout.jinja2:93
 msgid "page.navbar.login"
 msgstr "Anmelden"
 
-#: fietsboek/templates/layout.jinja2:95
+#: fietsboek/templates/layout.jinja2:97
 msgid "page.navbar.create_account"
 msgstr "Konto Erstellen"
 
@@ -571,6 +582,10 @@ msgstr "Anmelden"
 msgid "page.login.forgot_password"
 msgstr "Passwort vergessen"
 
+#: fietsboek/templates/login.jinja2:45
+msgid "page.login.resend_verification"
+msgstr "Bestätigungsmail erneut senden"
+
 #: fietsboek/templates/password_reset.jinja2:5
 msgid "page.password_reset.title"
 msgstr "Passwort Zurücksetzen"
@@ -669,6 +684,23 @@ msgstr "E-Mail-Adresse"
 msgid "page.request_password.request"
 msgstr "Anfrage senden"
 
+#: fietsboek/templates/resend_verification.jinja2:5
+msgid "page.resend_verification.title"
+msgstr "Bestätigungsmail Erneut Senden"
+
+#: fietsboek/templates/resend_verification.jinja2:6
+msgid "page.resend_verification.info"
+msgstr ""
+"Hier kannst Du eine neue E-Mail zur Bestätigung Deines Kontos anfordern."
+
+#: fietsboek/templates/resend_verification.jinja2:12
+msgid "page.resend_verification.email"
+msgstr "E-Mail-Adresse"
+
+#: fietsboek/templates/resend_verification.jinja2:17
+msgid "page.resend_verification.request"
+msgstr "E-Mail anfordern"
+
 #: fietsboek/templates/upload.jinja2:9
 msgid "page.upload.form.gpx"
 msgstr "GPX Datei"
@@ -753,26 +785,15 @@ msgstr ""
 "Diese Transformation passt die Höhenangabe für Punkte an, bei denen die "
 "Höhe sprunghaft steigt oder fällt."
 
-#: fietsboek/views/account.py:54
+#: fietsboek/views/account.py:53
 msgid "flash.invalid_name"
 msgstr "Ungültiger Name"
 
-#: fietsboek/views/account.py:59
+#: fietsboek/views/account.py:58
 msgid "flash.invalid_email"
 msgstr "Ungültige E-Mail-Adresse"
 
-#: fietsboek/views/account.py:72
-msgid "email.verify_mail.subject"
-msgstr "Fietsboek Konto Bestätigung"
-
-#: fietsboek/views/account.py:75
-msgid "email.verify.text"
-msgstr ""
-"Um Dein Fietsboek-Konto zu bestätigen, nutze diesen Link: {}\n"
-"\n"
-"Falls Du kein Konto angelegt hast, ignoriere diese E-Mail."
-
-#: fietsboek/views/account.py:86
+#: fietsboek/views/account.py:67
 msgid "flash.a_confirmation_link_has_been_sent"
 msgstr "Ein Bestätigungslink wurde versandt"
 
@@ -824,15 +845,23 @@ msgstr ""
 "Falls Du keine Passwortzurücksetzung beantragt hast, dann ignoriere diese"
 " E-Mail."
 
-#: fietsboek/views/default.py:214
+#: fietsboek/views/default.py:224
+msgid "flash.resend_verification_email_fail"
+msgstr "Ungültige E-Mail-Adresse angegeben"
+
+#: fietsboek/views/default.py:229
+msgid "flash.verification_token_generated"
+msgstr "Ein Link zur Bestätigung Deines Kontos wurde versandt"
+
+#: fietsboek/views/default.py:249
 msgid "flash.token_expired"
 msgstr "Der Link ist nicht mehr gültig"
 
-#: fietsboek/views/default.py:220
+#: fietsboek/views/default.py:255
 msgid "flash.email_verified"
 msgstr "E-Mail-Adresse bestätigt"
 
-#: fietsboek/views/default.py:234
+#: fietsboek/views/default.py:269
 msgid "flash.password_updated"
 msgstr "Passwort aktualisiert"
 
diff --git a/fietsboek/locale/en/LC_MESSAGES/messages.mo b/fietsboek/locale/en/LC_MESSAGES/messages.mo
index bfd051f..df35686 100644
Binary files a/fietsboek/locale/en/LC_MESSAGES/messages.mo and b/fietsboek/locale/en/LC_MESSAGES/messages.mo differ
diff --git a/fietsboek/locale/en/LC_MESSAGES/messages.po b/fietsboek/locale/en/LC_MESSAGES/messages.po
index 2ec7b76..1cac820 100644
--- a/fietsboek/locale/en/LC_MESSAGES/messages.po
+++ b/fietsboek/locale/en/LC_MESSAGES/messages.po
@@ -7,7 +7,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PROJECT VERSION\n"
 "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
-"POT-Creation-Date: 2023-05-22 22:40+0200\n"
+"POT-Creation-Date: 2023-05-31 20:46+0200\n"
 "PO-Revision-Date: 2023-04-03 20:42+0200\n"
 "Last-Translator: \n"
 "Language: en\n"
@@ -18,11 +18,22 @@ msgstr ""
 "Content-Transfer-Encoding: 8bit\n"
 "Generated-By: Babel 2.12.1\n"
 
-#: fietsboek/util.py:280
+#: fietsboek/actions.py:250
+msgid "email.verify_mail.subject"
+msgstr "Fietsboek Account Verification"
+
+#: fietsboek/actions.py:253
+msgid "email.verify.text"
+msgstr ""
+"To verify your Fietsboek account, please use this link: {}\n"
+"\n"
+"If you did not create an account, ignore this email."
+
+#: fietsboek/util.py:304
 msgid "password_constraint.mismatch"
 msgstr "Passwords don't match"
 
-#: fietsboek/util.py:282
+#: fietsboek/util.py:306
 msgid "password_constraint.length"
 msgstr "Password not long enough"
 
@@ -474,70 +485,70 @@ msgstr "Upload"
 msgid "page.upload.form.cancel"
 msgstr "Cancel"
 
-#: fietsboek/templates/home.jinja2:5
+#: fietsboek/templates/home.jinja2:6
 msgid "page.home.title"
 msgstr "Home"
 
-#: fietsboek/templates/home.jinja2:8
+#: fietsboek/templates/home.jinja2:17
 msgid "page.home.unfinished_uploads"
 msgstr "You have unfinished uploads. Click on the links below to resume them:"
 
-#: fietsboek/templates/home.jinja2:27 fietsboek/templates/home.jinja2:34
-#: fietsboek/templates/home.jinja2:52
+#: fietsboek/templates/home.jinja2:31 fietsboek/templates/home.jinja2:38
+#: fietsboek/templates/home.jinja2:56
 msgid "page.home.summary.track"
 msgid_plural "page.home.summary.tracks"
 msgstr[0] "%(num)d track"
 msgstr[1] "%(num)d tracks"
 
-#: fietsboek/templates/home.jinja2:52
+#: fietsboek/templates/home.jinja2:56
 msgid "page.home.total"
 msgstr "Total"
 
-#: fietsboek/templates/layout.jinja2:41
+#: fietsboek/templates/layout.jinja2:43
 msgid "page.navbar.toggle"
 msgstr "Toggle navigation"
 
-#: fietsboek/templates/layout.jinja2:52
+#: fietsboek/templates/layout.jinja2:54
 msgid "page.navbar.home"
 msgstr "Home"
 
-#: fietsboek/templates/layout.jinja2:55
+#: fietsboek/templates/layout.jinja2:57
 msgid "page.navbar.browse"
 msgstr "Browse"
 
-#: fietsboek/templates/layout.jinja2:59
+#: fietsboek/templates/layout.jinja2:61
 msgid "page.navbar.upload"
 msgstr "Upload"
 
-#: fietsboek/templates/layout.jinja2:68
+#: fietsboek/templates/layout.jinja2:70
 msgid "page.navbar.user"
 msgstr "User"
 
-#: fietsboek/templates/layout.jinja2:72
+#: fietsboek/templates/layout.jinja2:74
 msgid "page.navbar.welcome_user"
 msgstr "Welcome, {}!"
 
-#: fietsboek/templates/layout.jinja2:75
+#: fietsboek/templates/layout.jinja2:77
 msgid "page.navbar.logout"
 msgstr "Logout"
 
-#: fietsboek/templates/layout.jinja2:78
+#: fietsboek/templates/layout.jinja2:80
 msgid "page.navbar.profile"
 msgstr "Profile"
 
-#: fietsboek/templates/layout.jinja2:81
+#: fietsboek/templates/layout.jinja2:83
 msgid "page.navbar.user_data"
 msgstr "Personal Data"
 
-#: fietsboek/templates/layout.jinja2:85
+#: fietsboek/templates/layout.jinja2:87
 msgid "page.navbar.admin"
 msgstr "Admin"
 
-#: fietsboek/templates/layout.jinja2:91
+#: fietsboek/templates/layout.jinja2:93
 msgid "page.navbar.login"
 msgstr "Login"
 
-#: fietsboek/templates/layout.jinja2:95
+#: fietsboek/templates/layout.jinja2:97
 msgid "page.navbar.create_account"
 msgstr "Create Account"
 
@@ -565,6 +576,10 @@ msgstr "Login"
 msgid "page.login.forgot_password"
 msgstr "Forgot password"
 
+#: fietsboek/templates/login.jinja2:45
+msgid "page.login.resend_verification"
+msgstr "Re-send verification mail"
+
 #: fietsboek/templates/password_reset.jinja2:5
 msgid "page.password_reset.title"
 msgstr "Reset Your Password"
@@ -663,6 +678,22 @@ msgstr "Email"
 msgid "page.request_password.request"
 msgstr "Send request"
 
+#: fietsboek/templates/resend_verification.jinja2:5
+msgid "page.resend_verification.title"
+msgstr "Re-send Verification Mail"
+
+#: fietsboek/templates/resend_verification.jinja2:6
+msgid "page.resend_verification.info"
+msgstr "Here you can request a new mail to verify your account"
+
+#: fietsboek/templates/resend_verification.jinja2:12
+msgid "page.resend_verification.email"
+msgstr "Email"
+
+#: fietsboek/templates/resend_verification.jinja2:17
+msgid "page.resend_verification.request"
+msgstr "Send request"
+
 #: fietsboek/templates/upload.jinja2:9
 msgid "page.upload.form.gpx"
 msgstr "GPX file"
@@ -743,26 +774,15 @@ msgstr "Fix elevation jumps"
 msgid "transformers.fix-elevation-jumps.description"
 msgstr "This transformer fixes abrupt jumps in the elevation value."
 
-#: fietsboek/views/account.py:54
+#: fietsboek/views/account.py:53
 msgid "flash.invalid_name"
 msgstr "Invalid name"
 
-#: fietsboek/views/account.py:59
+#: fietsboek/views/account.py:58
 msgid "flash.invalid_email"
 msgstr "Invalid email"
 
-#: fietsboek/views/account.py:72
-msgid "email.verify_mail.subject"
-msgstr "Fietsboek Account Verification"
-
-#: fietsboek/views/account.py:75
-msgid "email.verify.text"
-msgstr ""
-"To verify your Fietsboek account, please use this link: {}\n"
-"\n"
-"If you did not create an account, ignore this email."
-
-#: fietsboek/views/account.py:86
+#: fietsboek/views/account.py:67
 msgid "flash.a_confirmation_link_has_been_sent"
 msgstr "A confirmation link has been sent"
 
@@ -813,15 +833,23 @@ msgstr ""
 "\n"
 "If you did not request a password reset, ignore this email."
 
-#: fietsboek/views/default.py:214
+#: fietsboek/views/default.py:224
+msgid "flash.resend_verification_email_fail"
+msgstr "Invalid email address provided"
+
+#: fietsboek/views/default.py:229
+msgid "flash.verification_token_generated"
+msgstr "A verification email has been sent"
+
+#: fietsboek/views/default.py:249
 msgid "flash.token_expired"
 msgstr "The link has expired"
 
-#: fietsboek/views/default.py:220
+#: fietsboek/views/default.py:255
 msgid "flash.email_verified"
 msgstr "Your email address has been verified"
 
-#: fietsboek/views/default.py:234
+#: fietsboek/views/default.py:269
 msgid "flash.password_updated"
 msgstr "Password has been updated"
 
diff --git a/fietsboek/locale/fietslog.pot b/fietsboek/locale/fietslog.pot
index 113d2d1..8d43284 100644
--- a/fietsboek/locale/fietslog.pot
+++ b/fietsboek/locale/fietslog.pot
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PROJECT VERSION\n"
 "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
-"POT-Creation-Date: 2023-05-22 22:40+0200\n"
+"POT-Creation-Date: 2023-05-31 20:46+0200\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -17,11 +17,19 @@ msgstr ""
 "Content-Transfer-Encoding: 8bit\n"
 "Generated-By: Babel 2.12.1\n"
 
-#: fietsboek/util.py:280
+#: fietsboek/actions.py:250
+msgid "email.verify_mail.subject"
+msgstr ""
+
+#: fietsboek/actions.py:253
+msgid "email.verify.text"
+msgstr ""
+
+#: fietsboek/util.py:304
 msgid "password_constraint.mismatch"
 msgstr ""
 
-#: fietsboek/util.py:282
+#: fietsboek/util.py:306
 msgid "password_constraint.length"
 msgstr ""
 
@@ -471,70 +479,70 @@ msgstr ""
 msgid "page.upload.form.cancel"
 msgstr ""
 
-#: fietsboek/templates/home.jinja2:5
+#: fietsboek/templates/home.jinja2:6
 msgid "page.home.title"
 msgstr ""
 
-#: fietsboek/templates/home.jinja2:8
+#: fietsboek/templates/home.jinja2:17
 msgid "page.home.unfinished_uploads"
 msgstr ""
 
-#: fietsboek/templates/home.jinja2:27 fietsboek/templates/home.jinja2:34
-#: fietsboek/templates/home.jinja2:52
+#: fietsboek/templates/home.jinja2:31 fietsboek/templates/home.jinja2:38
+#: fietsboek/templates/home.jinja2:56
 msgid "page.home.summary.track"
 msgid_plural "page.home.summary.tracks"
 msgstr[0] ""
 msgstr[1] ""
 
-#: fietsboek/templates/home.jinja2:52
+#: fietsboek/templates/home.jinja2:56
 msgid "page.home.total"
 msgstr ""
 
-#: fietsboek/templates/layout.jinja2:41
+#: fietsboek/templates/layout.jinja2:43
 msgid "page.navbar.toggle"
 msgstr ""
 
-#: fietsboek/templates/layout.jinja2:52
+#: fietsboek/templates/layout.jinja2:54
 msgid "page.navbar.home"
 msgstr ""
 
-#: fietsboek/templates/layout.jinja2:55
+#: fietsboek/templates/layout.jinja2:57
 msgid "page.navbar.browse"
 msgstr ""
 
-#: fietsboek/templates/layout.jinja2:59
+#: fietsboek/templates/layout.jinja2:61
 msgid "page.navbar.upload"
 msgstr ""
 
-#: fietsboek/templates/layout.jinja2:68
+#: fietsboek/templates/layout.jinja2:70
 msgid "page.navbar.user"
 msgstr ""
 
-#: fietsboek/templates/layout.jinja2:72
+#: fietsboek/templates/layout.jinja2:74
 msgid "page.navbar.welcome_user"
 msgstr ""
 
-#: fietsboek/templates/layout.jinja2:75
+#: fietsboek/templates/layout.jinja2:77
 msgid "page.navbar.logout"
 msgstr ""
 
-#: fietsboek/templates/layout.jinja2:78
+#: fietsboek/templates/layout.jinja2:80
 msgid "page.navbar.profile"
 msgstr ""
 
-#: fietsboek/templates/layout.jinja2:81
+#: fietsboek/templates/layout.jinja2:83
 msgid "page.navbar.user_data"
 msgstr ""
 
-#: fietsboek/templates/layout.jinja2:85
+#: fietsboek/templates/layout.jinja2:87
 msgid "page.navbar.admin"
 msgstr ""
 
-#: fietsboek/templates/layout.jinja2:91
+#: fietsboek/templates/layout.jinja2:93
 msgid "page.navbar.login"
 msgstr ""
 
-#: fietsboek/templates/layout.jinja2:95
+#: fietsboek/templates/layout.jinja2:97
 msgid "page.navbar.create_account"
 msgstr ""
 
@@ -562,6 +570,10 @@ msgstr ""
 msgid "page.login.forgot_password"
 msgstr ""
 
+#: fietsboek/templates/login.jinja2:45
+msgid "page.login.resend_verification"
+msgstr ""
+
 #: fietsboek/templates/password_reset.jinja2:5
 msgid "page.password_reset.title"
 msgstr ""
@@ -658,6 +670,22 @@ msgstr ""
 msgid "page.request_password.request"
 msgstr ""
 
+#: fietsboek/templates/resend_verification.jinja2:5
+msgid "page.resend_verification.title"
+msgstr ""
+
+#: fietsboek/templates/resend_verification.jinja2:6
+msgid "page.resend_verification.info"
+msgstr ""
+
+#: fietsboek/templates/resend_verification.jinja2:12
+msgid "page.resend_verification.email"
+msgstr ""
+
+#: fietsboek/templates/resend_verification.jinja2:17
+msgid "page.resend_verification.request"
+msgstr ""
+
 #: fietsboek/templates/upload.jinja2:9
 msgid "page.upload.form.gpx"
 msgstr ""
@@ -738,23 +766,15 @@ msgstr ""
 msgid "transformers.fix-elevation-jumps.description"
 msgstr ""
 
-#: fietsboek/views/account.py:54
+#: fietsboek/views/account.py:53
 msgid "flash.invalid_name"
 msgstr ""
 
-#: fietsboek/views/account.py:59
+#: fietsboek/views/account.py:58
 msgid "flash.invalid_email"
 msgstr ""
 
-#: fietsboek/views/account.py:72
-msgid "email.verify_mail.subject"
-msgstr ""
-
-#: fietsboek/views/account.py:75
-msgid "email.verify.text"
-msgstr ""
-
-#: fietsboek/views/account.py:86
+#: fietsboek/views/account.py:67
 msgid "flash.a_confirmation_link_has_been_sent"
 msgstr ""
 
@@ -802,15 +822,23 @@ msgstr ""
 msgid "page.password_reset.email.body"
 msgstr ""
 
-#: fietsboek/views/default.py:214
+#: fietsboek/views/default.py:224
+msgid "flash.resend_verification_email_fail"
+msgstr ""
+
+#: fietsboek/views/default.py:229
+msgid "flash.verification_token_generated"
+msgstr ""
+
+#: fietsboek/views/default.py:249
 msgid "flash.token_expired"
 msgstr ""
 
-#: fietsboek/views/default.py:220
+#: fietsboek/views/default.py:255
 msgid "flash.email_verified"
 msgstr ""
 
-#: fietsboek/views/default.py:234
+#: fietsboek/views/default.py:269
 msgid "flash.password_updated"
 msgstr ""
 
-- 
cgit v1.2.3