Просмотр исходного кода

Further work on the activate account functionality

I have renamed this functionality to "Activate Account" which I think
fits much better. In addition to that, I also make use of Flask-Login's
``is_activte`` method. I am not completely sure if I should go this
route like I did by simply adding a check in the User's model to see if
the admin activated this option (activate_account) AND if the user has
activated his account by following the instructions sent to him by email
and thus sets the property ``user.is_activate`` to True. The other
option would be to just force the login (e.q. login_user(force=True))
sh4nks 9 лет назад
Родитель
Сommit
798dd03967

+ 23 - 2
flaskbb/auth/forms.py

@@ -116,5 +116,26 @@ class ResetPasswordForm(Form):
             raise ValidationError(_("Wrong E-Mail Address."))
 
 
-class EmailConfirmationForm(Form):
-    pass
+class RequestActivationForm(Form):
+    username = StringField(_("Username"), validators=[
+        DataRequired(message=_("A Username is required.")),
+        is_username])
+
+    email = StringField(_("E-Mail Address"), validators=[
+        DataRequired(message=_("A E-Mail Address is required.")),
+        Email(message=_("Invalid E-Mail Address."))])
+
+    password = PasswordField(_('Password'), validators=[
+        InputRequired(),
+        EqualTo('confirm_password', message=_('Passwords must match.'))])
+
+    submit = SubmitField(_("Send Confirmation Mail"))
+
+
+class AccountActivationForm(Form):
+    token = StringField(_("E-Mail Confirmation Token"), validators=[
+        DataRequired(message=_("Please enter the token that we have sent to "
+                               "you."))
+    ])
+
+    submit = SubmitField(_("Confirm E-Mail"))

+ 46 - 27
flaskbb/auth/views.py

@@ -21,7 +21,7 @@ from flaskbb.email import send_reset_token
 from flaskbb.exceptions import AuthenticationError, LoginAttemptsExceeded
 from flaskbb.auth.forms import (LoginForm, ReauthForm, ForgotPasswordForm,
                                 ResetPasswordForm, RegisterForm,
-                                EmailConfirmationForm)
+                                AccountActivationForm, RequestActivationForm)
 from flaskbb.user.models import User
 from flaskbb.fixtures.settings import available_languages
 from flaskbb.utils.settings import flaskbb_config
@@ -59,9 +59,7 @@ def login():
 @auth.route("/reauth", methods=["GET", "POST"])
 @login_required
 def reauth():
-    """
-    Reauthenticates a user.
-    """
+    """Reauthenticates a user."""
     if not login_fresh():
         form = ReauthForm(request.form)
         if form.validate_on_submit():
@@ -78,6 +76,7 @@ def reauth():
 @auth.route("/logout")
 @login_required
 def logout():
+    """Logs the user out."""
     logout_user()
     flash(("Logged out"), "success")
     return redirect(url_for("forum.index"))
@@ -85,9 +84,7 @@ def logout():
 
 @auth.route("/register", methods=["GET", "POST"])
 def register():
-    """
-    Register a new user.
-    """
+    """Register a new user."""
     if current_user is not None and current_user.is_authenticated:
         return redirect_or_next(current_user.url)
 
@@ -105,7 +102,7 @@ def register():
         user = form.save()
         login_user(user)
 
-        if flaskbb_config["VERFIY_EMAIL"]:
+        if flaskbb_config["ACTIVATE_ACCOUNT"]:
 
             flash(_("verify your email by blablaabla"))
         else:
@@ -117,10 +114,7 @@ def register():
 
 @auth.route('/reset-password', methods=["GET", "POST"])
 def forgot_password():
-    """
-    Sends a reset password token to the user.
-    """
-
+    """Sends a reset password token to the user."""
     if not current_user.is_anonymous:
         return redirect(url_for("forum.index"))
 
@@ -142,10 +136,7 @@ def forgot_password():
 
 @auth.route("/reset-password/<token>", methods=["GET", "POST"])
 def reset_password(token):
-    """
-    Handles the reset password process.
-    """
-
+    """Handles the reset password process."""
     if not current_user.is_anonymous:
         return redirect(url_for("forum.index"))
 
@@ -172,20 +163,48 @@ def reset_password(token):
     return render_template("auth/reset_password.html", form=form)
 
 
-@auth.route("/confirm-email/<token>", methods=["GET", "POST"])
-def email_confirmation(token=None):
-    """Handles the email verification process."""
-    if current_user.is_authenticated and current_user.confirmed is not None:
+@auth.route("/activate", methods=["GET", "POST"])
+def request_activation_token(token=None):
+    """Requests a new account activation token."""
+    if current_user.is_active or not flaskbb_config["ACTIVATE_ACCOUNT"]:
         return redirect(url_for('forum.index'))
 
-    if token is not None:
-        expired, invalid, user = get_token_status(token, "verify_email")
-        return
+    form = RequestActivationForm()
+    if form.validate_on_submit():
+        # TODO: make sure validate some data (make sure this is the user whose
+        # token expired and/or is invalid).
+        pass
+
+    return render_template("auth/request_account_activation.html", form=form)
+
+
+@auth.route("/activate/<token>", methods=["GET", "POST"])
+def activate_account(token=None):
+    """Handles the account activation process."""
+    if current_user.is_active or not flaskbb_config["ACTIVATE_ACCOUNT"]:
+        return redirect(url_for('forum.index'))
 
     form = None
-    if token is None:
-        form = EmailConfirmationForm()
+    if token is not None:
+        expired, invalid, user = get_token_status(token, "activate_account")
+    else:
+        form = AccountActivationForm()
         if form.validate_on_submit():
-            pass
+            expired, invalid, user = get_token_status(form.token.data,
+                                                      "activate_account")
+
+    if invalid:
+        flash(_("Your account activation token is invalid."), "danger")
+        return redirect(url_for("auth.request_email_confirmation"))
+
+    if expired:
+        flash(_("Your account activation token is expired."), "danger")
+        return redirect(url_for("auth.request_activation_token"))
+
+    if user:
+        user.activated = datetime.utcnow()
+        user.save()
+        flash(_("Your Account has been activated.", "success"))
+        return redirect(url_for("forum.index"))
 
-    return render_template("auth/email_verification.html", form=form)
+    return render_template("auth/account_activation.html", form=form)

+ 3 - 4
flaskbb/email.py

@@ -13,12 +13,11 @@ from flask_mail import Message
 from flask_babelplus import lazy_gettext as _
 
 from flaskbb.extensions import mail
-from flaskbb.utils.tokens import (generate_password_reset_token,
-                                  generate_email_confirmation_token)
+from flaskbb.utils.tokens import make_token
 
 
 def send_reset_token(user):
-    token = generate_password_reset_token(user)
+    token = make_token(user=user, operation="reset_password")
     send_email(
         subject=_("Password Recovery Confirmation"),
         recipients=[user.email],
@@ -36,7 +35,7 @@ def send_reset_token(user):
 
 
 def send_email_confirmation(user):
-    token = generate_email_confirmation_token()
+    token = make_token(user=user, operation="confirm_email")
     send_email(
         subject=_("E-Mail Confirmation"),
         recipients=[user.email],

+ 3 - 3
flaskbb/fixtures/settings.py

@@ -77,11 +77,11 @@ fixture = (
                 'name':         "Enable Registration",
                 'description':  "Enable or disable the registration",
             }),
-            ('verify_email', {
+            ('activate_account', {
                 'value':        True,
                 'value_type':   "boolean",
-                'name':         "Verify E-Mail Address",
-                'description':  "Enable to let the user verify the email address by sending a verification email."
+                'name':         "Enable Account Activation",
+                'description':  "Enable to let the user activate their account by sending a email with an activation link."
             }),
             ('login_attempts', {
                 'value':        5,

+ 8 - 39
flaskbb/user/models.py

@@ -96,6 +96,7 @@ class User(db.Model, UserMixin, CRUDMixin):
 
     last_failed_login = db.Column(db.DateTime)
     login_attempts = db.Column(db.Integer, default=0)
+    activated = db.Column(db.DateTime)
 
     theme = db.Column(db.String(15))
     language = db.Column(db.String(15), default="en")
@@ -127,6 +128,12 @@ class User(db.Model, UserMixin, CRUDMixin):
 
     # Properties
     @property
+    def is_active(self):
+        if flaskbb_config["ACTIVATE_ACCOUNT"] and self.activated is not None:
+            return True
+        return False
+
+    @property
     def last_post(self):
         """Returns the latest post from the user."""
 
@@ -228,7 +235,7 @@ class User(db.Model, UserMixin, CRUDMixin):
         if user:
             # check for the login attempts first
             login_timeout = datetime.utcnow() - timedelta(
-                    minutes=flaskbb_config["LOGIN_TIMEOUT"]
+                minutes=flaskbb_config["LOGIN_TIMEOUT"]
             )
             if user.login_attempts >= flaskbb_config["LOGIN_ATTEMPTS"] and \
                     user.last_failed_login > login_timeout:
@@ -252,44 +259,6 @@ class User(db.Model, UserMixin, CRUDMixin):
 
         raise AuthenticationError
 
-    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):
-        """Creates a reset token. The duration can be configured through the
-        expiration parameter.
-
-        :param expiration: The time in seconds how long the token is valid.
-        """
-        return self._make_token({'id': self.id, 'op': 'reset'}, expiration)
-
-    def verify_reset_token(self, token):
-        """Verifies a reset token. It returns three boolean values based on
-        the state of the token (expired, invalid, data).
-
-        :param token: The reset token that should be checked.
-        """
-
-        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
-
     def recalculate(self):
         """Recalculates the post count from the user."""
         post_count = Post.query.filter_by(user_id=self.id).count()

+ 26 - 0
migrations/versions/0ece7c8bfb07_add_account_activation_timestamp.py

@@ -0,0 +1,26 @@
+"""Add account activation timestamp
+
+Revision ID: 0ece7c8bfb07
+Revises: 10879048c708
+Create Date: 2016-04-25 22:38:44.504224
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '0ece7c8bfb07'
+down_revision = '10879048c708'
+
+from alembic import op
+import sqlalchemy as sa
+
+
+def upgrade():
+    ### commands auto generated by Alembic - please adjust! ###
+    op.add_column('users', sa.Column('activated', sa.DateTime(), nullable=True))
+    ### end Alembic commands ###
+
+
+def downgrade():
+    ### commands auto generated by Alembic - please adjust! ###
+    op.drop_column('users', 'activated')
+    ### end Alembic commands ###

+ 11 - 5
tests/fixtures/user.py

@@ -1,3 +1,4 @@
+import datetime
 import pytest
 from flaskbb.user.models import User, Guest
 
@@ -12,7 +13,8 @@ def guest():
 def user(default_groups):
     """Creates a user with normal permissions."""
     user = User(username="test_normal", email="test_normal@example.org",
-                password="test", primary_group=default_groups[3])
+                password="test", primary_group=default_groups[3],
+                activated=datetime.datetime.utcnow())
     user.save()
     return user
 
@@ -22,7 +24,8 @@ def moderator_user(user, forum, default_groups):
     """Creates a test user with moderator permissions."""
 
     user = User(username="test_mod", email="test_mod@example.org",
-                password="test", primary_group=default_groups[2])
+                password="test", primary_group=default_groups[2],
+                activated=datetime.datetime.utcnow())
     user.save()
 
     forum.moderators.append(user)
@@ -34,7 +37,8 @@ def moderator_user(user, forum, default_groups):
 def admin_user(default_groups):
     """Creates a admin user."""
     user = User(username="test_admin", email="test_admin@example.org",
-                password="test", primary_group=default_groups[0])
+                password="test", primary_group=default_groups[0],
+                activated=datetime.datetime.utcnow())
     user.save()
     return user
 
@@ -43,7 +47,8 @@ def admin_user(default_groups):
 def super_moderator_user(default_groups):
     """Creates a super moderator user."""
     user = User(username="test_super_mod", email="test_super@example.org",
-                password="test", primary_group=default_groups[1])
+                password="test", primary_group=default_groups[1],
+                activated=datetime.datetime.utcnow())
     user.save()
     return user
 
@@ -57,6 +62,7 @@ def Fred(default_groups):
     Our job is stop Fred.
     """
     fred = User(username='Fred', email='fred@fred.fred',
-                password='fred', primary_group=default_groups[3])
+                password='fred', primary_group=default_groups[3],
+                activated=datetime.datetime.utcnow())
     fred.save()
     return fred