From 391ede9ea062a3935092b2aa7ffdf3e1ee759180 Mon Sep 17 00:00:00 2001 From: Michael Tremer Date: Thu, 31 Oct 2019 17:23:47 +0000 Subject: [PATCH] people: Implement password reset Signed-off-by: Michael Tremer --- Makefile.am | 1 + src/backend/accounts.py | 15 +++++++++++- src/templates/auth/password-reset.html | 21 ++++++++++++++++ src/web/__init__.py | 3 ++- src/web/auth.py | 34 ++++++++++++++++++++++++-- 5 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 src/templates/auth/password-reset.html diff --git a/Makefile.am b/Makefile.am index 6d4c8e5f..08ed643b 100644 --- a/Makefile.am +++ b/Makefile.am @@ -113,6 +113,7 @@ templates_auth_DATA = \ src/templates/auth/activate.html \ src/templates/auth/activated.html \ src/templates/auth/login.html \ + src/templates/auth/password-reset.html \ src/templates/auth/password-reset-initiation.html \ src/templates/auth/password-reset-successful.html \ src/templates/auth/register.html \ diff --git a/src/backend/accounts.py b/src/backend/accounts.py index 341709e2..921d38cf 100644 --- a/src/backend/accounts.py +++ b/src/backend/accounts.py @@ -603,7 +603,7 @@ class Account(Object): self.first_name, self.last_name, )) - def reset_password(self, address=None): + def request_password_reset(self, address=None): reset_code = util.random_string(64) self.db.execute("INSERT INTO account_password_resets(uid, reset_code, address) \ @@ -613,6 +613,19 @@ class Account(Object): self.backend.messages.send_template("auth/messages/password-reset", recipients=[self.email], priority=100, account=self, reset_code=reset_code) + def reset_password(self, reset_code, new_password): + # Delete the reset token + res = self.db.query("DELETE FROM account_password_resets \ + WHERE uid = %s AND reset_code = %s AND expires_at >= NOW() \ + RETURNING *", self.uid, reset_code) + + # The reset code was invalid + if not res: + raise ValueError("Invalid password reset token for %s: %s" % (self, reset_code)) + + # Perform password change + return self.passwd(new_password) + def is_admin(self): return self.is_member_of_group("sudo") diff --git a/src/templates/auth/password-reset.html b/src/templates/auth/password-reset.html new file mode 100644 index 00000000..3a26a170 --- /dev/null +++ b/src/templates/auth/password-reset.html @@ -0,0 +1,21 @@ +{% extends "../base.html" %} + +{% block title %}{{ _("Password Reset") }}{% end block %} + +{% block content %} +
+
+
{{ _("Reset Your Password") }}
+ +
+ {% raw xsrf_form_html() %} + + {% module Password(account) %} + + +
+
+
+{% end block %} diff --git a/src/web/__init__.py b/src/web/__init__.py index 4acb0dfc..76d23edd 100644 --- a/src/web/__init__.py +++ b/src/web/__init__.py @@ -288,7 +288,8 @@ class Application(tornado.web.Application): (r"/sso/discourse", people.SSODiscourse), # Password Reset - (r"/password-reset", auth.PasswordResetInitiationHandler), + (r"/password\-reset", auth.PasswordResetInitiationHandler), + (r"/password\-reset/([a-z_][a-z0-9_-]{0,31})/(\w+)", auth.PasswordResetHandler), # API (r"/api/check/uid", auth.APICheckUID), diff --git a/src/web/auth.py b/src/web/auth.py index e3fb5f13..4bd563ff 100644 --- a/src/web/auth.py +++ b/src/web/auth.py @@ -147,7 +147,7 @@ class ActivateHandler(AuthenticationMixin, base.BaseHandler): self.render("auth/activated.html", account=account) -class PasswordResetInitiationHandler(AuthenticationMixin, base.BaseHandler): +class PasswordResetInitiationHandler(base.BaseHandler): def get(self): username = self.get_argument("username", None) @@ -161,11 +161,41 @@ class PasswordResetInitiationHandler(AuthenticationMixin, base.BaseHandler): account = self.backend.accounts.get_by_uid(username) if account: with self.db.transaction(): - account.reset_password() + account.request_password_reset() self.render("auth/password-reset-successful.html") +class PasswordResetHandler(AuthenticationMixin, base.BaseHandler): + def get(self, uid, reset_code): + account = self.backend.accounts.get_by_uid(uid) + if not account: + raise tornado.web.HTTPError(404, "Could not find account: %s" % uid) + + self.render("auth/password-reset.html", account=account) + + def post(self, uid, reset_code): + account = self.backend.accounts.get_by_uid(uid) + if not account: + raise tornado.web.HTTPError(404, "Could not find account: %s" % uid) + + password1 = self.get_argument("password1") + password2 = self.get_argument("password2") + + if not password1 == password2: + raise tornado.web.HTTPError(400, "Passwords do not match") + + # Try to perform password reset + with self.db.transaction(): + account.reset_password(reset_code, password1) + + # Login the user straight away after reset was successful + self.login(account) + + # Redirect back to / + self.redirect("/") + + class APICheckUID(base.APIHandler): @base.ratelimit(minutes=10, requests=100) def get(self): -- 2.47.2