From da0dd066d3ca6bcbf38d946a54c2867dd9ed3bc5 Mon Sep 17 00:00:00 2001 From: Drew DeVault Date: Sun, 6 Sep 2015 17:35:30 -0400 Subject: [PATCH] Implement password reset --- emails/reset-password | 13 ++++++++++ fosspay/blueprints/html.py | 52 +++++++++++++++++++++++++------------- fosspay/email.py | 20 +++++++++++++++ templates/reset.html | 43 +++++++++++++++++++++++++++++++ 4 files changed, 111 insertions(+), 17 deletions(-) create mode 100644 emails/reset-password create mode 100644 templates/reset.html diff --git a/emails/reset-password b/emails/reset-password new file mode 100644 index 0000000..9d1d760 --- /dev/null +++ b/emails/reset-password @@ -0,0 +1,13 @@ +Someone, probably you, wants to reset your donor password. + +To proceed, click this link: + +{{root}}/password-reset/{{user.password_reset}} + +This link expires in 24 hours. If you don't want to change your password or you +weren't expecting this email, just ignore it. + +If you have questions, send an email to {{your_email}}. + +-- +{{your_name}} diff --git a/fosspay/blueprints/html.py b/fosspay/blueprints/html.py index 572c3cb..4ff7727 100644 --- a/fosspay/blueprints/html.py +++ b/fosspay/blueprints/html.py @@ -5,7 +5,7 @@ from fosspay.objects import * from fosspay.database import db from fosspay.common import * from fosspay.config import _cfg, load_config -from fosspay.email import send_thank_you +from fosspay.email import send_thank_you, send_password_reset import os import locale @@ -40,13 +40,13 @@ def setup(): email = request.form.get("email") password = request.form.get("password") if not email or not password: - return redirect("/") # TODO: Tell them what they did wrong (i.e. being stupid) + return redirect("..") # TODO: Tell them what they did wrong (i.e. being stupid) user = User(email, password) user.admin = True db.add(user) db.commit() login_user(user) - return redirect("/admin?first-run=1") + return redirect("admin?first-run=1") @html.route("/admin") @adminrequired @@ -72,14 +72,14 @@ def create_project(): project = Project(name) db.add(project) db.commit() - return redirect("/admin") + return redirect("admin") @html.route("/login", methods=["GET", "POST"]) def login(): if current_user: if current_user.admin: - return redirect("/admin") - return redirect("/panel") + return redirect("admin") + return redirect("panel") if request.method == "GET": return render_template("login.html") email = request.form.get("email") @@ -93,14 +93,14 @@ def login(): return render_template("login.html", errors=True) login_user(user) if user.admin: - return redirect("/admin") - return redirect("/panel") + return redirect("admin") + return redirect("panel") @html.route("/logout") @loginrequired def logout(): logout_user() - return redirect("/") + return redirect("..") @html.route("/donate", methods=["POST"]) @json_output @@ -171,23 +171,41 @@ def donate(): else: return { "success": True, "new_account": new_account } +def issue_password_reset(email): + user = User.query.filter(User.email == email).first() + if not user: + return render_template("reset.html", errors="No one with that email found.") + user.password_reset = binascii.b2a_hex(os.urandom(20)).decode("utf-8") + user.password_reset_expires = datetime.now() + timedelta(days=1) + send_password_reset(user) + db.commit() + return render_template("reset.html", done=True) + @html.route("/password-reset", methods=['GET', 'POST'], defaults={'token': None}) @html.route("/password-reset/", methods=['GET', 'POST']) def reset_password(token): - if not token and request.method == "POST": + if request.method == "GET" and not token: + return render_template("reset.html") + + if request.method == "POST": token = request.form.get("token") + email = request.form.get("email") + + if email: + return issue_password_reset(email) + if not token: - redirect("/") - else: - redirect("/") + return redirect("..") + user = User.query.filter(User.password_reset == token).first() if not user: - redirect("/") + return render_template("reset.html", errors="This link has expired.") + if request.method == 'GET': if user.password_reset_expires == None or user.password_reset_expires < datetime.now(): - return render_template("reset.html", expired=True) + return render_template("reset.html", errors="This link has expired.") if user.password_reset != token: - redirect("/") + redirect("..") return render_template("reset.html", token=token) else: if user.password_reset_expires == None or user.password_reset_expires < datetime.now(): @@ -202,7 +220,7 @@ def reset_password(token): user.password_reset_expires = None db.commit() login_user(user) - return redirect("/panel") + return redirect("panel") @html.route("/panel") @loginrequired diff --git a/fosspay/email.py b/fosspay/email.py index 0d8ab73..561d61c 100644 --- a/fosspay/email.py +++ b/fosspay/email.py @@ -31,3 +31,23 @@ def send_thank_you(user, amount, monthly): message['To'] = user.email smtp.sendmail(_cfg("smtp-from"), [ user.email ], message.as_string()) smtp.quit() + +def send_password_reset(user): + if _cfg("smtp-host") == "": + return + smtp = smtplib.SMTP(_cfg("smtp-host"), _cfgi("smtp-port")) + smtp.login(_cfg("smtp-user"), _cfg("smtp-password")) + with open("emails/reset-password") as f: + message = MIMEText(html.parser.HTMLParser().unescape(\ + pystache.render(f.read(), { + "user": user, + "root": _cfg("protocol") + "://" + _cfg("domain"), + "your_name": _cfg("your-name"), + "your_email": _cfg("your-email") + }))) + message['X-MC-PreserveRecipients'] = "false" + message['Subject'] = "Reset your donor password" + message['From'] = _cfg("smtp-from") + message['To'] = user.email + smtp.sendmail(_cfg("smtp-from"), [ user.email ], message.as_string()) + smtp.quit() diff --git a/templates/reset.html b/templates/reset.html new file mode 100644 index 0000000..8c95523 --- /dev/null +++ b/templates/reset.html @@ -0,0 +1,43 @@ +{% extends "layout.html" %} +{% block body %} +
+
+

Donate to {{ _cfg("your-name") }}

+
+
+
+
+
+

Reset Password

+ {% if errors %} +
+

+ {{ errors }} +

+
+ {% endif %} + {% if done %} +

+ An email should arrive shortly. If you need help, contact + {{_cfg("your-email")}}. +

+ {% elif token %} +
+
+ + +
+ +
+ {% else %} +
+
+ +
+ +
+ {% endif %} +
+
+
+{% endblock %}