diff --git a/hiboo/account/login.py b/hiboo/account/login.py index bc54424224193ffd3d88b5eb4bb53c31c41834cb..0b40e7d0e4b7ae31844aa94da800f2d066197919 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 0000000000000000000000000000000000000000..04e69889d8b3478aa819be0b49a2c078b574645c --- /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 85127d356e0df943080b7e5e449769e2df58b8fe..d174f0ddda910bbb2c4765468c12fe328a8840d7 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 45d760b67034368c27c2df3ab2238f694007ccdd..dc159153b6eb8f5153e5c1c02283f30faab4b552 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 7b45330a80e705ad07b10bfc02ba293fbad8787d..7a814435e2a8578f03e7f8f74fe18185a097691c 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 0000000000000000000000000000000000000000..e1bbc651550d3c27c16a8d2391eabd657ba3523e --- /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')