From 0a9a57063d20223b4d3060f1b32c2513a7c27228 Mon Sep 17 00:00:00 2001 From: kaiyou <pierre@jaury.eu> Date: Sun, 2 Aug 2020 14:55:38 +0200 Subject: [PATCH] Implement admin-driven password reset --- hiboo/account/login.py | 22 +++++++++++++ hiboo/account/templates/account_reset.html | 3 ++ hiboo/models.py | 9 ++++++ hiboo/user/templates/user_details.html | 4 +++ hiboo/user/users.py | 16 ++++++++++ .../665cdee2f311_add_password_reset.py | 32 +++++++++++++++++++ 6 files changed, 86 insertions(+) create mode 100644 hiboo/account/templates/account_reset.html create mode 100644 migrations/versions/665cdee2f311_add_password_reset.py diff --git a/hiboo/account/login.py b/hiboo/account/login.py index bc54424..0b40e7d 100644 --- a/hiboo/account/login.py +++ b/hiboo/account/login.py @@ -2,6 +2,7 @@ from hiboo import models, utils, security from hiboo.account import blueprint, forms from flask_babel import lazy_gettext as _ +import datetime import flask_login import flask @@ -49,3 +50,24 @@ def signup(): flask_login.login_user(user) return flask.redirect(utils.url_or_intent(".home")) return flask.render_template("account_signup.html", form=form) + + +@blueprint.route("/reset/<token_uuid>", methods=["GET", "POST"]) +def reset(token_uuid): + token = models.ResetToken.query.get(token_uuid) or flask.abort(403) + if token.updated_at is not None or token.expired_at < datetime.datetime.now(): + flask.flash(_("Invalid or expired reset link"), "danger") + return flask.redirect(flask.url_for(".signin")) + form = forms.PasswordForm() + del form.old + if form.validate_on_submit(): + token.expired_at = datetime.datetime.now() + models.db.session.add(token) + auth = token.user.auths[0] + auth.set_password(form.password.data) + models.log(models.History.PASSWORD, user=token.user) + models.db.session.add(auth) + models.db.session.commit() + flask.flash(_("Successfully reset your password"), "success") + return flask.redirect(flask.url_for(".signin")) + return flask.render_template("account_reset.html", form=form) \ No newline at end of file diff --git a/hiboo/account/templates/account_reset.html b/hiboo/account/templates/account_reset.html new file mode 100644 index 0000000..04e6988 --- /dev/null +++ b/hiboo/account/templates/account_reset.html @@ -0,0 +1,3 @@ +{% extends "form.html" %} + +{% block title %}{% trans %}Reset your password{% endtrans %}{% endblock %} diff --git a/hiboo/models.py b/hiboo/models.py index 85127d3..d174f0d 100644 --- a/hiboo/models.py +++ b/hiboo/models.py @@ -221,6 +221,15 @@ class ClaimName(db.Model): username = db.Column(db.String(255), nullable=False) +class ResetToken(db.Model): + """ A reset token is used to reset authentication for a given user. + """ + __tablename__ = "resettoken" + + user_uuid = db.Column(db.String(36), db.ForeignKey(User.uuid)) + user = db.relationship(User) + expired_at = db.Column(db.DateTime, nullable=False) + class History(db.Model): """ Records an even in an account's or profile's lifetime. diff --git a/hiboo/user/templates/user_details.html b/hiboo/user/templates/user_details.html index 45d760b..dc15915 100644 --- a/hiboo/user/templates/user_details.html +++ b/hiboo/user/templates/user_details.html @@ -56,3 +56,7 @@ </div> </div> {% endblock %} + +{% block actions %} +<a href="{{ url_for(".password_reset", user_uuid=user.uuid) }}" class="btn btn-primary">{% trans %}Password reset{% endtrans %}</a> +{% endblock %} diff --git a/hiboo/user/users.py b/hiboo/user/users.py index 7b45330..7a81443 100644 --- a/hiboo/user/users.py +++ b/hiboo/user/users.py @@ -1,6 +1,8 @@ from hiboo.user import blueprint, forms from hiboo import models, utils, security +from flask_babel import lazy_gettext as _ +import datetime import flask @@ -24,3 +26,17 @@ def list(): def details(user_uuid): user = models.User.query.get(user_uuid) or flask.abort(404) return flask.render_template("user_details.html", user=user) + + +@blueprint.route("/reset/<user_uuid>", methods=["GET", "POST"]) +@security.admin_required() +@security.confirmation_required("generate a password reset link") +def password_reset(user_uuid): + user = models.User.query.get(user_uuid) or flask.abort(404) + expired = datetime.datetime.now() + datetime.timedelta(days=1) + token = models.ResetToken(user=user, expired_at=expired) + models.db.session.add(token) + models.db.session.commit() + reset_link = flask.url_for("account.reset", token_uuid=token.uuid, _external=True) + flask.flash(_("Reset link: {}").format(reset_link), "success") + return flask.redirect(flask.url_for(".details", user_uuid=user.uuid)) \ No newline at end of file diff --git a/migrations/versions/665cdee2f311_add_password_reset.py b/migrations/versions/665cdee2f311_add_password_reset.py new file mode 100644 index 0000000..e1bbc65 --- /dev/null +++ b/migrations/versions/665cdee2f311_add_password_reset.py @@ -0,0 +1,32 @@ +""" Add reset tokens + +Revision ID: 665cdee2f311 +Revises: 29a70a960a97 +Create Date: 2020-08-02 14:34:56.262198 +""" + +from alembic import op +import sqlalchemy as sa + + +revision = '665cdee2f311' +down_revision = '29a70a960a97' +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table('resettoken', + sa.Column('user_uuid', sa.String(length=36), nullable=True), + sa.Column('expired_at', sa.DateTime(), nullable=False), + sa.Column('uuid', sa.String(length=36), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.Column('comment', sa.String(length=255), nullable=True), + sa.ForeignKeyConstraint(['user_uuid'], ['user.uuid'], ), + sa.PrimaryKeyConstraint('uuid') + ) + + +def downgrade(): + op.drop_table('resettoken') -- GitLab