From a51da4c06db5f0f0222486998ca7eb2974a2841c Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Mon, 22 May 2023 22:41:52 +0200 Subject: make tokens expire after 24 hours --- fietsboek/locale/de/LC_MESSAGES/messages.po | 28 ++++++++++--------- fietsboek/locale/en/LC_MESSAGES/messages.po | 28 ++++++++++--------- fietsboek/locale/fietslog.pot | 28 ++++++++++--------- fietsboek/models/user.py | 7 +++++ fietsboek/scripts/fietscron.py | 12 +++++++++ fietsboek/views/default.py | 42 +++++++++++------------------ 6 files changed, 83 insertions(+), 62 deletions(-) diff --git a/fietsboek/locale/de/LC_MESSAGES/messages.po b/fietsboek/locale/de/LC_MESSAGES/messages.po index 5b6c654..f5528c9 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-15 20:20+0200\n" +"POT-Creation-Date: 2023-05-22 22:40+0200\n" "PO-Revision-Date: 2022-07-02 17:35+0200\n" "Last-Translator: FULL NAME \n" "Language: de\n" @@ -788,35 +788,35 @@ msgstr "Wappen bearbeitet" msgid "flash.badge_deleted" msgstr "Wappen gelöscht" -#: fietsboek/views/default.py:121 +#: fietsboek/views/default.py:115 msgid "flash.invalid_credentials" msgstr "Ungültige Nutzerdaten" -#: fietsboek/views/default.py:125 +#: fietsboek/views/default.py:119 msgid "flash.account_not_verified" msgstr "Konto noch nicht bestätigt" -#: fietsboek/views/default.py:128 +#: fietsboek/views/default.py:122 msgid "flash.logged_in" msgstr "Du bist nun angemeldet" -#: fietsboek/views/default.py:150 +#: fietsboek/views/default.py:142 msgid "flash.logged_out" msgstr "Du bist nun abgemeldet" -#: fietsboek/views/default.py:184 +#: fietsboek/views/default.py:172 msgid "flash.reset_invalid_email" msgstr "Ungültige E-Mail-Adresse angegeben" -#: fietsboek/views/default.py:189 +#: fietsboek/views/default.py:177 msgid "flash.password_token_generated" msgstr "Ein Link zum Zurücksetzen des Passworts wurde versandt" -#: fietsboek/views/default.py:194 +#: fietsboek/views/default.py:182 msgid "page.password_reset.email.subject" msgstr "Fietsboek Passwortzurücksetzung" -#: fietsboek/views/default.py:197 +#: fietsboek/views/default.py:185 msgid "page.password_reset.email.body" msgstr "" "Du kannst Dein Fietsboek-Passwort hier zurücksetzen: {}\n" @@ -824,15 +824,19 @@ msgstr "" "Falls Du keine Passwortzurücksetzung beantragt hast, dann ignoriere diese" " E-Mail." -#: fietsboek/views/default.py:230 +#: fietsboek/views/default.py:214 +msgid "flash.token_expired" +msgstr "Der Link ist nicht mehr gültig" + +#: fietsboek/views/default.py:220 msgid "flash.email_verified" msgstr "E-Mail-Adresse bestätigt" -#: fietsboek/views/default.py:244 +#: fietsboek/views/default.py:234 msgid "flash.password_updated" msgstr "Passwort aktualisiert" -#: fietsboek/views/detail.py:140 +#: fietsboek/views/detail.py:155 msgid "flash.track_deleted" msgstr "Strecke gelöscht" diff --git a/fietsboek/locale/en/LC_MESSAGES/messages.po b/fietsboek/locale/en/LC_MESSAGES/messages.po index d1a8e8d..2ec7b76 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-15 20:20+0200\n" +"POT-Creation-Date: 2023-05-22 22:40+0200\n" "PO-Revision-Date: 2023-04-03 20:42+0200\n" "Last-Translator: \n" "Language: en\n" @@ -778,50 +778,54 @@ msgstr "Badge has been modified" msgid "flash.badge_deleted" msgstr "Badge has been deleted" -#: fietsboek/views/default.py:121 +#: fietsboek/views/default.py:115 msgid "flash.invalid_credentials" msgstr "Invalid login credentials" -#: fietsboek/views/default.py:125 +#: fietsboek/views/default.py:119 msgid "flash.account_not_verified" msgstr "Your account is not verified yet" -#: fietsboek/views/default.py:128 +#: fietsboek/views/default.py:122 msgid "flash.logged_in" msgstr "You are now logged in" -#: fietsboek/views/default.py:150 +#: fietsboek/views/default.py:142 msgid "flash.logged_out" msgstr "You have been logged out" -#: fietsboek/views/default.py:184 +#: fietsboek/views/default.py:172 msgid "flash.reset_invalid_email" msgstr "Invalid email address provided" -#: fietsboek/views/default.py:189 +#: fietsboek/views/default.py:177 msgid "flash.password_token_generated" msgstr "A password reset email has been sent" -#: fietsboek/views/default.py:194 +#: fietsboek/views/default.py:182 msgid "page.password_reset.email.subject" msgstr "Fietsboek Password Reset" -#: fietsboek/views/default.py:197 +#: fietsboek/views/default.py:185 msgid "page.password_reset.email.body" msgstr "" "You can reset your Fietsboek password here: {}\n" "\n" "If you did not request a password reset, ignore this email." -#: fietsboek/views/default.py:230 +#: fietsboek/views/default.py:214 +msgid "flash.token_expired" +msgstr "The link has expired" + +#: fietsboek/views/default.py:220 msgid "flash.email_verified" msgstr "Your email address has been verified" -#: fietsboek/views/default.py:244 +#: fietsboek/views/default.py:234 msgid "flash.password_updated" msgstr "Password has been updated" -#: fietsboek/views/detail.py:140 +#: fietsboek/views/detail.py:155 msgid "flash.track_deleted" msgstr "Track has been deleted" diff --git a/fietsboek/locale/fietslog.pot b/fietsboek/locale/fietslog.pot index 7e3d440..113d2d1 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-15 20:20+0200\n" +"POT-Creation-Date: 2023-05-22 22:40+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -770,47 +770,51 @@ msgstr "" msgid "flash.badge_deleted" msgstr "" -#: fietsboek/views/default.py:121 +#: fietsboek/views/default.py:115 msgid "flash.invalid_credentials" msgstr "" -#: fietsboek/views/default.py:125 +#: fietsboek/views/default.py:119 msgid "flash.account_not_verified" msgstr "" -#: fietsboek/views/default.py:128 +#: fietsboek/views/default.py:122 msgid "flash.logged_in" msgstr "" -#: fietsboek/views/default.py:150 +#: fietsboek/views/default.py:142 msgid "flash.logged_out" msgstr "" -#: fietsboek/views/default.py:184 +#: fietsboek/views/default.py:172 msgid "flash.reset_invalid_email" msgstr "" -#: fietsboek/views/default.py:189 +#: fietsboek/views/default.py:177 msgid "flash.password_token_generated" msgstr "" -#: fietsboek/views/default.py:194 +#: fietsboek/views/default.py:182 msgid "page.password_reset.email.subject" msgstr "" -#: fietsboek/views/default.py:197 +#: fietsboek/views/default.py:185 msgid "page.password_reset.email.body" msgstr "" -#: fietsboek/views/default.py:230 +#: fietsboek/views/default.py:214 +msgid "flash.token_expired" +msgstr "" + +#: fietsboek/views/default.py:220 msgid "flash.email_verified" msgstr "" -#: fietsboek/views/default.py:244 +#: fietsboek/views/default.py:234 msgid "flash.password_updated" msgstr "" -#: fietsboek/views/detail.py:140 +#: fietsboek/views/detail.py:155 msgid "flash.track_deleted" msgstr "" diff --git a/fietsboek/models/user.py b/fietsboek/models/user.py index e1b4841..4201d14 100644 --- a/fietsboek/models/user.py +++ b/fietsboek/models/user.py @@ -53,6 +53,9 @@ SCRYPT_PARAMETERS = { } SALT_LENGTH = 32 +TOKEN_LIFETIME = datetime.timedelta(hours=24) +"""Maximum validity time of a token.""" + friends_assoc = Table( "friends_assoc", @@ -457,5 +460,9 @@ class Token(Base): now = datetime.datetime.utcnow() return cls(user=user, uuid=token_uuid, date=now, token_type=token_type) + def age(self) -> datetime.timedelta: + """Returns the age of the token.""" + return abs(datetime.datetime.utcnow() - self.date) + Index("idx_token_uuid", Token.uuid, unique=True) diff --git a/fietsboek/scripts/fietscron.py b/fietsboek/scripts/fietscron.py index 8116204..417b188 100644 --- a/fietsboek/scripts/fietscron.py +++ b/fietsboek/scripts/fietscron.py @@ -16,6 +16,7 @@ from .. import config as mod_config from .. import hittekaart, models from ..config import Config from ..data import DataManager +from ..models.user import TOKEN_LIFETIME from . import config_option LOGGER = logging.getLogger(__name__) @@ -48,6 +49,7 @@ def cli(config): LOGGER.debug("Starting maintenance tasks") remove_old_uploads(engine) + remove_old_tokens(engine) rebuild_cache(engine, data_manager) if config.hittekaart_autogenerate: @@ -65,6 +67,16 @@ def remove_old_uploads(engine: Engine): session.commit() +def remove_old_tokens(engine: Engine): + """Removes old tokens from the database.""" + LOGGER.info("Deleting old tokens") + limit = datetime.datetime.utcnow() - TOKEN_LIFETIME + session = Session(engine) + stmt = delete(models.Token).where(models.Token.date < limit) + session.execute(stmt) + session.commit() + + def rebuild_cache(engine: Engine, data_manager: DataManager): """Rebuilds the cache entries that are currently missing.""" LOGGER.debug("Rebuilding caches") diff --git a/fietsboek/views/default.py b/fietsboek/views/default.py index 08f32ce..b8b1835 100644 --- a/fietsboek/views/default.py +++ b/fietsboek/views/default.py @@ -4,6 +4,8 @@ from pyramid.httpexceptions import HTTPFound, HTTPNotFound from pyramid.i18n import TranslationString as _ from pyramid.interfaces import ISecurityPolicy from pyramid.renderers import render_to_response +from pyramid.request import Request +from pyramid.response import Response from pyramid.security import forget, remember from pyramid.view import view_config from sqlalchemy import select @@ -12,17 +14,15 @@ from sqlalchemy.orm import aliased from .. import email, models, summaries, util from ..models.track import TrackType, TrackWithMetadata -from ..models.user import PasswordMismatch, TokenType +from ..models.user import TOKEN_LIFETIME, PasswordMismatch, TokenType @view_config(route_name="home", renderer="fietsboek:templates/home.jinja2") -def home(request): +def home(request: Request) -> Response: """Renders the home page. :param request: The Pyramid request. - :type request: pyramid.request.Request :return: The HTTP response. - :rtype: pyramid.response.Response """ if not request.identity: # See if the admin set a custom home page @@ -73,13 +73,11 @@ def home(request): @view_config(route_name="static-page", renderer="fietsboek:templates/static-page.jinja2") -def static_page(request): +def static_page(request: Request) -> Response: """Renders a static page. :param request: The Pyramid request. - :type request: pyramid.request.Request :return: The HTTP response. - :rtype: pyramid.response.Response """ page = request.pages.find(request.matchdict["slug"], request) if page is None: @@ -92,26 +90,22 @@ def static_page(request): @view_config(route_name="login", renderer="fietsboek:templates/login.jinja2", request_method="GET") -def login(request): +def login(request: Request) -> Response: """Renders the login page. :param request: The Pyramid request. - :type request: pyramid.request.Request :return: The HTTP response. - :rtype: pyramid.response.Response """ # pylint: disable=unused-argument return {} @view_config(route_name="login", request_method="POST") -def do_login(request): +def do_login(request: Request) -> Response: """Endpoint for the login form. :param request: The Pyramid request. - :type request: pyramid.request.Request :return: The HTTP response. - :rtype: pyramid.response.Response """ query = models.User.query_by_email(request.params["email"]) try: @@ -139,13 +133,11 @@ def do_login(request): @view_config(route_name="logout") -def logout(request): +def logout(request: Request) -> Response: """Logs the user out. :param request: The Pyramid request. - :type request: pyramid.request.Request :return: The HTTP response. - :rtype: pyramid.response.Response """ request.session.flash(request.localizer.translate(_("flash.logged_out"))) headers = forget(request) @@ -157,26 +149,22 @@ def logout(request): request_method="GET", renderer="fietsboek:templates/request_password.jinja2", ) -def password_reset(request): +def password_reset(request: Request) -> Response: """Form to request a new password. :param request: The Pyramid request. - :type request: pyramid.request.Request :return: The HTTP response. - :rtype: pyramid.response.Response """ # pylint: disable=unused-argument return {} @view_config(route_name="password-reset", request_method="POST") -def do_password_reset(request): +def do_password_reset(request: Request) -> Response: """Endpoint for the password request form. :param request: The Pyramid request. - :type request: pyramid.request.Request :return: The HTTP response. - :rtype: pyramid.response.Response """ query = models.User.query_by_email(request.params["email"]) user = request.dbsession.execute(query).scalar_one_or_none() @@ -209,14 +197,12 @@ def do_password_reset(request): @view_config(route_name="use-token") -def use_token(request): +def use_token(request: Request) -> Response: """Endpoint with which a user can use a token for a password reset or email verification. :param request: The Pyramid request. - :type request: pyramid.request.Request :return: The HTTP response. - :rtype: pyramid.response.Response """ token = request.dbsession.execute( select(models.Token).filter_by(uuid=request.matchdict["uuid"]) @@ -224,6 +210,10 @@ def use_token(request): if token is None: return HTTPNotFound() + if token.age() > TOKEN_LIFETIME: + request.session.flash(request.localizer.translate(_("flash.token_expired"))) + return HTTPFound(request.route_url("home")) + if token.token_type == TokenType.VERIFY_EMAIL: token.user.is_verified = True request.dbsession.delete(token) @@ -243,4 +233,4 @@ def use_token(request): request.dbsession.delete(token) request.session.flash(request.localizer.translate(_("flash.password_updated"))) return HTTPFound(request.route_url("login")) - return None + raise NotImplementedError("No matching action found") -- cgit v1.2.3