Browse Source

Improved the password reset functionality.

sh4nks 11 years ago
parent
commit
e3ed152fc2

+ 1 - 0
.gitignore

@@ -36,3 +36,4 @@ __pycache__
 *.pyc
 *.db
 .venv
+flaskbb/configs/flaskbb_dev.py

+ 19 - 12
flaskbb/auth/forms.py

@@ -9,8 +9,8 @@
     :license: BSD, see LICENSE for more details.
 """
 from flask.ext.wtf import Form
-from wtforms import TextField, PasswordField, BooleanField, ValidationError
-from wtforms.validators import Required, Email, EqualTo, regexp
+from wtforms import TextField, PasswordField, BooleanField, HiddenField
+from wtforms.validators import Required, Email, EqualTo, regexp, ValidationError
 
 from flaskbb.user.models import User
 
@@ -68,20 +68,27 @@ class ReauthForm(Form):
     password = PasswordField('Password', [Required()])
 
 
+class ForgotPasswordForm(Form):
+    email = TextField('Email', validators = [
+        Required(message="Email reguired"),
+        Email()])
+
+
 class ResetPasswordForm(Form):
-    email = TextField("E-Mail", validators=[
-        Required(message="Email required"),
-        Email(message="This email is invalid")])
+    token = HiddenField('Token')
 
-    username = TextField("Username", validators=[
-        Required(message="Username required")])
+    email = TextField('Email', validators = [
+        Required(),
+        Email()])
 
-    def validate_username(self, field):
-        user = User.query.filter_by(username=field.data).first()
-        if not user:
-            raise ValidationError("Wrong username?")
+    password = PasswordField('Password', validators = [
+        Required()])
+
+    confirm_password = PasswordField('Confirm password', validators = [
+        Required(),
+        EqualTo('password', message = 'Passwords must match')])
 
     def validate_email(self, field):
         email = User.query.filter_by(email=field.data).first()
         if not email:
-            raise ValidationError("Wrong E-Mail?")
+            raise ValidationError("Wrong E-Mail.")

+ 46 - 19
flaskbb/auth/views.py

@@ -9,16 +9,13 @@
     :copyright: (c) 2013 by the FlaskBB Team.
     :license: BSD, see LICENSE for more details.
 """
-from werkzeug import generate_password_hash
 from flask import (Blueprint, flash, redirect, render_template,
                    url_for, request)
 from flask.ext.login import (current_user, login_user, login_required,
                              logout_user, confirm_login, login_fresh)
-from flaskbb.extensions import db
-from flaskbb.utils import generate_random_pass
-from flaskbb.email import send_new_password
+from flaskbb.email import send_reset_token
 from flaskbb.auth.forms import (LoginForm, ReauthForm, RegisterForm,
-                                ResetPasswordForm)
+                                ForgotPasswordForm, ResetPasswordForm)
 from flaskbb.user.models import User
 
 auth = Blueprint("auth", __name__)
@@ -93,28 +90,58 @@ def register():
     return render_template("auth/register.html", form=form)
 
 
-@auth.route("/resetpassword", methods=["GET", "POST"])
-def reset_password():
+@auth.route('/resetpassword', methods=["GET", "POST"])
+def forgot_password():
     """
-    Resets the password from a user
+    Sends a reset password token to the user.
     """
 
-    form = ResetPasswordForm(request.form)
-    if form.validate_on_submit():
-        user1 = User.query.filter_by(email=form.email.data).first()
-        user2 = User.query.filter_by(username=form.username.data).first()
+    if not current_user.is_anonymous():
+        return redirect(url_for("forum.index"))
 
-        if user1.email == user2.email:
-            password = generate_random_pass()
-            user1.password = generate_password_hash(password)
-            db.session.commit()
+    form = ForgotPasswordForm()
+    if form.validate_on_submit():
+        user = User.query.filter_by(email=form.email.data).first()
 
-            send_new_password(user1, password)
+        if user:
+            token = user.make_reset_token()
+            send_reset_token(user, token=token)
 
             flash(("E-Mail sent! Please check your inbox."), "info")
-            return redirect(url_for("auth.login"))
+            return redirect(url_for("auth.forgot_password"))
         else:
             flash(("You have entered an username or email that is not linked \
-                with your account"), "danger")
+                with your account"), "error")
+    return render_template("auth/forgot_password.html", form=form)
+
+
+@auth.route("/resetpassword/<token>", methods=["GET", "POST"])
+def reset_password(token):
+    """
+    Handles the reset password process.
+    """
+
+    if not current_user.is_anonymous():
+        return redirect(url_for("forum.index"))
+
+    form = ResetPasswordForm()
+    if form.validate_on_submit():
+        user = User.query.filter_by(email=form.email.data).first()
+        expired, invalid, data = user.verify_reset_token(form.token.data)
+
+        if invalid:
+            flash(("Your password token is invalid."), "error")
+            return redirect(url_for("auth.forgot_password"))
+
+        if expired:
+            flash(("Your password is expired."), "error")
+            return redirect(url_for("auth.forgot_password"))
+
+        if user and data:
+            user.password = form.password.data
+            user.save()
+            flash(("Your password has been updated."), "success")
+            return redirect(url_for("auth.login"))
 
+    form.token.data = token
     return render_template("auth/reset_password.html", form=form)

+ 2 - 10
flaskbb/configs/base.py

@@ -45,8 +45,8 @@ class BaseConfig(object):
     SQLALCHEMY_ECHO = True
 
     # Protection against form post fraud
-    CSRF_ENABLED = True
-    CSRF_SESSION_KEY = "reallyhardtoguess"
+    WTF_CSRF_ENABLED = True
+    WTF_CSRF_SESSION_KEY = "reallyhardtoguess"
 
     # Auth
     LOGIN_VIEW = "auth.login"
@@ -59,14 +59,6 @@ class BaseConfig(object):
     # Recaptcha
     RECAPTCHA_ENABLE = False
 
-    # Mail
-    MAIL_SERVER = "localhost"
-    MAIL_PORT = 25
-    MAIL_USERNAME = "FlaskBB Service"
-    MAIL_PASSWORD = ""
-    DEFAULT_MAIL_SENDER = "noreply@flaskbb.org"
-    ADMINS = ["admin@flaskbb.org"]
-
     # Pagination
     POSTS_PER_PAGE = 10
     TOPICS_PER_PAGE = 10

+ 4 - 4
flaskbb/email.py

@@ -14,19 +14,19 @@ from flaskbb.extensions import mail
 from flaskbb.decorators import async
 
 
-def send_new_password(user, pw):
+def send_reset_token(user, token):
     send_email(
         subject="Password Reset",
         recipients=[user.email],
         text_body=render_template(
             "email/reset_password.txt",
             user=user,
-            password=pw
+            token=token
         ),
         html_body=render_template(
             "email/reset_password.html",
             user=user,
-            password=pw
+            token=token
         )
     )
 
@@ -44,4 +44,4 @@ def send_email(subject, recipients, text_body, html_body, sender=""):
 
     msg.body = text_body
     msg.html = html_body
-    send_async_email(msg)
+    mail.send(msg)

+ 20 - 0
flaskbb/templates/auth/forgot_password.html

@@ -0,0 +1,20 @@
+{% set page_title = "Reset Password" %}
+
+{% extends "layout.html" %}
+{% block content %}
+{% import "macros.html" as wtf %}
+
+<form class="form-horizontal" role="form" method="POST">
+    <h2>Reset Password</h2>
+    <hr>
+        {{ form.hidden_tag() }}
+        {{ wtf.horizontal_field(form.email) }}
+
+    <div class="form-group">
+        <div class="col-lg-offset-2 col-lg-10">
+            <button type="submit" class="btn btn-primary">Reset Password</button>
+        </div>
+    </div>
+</form>
+
+{% endblock %}

+ 4 - 2
flaskbb/templates/auth/reset_password.html

@@ -8,8 +8,10 @@
     <h2>Reset Password</h2>
     <hr>
         {{ form.hidden_tag() }}
-        {{ wtf.horizontal_field(form.username)}}
-        {{ wtf.horizontal_field(form.email)}}
+        {{ form.token }}
+        {{ wtf.horizontal_field(form.email) }}
+        {{ wtf.horizontal_field(form.password) }}
+        {{ wtf.horizontal_field(form.confirm_password)}}
 
     <div class="form-group">
         <div class="col-lg-offset-2 col-lg-10">

+ 5 - 0
flaskbb/templates/email/reset_password.html

@@ -0,0 +1,5 @@
+<p>Dear {{ user.username }},</p>
+<p>To reset your password click on the following link:</p>
+<p><a href="{{ url_for('auth.reset_password', token=token, _external=True) }}">{{ url_for('auth.reset_password', token=token, _external=True) }}</a></p>
+<p>Sincerely,</p>
+<p>The Administration</p>

+ 9 - 0
flaskbb/templates/email/reset_password.txt

@@ -0,0 +1,9 @@
+Dear {{ user.username }},
+
+To reset your password click on the following link:
+
+{{ url_for('auth.reset_password', token=token, _external=True) }}
+
+Sincerely,
+
+The Administration

+ 1 - 1
flaskbb/templates/layout.html

@@ -73,7 +73,7 @@
                             </button>
                             <ul class="dropdown-menu" role="menu">
                                 <li><a href="{{ url_for('auth.register') }}">Register</a></li>
-                                <li><a href="{{ url_for('auth.reset_password') }}">Reset Password</a></li>
+                                <li><a href="{{ url_for('auth.forgot_password') }}">Reset Password</a></li>
                             </ul>
                         </div>
                     {% endif %}

+ 31 - 1
flaskbb/user/models.py

@@ -9,6 +9,9 @@
     :license: BSD, see LICENSE for more details.
 """
 from datetime import datetime
+
+from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
+from itsdangerous import SignatureExpired, BadSignature
 from werkzeug import generate_password_hash, check_password_hash
 from flask import current_app
 from flask.ext.login import UserMixin
@@ -32,7 +35,7 @@ class User(db.Model, UserMixin):
     website = db.Column(db.String)
     location = db.Column(db.String)
     signature = db.Column(db.String)
-    avatar = db.Column(db.String, default="/static/uploads/avatar/default.png")
+    avatar = db.Column(db.String)
     notes = db.Column(db.Text(5000), default="User has not added any notes about him.")
 
     posts = db.relationship("Post", backref="user", lazy="dynamic")
@@ -77,6 +80,33 @@ class User(db.Model, UserMixin):
             authenticated = False
         return user, authenticated
 
+    def _make_token(self, data, timeout):
+        s = Serializer(current_app.config['SECRET_KEY'], timeout)
+        return s.dumps(data)
+
+    def _verify_token(self, token):
+        s = Serializer(current_app.config['SECRET_KEY'])
+        data = None
+        expired, invalid = False, False
+        try:
+            data = s.loads(token)
+        except SignatureExpired:
+            expired = True
+        except Exception:
+            invalid = True
+        return expired, invalid, data
+
+    def make_reset_token(self, expiration=3600):
+        return self._make_token({'id': self.id, 'op': 'reset'}, expiration)
+
+    def verify_reset_token(self, token):
+        expired, invalid, data = self._verify_token(token)
+        if data and data.get('id') == self.id and data.get('op') == 'reset':
+            data = True
+        else:
+            data = False
+        return expired, invalid, data
+
     @property
     def last_post(self):
         """

+ 3 - 0
requirements.txt

@@ -6,3 +6,6 @@ Flask-Script
 Flask-Mail
 Flask-Cache
 Flask-Themes2
+Flask-Debugtoolbar
+itsdangerous
+passlib