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

Convert Auth views to CBV

Leave logout alone as it is a single HTTP method view
Alec Nikolas Reiter 7 лет назад
Родитель
Сommit
20c8eeab39
4 измененных файлов с 280 добавлено и 179 удалено
  1. 1 0
      flaskbb/auth/forms.py
  2. 232 177
      flaskbb/auth/views.py
  3. 1 1
      flaskbb/templates/auth/login.html
  4. 46 1
      flaskbb/utils/helpers.py

+ 1 - 0
flaskbb/auth/forms.py

@@ -37,6 +37,7 @@ class LoginForm(FlaskForm):
     remember_me = BooleanField(_("Remember me"), default=False)
 
     submit = SubmitField(_("Login"))
+    recaptcha = HiddenField(_("Captcha"))
 
 
 class LoginRecaptchaForm(LoginForm):

+ 232 - 177
flaskbb/auth/views.py

@@ -11,21 +11,24 @@
 """
 from datetime import datetime
 
-from flask import Blueprint, flash, redirect, url_for, request, g
-from flask_login import (current_user, login_user, login_required,
-                         logout_user, confirm_login, login_fresh)
+from flask import Blueprint, flash, g, redirect, request, url_for
+from flask.views import MethodView
 from flask_babelplus import gettext as _
-
-from flaskbb.extensions import limiter
-from flaskbb.utils.helpers import (render_template, redirect_or_next,
-                                   format_timedelta, get_available_languages)
-from flaskbb.email import send_reset_token, send_activation_token
+from flask_login import (confirm_login, current_user, login_fresh,
+                         login_required, login_user, logout_user)
+
+from flaskbb.auth.forms import (AccountActivationForm, ForgotPasswordForm,
+                                LoginForm, LoginRecaptchaForm, ReauthForm,
+                                RegisterForm, RequestActivationForm,
+                                ResetPasswordForm)
+from flaskbb.email import send_activation_token, send_reset_token
 from flaskbb.exceptions import AuthenticationError
-from flaskbb.auth.forms import (LoginForm, LoginRecaptchaForm, ReauthForm,
-                                ForgotPasswordForm, ResetPasswordForm,
-                                RegisterForm, AccountActivationForm,
-                                RequestActivationForm)
+from flaskbb.extensions import limiter
 from flaskbb.user.models import User
+from flaskbb.utils.helpers import (anonymous_required, enforce_recaptcha,
+                                   format_timedelta, get_available_languages,
+                                   redirect_or_next, registration_enabled,
+                                   render_template, requires_unactivated)
 from flaskbb.utils.settings import flaskbb_config
 from flaskbb.utils.tokens import get_token_status
 
@@ -44,16 +47,14 @@ def check_rate_limiting():
 def login_rate_limit_error(error):
     """Register a custom error handler for a 'Too Many Requests'
     (HTTP CODE 429) error."""
-    return render_template("errors/too_many_logins.html",
-                           timeout=error.description)
+    return render_template("errors/too_many_logins.html", timeout=error.description)
 
 
 def login_rate_limit():
     """Dynamically load the rate limiting config from the database."""
     # [count] [per|/] [n (optional)] [second|minute|hour|day|month|year]
     return "{count}/{timeout}minutes".format(
-        count=flaskbb_config["AUTH_REQUESTS"],
-        timeout=flaskbb_config["AUTH_TIMEOUT"]
+        count=flaskbb_config["AUTH_REQUESTS"], timeout=flaskbb_config["AUTH_TIMEOUT"]
     )
 
 
@@ -72,45 +73,58 @@ def login_rate_limit_message():
 limiter.limit(login_rate_limit, error_message=login_rate_limit_message)(auth)
 
 
-@auth.route("/login", methods=["GET", "POST"])
-def login():
-    """Logs the user in."""
-    if current_user is not None and current_user.is_authenticated:
-        return redirect_or_next(url_for("forum.index"))
-
-    current_limit = getattr(g, 'view_rate_limit', None)
-    login_recaptcha = False
-    if current_limit is not None:
-        window_stats = limiter.limiter.get_window_stats(*current_limit)
-        stats_diff = flaskbb_config["AUTH_REQUESTS"] - window_stats[1]
-        login_recaptcha = stats_diff >= flaskbb_config["LOGIN_RECAPTCHA"]
+@auth.route("/logout")
+@limiter.exempt
+@login_required
+def logout():
+    """Logs the user out."""
+    logout_user()
+    flash(_("Logged out"), "success")
+    return redirect(url_for("forum.index"))
 
-    form = LoginForm()
-    if login_recaptcha and flaskbb_config["RECAPTCHA_ENABLED"]:
-        form = LoginRecaptchaForm()
 
-    if form.validate_on_submit():
-        try:
-            user = User.authenticate(form.login.data, form.password.data)
-            if not login_user(user, remember=form.remember_me.data):
-                flash(_("In order to use your account you have to activate it "
-                        "through the link we have sent to your email "
-                        "address."), "danger")
-            return redirect_or_next(url_for("forum.index"))
-        except AuthenticationError:
-            flash(_("Wrong username or password."), "danger")
+class Login(MethodView):
+    decorators = [anonymous_required]
 
-    return render_template("auth/login.html", form=form,
-                           login_recaptcha=login_recaptcha)
+    def form(self):
+        if enforce_recaptcha(limiter):
+            return LoginRecaptchaForm()
+        return LoginForm()
 
+    def get(self):
+        return render_template("auth/login.html", form=self.form())
 
-@auth.route("/reauth", methods=["GET", "POST"])
-@limiter.exempt
-@login_required
-def reauth():
-    """Reauthenticates a user."""
-    if not login_fresh():
-        form = ReauthForm(request.form)
+    def post(self):
+        form = self.form()
+        if form.validate_on_submit():
+            try:
+                user = User.authenticate(form.login.data, form.password.data)
+                if not login_user(user, remember=form.remember_me.data):
+                    flash(
+                        _(
+                            "In order to use your account you have to activate it "
+                            "through the link we have sent to your email "
+                            "address."
+                        ), "danger"
+                    )
+                return redirect_or_next(url_for("forum.index"))
+            except AuthenticationError:
+                flash(_("Wrong username or password."), "danger")
+
+        return render_template("auth/login.html", form=form)
+
+
+class Reauth(MethodView):
+    decorators = [login_required, limiter.exempt]
+    form = ReauthForm
+
+    def get(self):
+        if not login_fresh():
+            return render_template("auth/reauth.html", form=self.form())
+        return redirect_or_next(current_user.url)
+
+    def post(self):
+        form = self.form()
         if form.validate_on_submit():
             if current_user.check_password(form.password.data):
                 confirm_login()
@@ -119,160 +133,201 @@ def reauth():
 
             flash(_("Wrong password."), "danger")
         return render_template("auth/reauth.html", form=form)
-    return redirect(request.args.get("next") or current_user.url)
 
 
-@auth.route("/logout")
-@limiter.exempt
-@login_required
-def logout():
-    """Logs the user out."""
-    logout_user()
-    flash(_("Logged out"), "success")
-    return redirect(url_for("forum.index"))
+class Register(MethodView):
+    decorators = [anonymous_required, registration_enabled]
+
+    def form(self):
+        form = RegisterForm()
+
+        form.language.choices = get_available_languages()
+        form.language.default = flaskbb_config['DEFAULT_LANGUAGE']
+        form.process(request.form)  # needed because a default is overriden
+        return form
+
+    def get(self):
+        return render_template("auth/register.html", form=self.form())
 
+    def post(self):
+        form = self.form()
+        if form.validate_on_submit():
+            user = form.save()
+
+            if flaskbb_config["ACTIVATE_ACCOUNT"]:
+                # Any call to an expired model requires a database hit, so
+                # accessing user.id would cause an DetachedInstanceError.
+                # This happens because the `user`'s session does no longer exist.
+                # So we just fire up another query to make sure that the session
+                # for the newly created user is fresh.
+                # PS: `db.session.merge(user)` did not work for me.
+                user = User.query.filter_by(email=user.email).first()
+                send_activation_token.delay(user)
+                flash(
+                    _("An account activation email has been sent to %(email)s", email=user.email),
+                    "success"
+                )
+            else:
+                login_user(user)
+                flash(_("Thanks for registering."), "success")
+
+            return redirect_or_next(url_for('forum.index'))
+
+        return render_template("auth/register.html", form=form)
+
+
+class ForgotPassword(MethodView):
+    decorators = [anonymous_required]
+    form = ForgotPasswordForm
+
+    def get(self):
+        return render_template("auth/forgot_password.html", form=self.form())
+
+    def post(self):
+        form = self.form()
+        if form.validate_on_submit():
+            user = User.query.filter_by(email=form.email.data).first()
+
+            if user:
+                send_reset_token.delay(user)
+                flash(_("Email sent! Please check your inbox."), "info")
+                return redirect(url_for("auth.forgot_password"))
+            else:
+                flash(
+                    _(
+                        "You have entered an username or email address that is "
+                        "not linked with your account."
+                    ), "danger"
+                )
+        return render_template("auth/forgot_password.html", form=form)
+
+
+class ResetPassword(MethodView):
+    decorators = [anonymous_required]
+    form = ResetPasswordForm
+
+    def get(self, token):
+        form = self.form()
+        form.token.data = token
+        return render_template("auth/reset_password.html", form=form)
+
+    def post(self, token):
+        form = self.form()
+        if form.validate_on_submit():
+            expired, invalid, user = get_token_status(form.token.data, "reset_password")
 
-@auth.route("/register", methods=["GET", "POST"])
-def register():
-    """Register a new user."""
-    if current_user is not None and current_user.is_authenticated:
-        return redirect_or_next(url_for("forum.index"))
+            if invalid:
+                flash(_("Your password token is invalid."), "danger")
+                return redirect(url_for("auth.forgot_password"))
 
-    if not flaskbb_config["REGISTRATION_ENABLED"]:
-        flash(_("The registration has been disabled."), "info")
-        return redirect_or_next(url_for("forum.index"))
+            if expired:
+                flash(_("Your password token is expired."), "danger")
+                return redirect(url_for("auth.forgot_password"))
 
-    form = RegisterForm(request.form)
+            if user:
+                user.password = form.password.data
+                user.save()
+                flash(_("Your password has been updated."), "success")
+                return redirect(url_for("auth.login"))
 
-    form.language.choices = get_available_languages()
-    form.language.default = flaskbb_config['DEFAULT_LANGUAGE']
-    form.process(request.form)  # needed because a default is overriden
+        form.token.data = token
+        return render_template("auth/reset_password.html", form=form)
 
-    if form.validate_on_submit():
-        user = form.save()
 
-        if flaskbb_config["ACTIVATE_ACCOUNT"]:
-            # Any call to an expired model requires a database hit, so
-            # accessing user.id would cause an DetachedInstanceError.
-            # This happens because the `user`'s session does no longer exist.
-            # So we just fire up another query to make sure that the session
-            # for the newly created user is fresh.
-            # PS: `db.session.merge(user)` did not work for me.
-            user = User.query.filter_by(email=user.email).first()
+class RequestActivationToken(MethodView):
+    decorators = [requires_unactivated]
+    form = RequestActivationForm
+
+    def get(self):
+        return render_template("auth/request_account_activation.html", form=self.form())
+
+    def post(self):
+        form = self.form()
+        if form.validate_on_submit():
+            user = User.query.filter_by(email=form.email.data).first()
             send_activation_token.delay(user)
-            flash(_("An account activation email has been sent to %(email)s",
-                    email=user.email), "success")
-        else:
-            login_user(user)
-            flash(_("Thanks for registering."), "success")
+            flash(
+                _("A new account activation token has been sent to "
+                  "your email address."), "success"
+            )
+            return redirect(url_for("auth.activate_account"))
 
-        return redirect_or_next(url_for('forum.index'))
+        return render_template("auth/request_account_activation.html", form=form)
 
-    return render_template("auth/register.html", form=form)
 
+class ActivateAccount(MethodView):
+    form = AccountActivationForm
+    decorators = [requires_unactivated]
 
-@auth.route('/reset-password', methods=["GET", "POST"])
-def forgot_password():
-    """Sends a reset password token to the user."""
-    if not current_user.is_anonymous:
-        return redirect(url_for("forum.index"))
+    def get(self, token=None):
+        expired = invalid = user = None
+        if token is not None:
+            expired, invalid, user = get_token_status(token, "activate_account")
 
-    form = ForgotPasswordForm()
-    if form.validate_on_submit():
-        user = User.query.filter_by(email=form.email.data).first()
+        if invalid:
+            flash(_("Your account activation token is invalid."), "danger")
+            return redirect(url_for("auth.request_activation_token"))
+
+        if expired:
+            flash(_("Your account activation token is expired."), "danger")
+            return redirect(url_for("auth.request_activation_token"))
 
         if user:
-            send_reset_token.delay(user)
-            flash(_("Email sent! Please check your inbox."), "info")
-            return redirect(url_for("auth.forgot_password"))
-        else:
-            flash(_("You have entered an username or email address that is "
-                    "not linked with your account."), "danger")
-    return render_template("auth/forgot_password.html", form=form)
-
-
-@auth.route("/reset-password/<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():
-        expired, invalid, user = get_token_status(form.token.data,
-                                                  "reset_password")
+            user.activated = True
+            user.save()
+
+            if current_user != user:
+                logout_user()
+                login_user(user)
+
+            flash(_("Your account has been activated."), "success")
+            return redirect(url_for("forum.index"))
+
+        return render_template("auth/account_activation.html", form=self.form())
+
+    def post(self, token=None):
+        expired = invalid = user = None
+        form = self.form()
+
+        if token is not None:
+            expired, invalid, user = get_token_status(token, "activate_account")
+
+        elif form.validate_on_submit():
+            expired, invalid, user = get_token_status(form.token.data, "activate_account")
 
         if invalid:
-            flash(_("Your password token is invalid."), "danger")
-            return redirect(url_for("auth.forgot_password"))
+            flash(_("Your account activation token is invalid."), "danger")
+            return redirect(url_for("auth.request_activation_token"))
 
         if expired:
-            flash(_("Your password token is expired."), "danger")
-            return redirect(url_for("auth.forgot_password"))
+            flash(_("Your account activation token is expired."), "danger")
+            return redirect(url_for("auth.request_activation_token"))
 
         if user:
-            user.password = form.password.data
+            user.activated = True
             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)
-
-
-@auth.route("/activate", methods=["GET", "POST"])
-def request_activation_token():
-    """Requests a new account activation token."""
-    if current_user.is_active or not flaskbb_config["ACTIVATE_ACCOUNT"]:
-        flash(_("This account is already activated."), "info")
-        return redirect(url_for('forum.index'))
-
-    form = RequestActivationForm()
-    if form.validate_on_submit():
-        user = User.query.filter_by(email=form.email.data).first()
-        send_activation_token.delay(user)
-        flash(_("A new account activation token has been sent to "
-                "your email address."), "success")
-        return redirect(url_for("auth.activate_account"))
-
-    return render_template("auth/request_account_activation.html", form=form)
-
-
-@auth.route("/activate/confirm", methods=["GET", "POST"])
-@auth.route("/activate/confirm/<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"]:
-        flash(_("This account is already activated."), "info")
-        return redirect(url_for('forum.index'))
-
-    expired = invalid = user = None
-    form = None
-    if token is not None:
-        expired, invalid, user = get_token_status(token, "activate_account")
-    else:
-        form = AccountActivationForm()
-        if form.validate_on_submit():
-            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_activation_token"))
+            if current_user != user:
+                logout_user()
+                login_user(user)
 
-    if expired:
-        flash(_("Your account activation token is expired."), "danger")
-        return redirect(url_for("auth.request_activation_token"))
+            flash(_("Your account has been activated."), "success")
+            return redirect(url_for("forum.index"))
 
-    if user:
-        user.activated = True
-        user.save()
+        return render_template("auth/account_activation.html", form=form)
 
-        if current_user != user:
-            logout_user()
-            login_user(user)
 
-        flash(_("Your account has been activated."), "success")
-        return redirect(url_for("forum.index"))
+auth.add_url_rule("/login", view_func=Login.as_view('login'))
+auth.add_url_rule("/reauth", view_func=Reauth.as_view('reauth'))
+auth.add_url_rule("/register", view_func=Register.as_view('register'))
+auth.add_url_rule("/reset-password", view_func=ForgotPassword.as_view('forgot_password'))
+auth.add_url_rule("/reset-password/<token>", view_func=ResetPassword.as_view('reset_password'))
+auth.add_url_rule(
+    "/activate", view_func=RequestActivationToken.as_view('request_activation_token')
+)
 
-    return render_template("auth/account_activation.html", form=form)
+# need to register this one specially because Flask complains otherwise
+_activate = ActivateAccount.as_view('activate_account')
+auth.add_url_rule("/activate/confirm", view_func=_activate)
+auth.add_url_rule("/activate/confirm/<token>", view_func=_activate)
+del _activate

+ 1 - 1
flaskbb/templates/auth/login.html

@@ -16,7 +16,7 @@
             {{ horizontal_field(form.login)}}
             {{ horizontal_field(form.password)}}
             {{ horizontal_field(form.remember_me) }}
-            {% if flaskbb_config["RECAPTCHA_ENABLED"] and login_recaptcha %}
+            {% if flaskbb_config["RECAPTCHA_ENABLED"] %}
                 {{ horizontal_field(form.recaptcha) }}
             {% endif %}
 

+ 46 - 1
flaskbb/utils/helpers.py

@@ -18,10 +18,11 @@ import glob
 from datetime import datetime, timedelta
 from pytz import UTC
 from PIL import ImageFile
+from functools import wraps
 
 import requests
 import unidecode
-from flask import session, url_for, flash, redirect, request
+from flask import session, url_for, flash, redirect, request, g
 from jinja2 import Markup
 from babel.core import get_locale_identifier
 from babel.dates import format_timedelta as babel_format_timedelta
@@ -625,3 +626,47 @@ def real(obj):
     if isinstance(obj, LocalProxy):
         return obj._get_current_object()
     return obj
+
+
+def anonymous_required(f):
+
+    @wraps(f)
+    def wrapper(*a, **k):
+        if current_user is not None and current_user.is_authenticated:
+            return redirect_or_next(url_for('forum.index'))
+        return f(*a, **k)
+
+    return wrapper
+
+
+def enforce_recaptcha(limiter):
+    current_limit = getattr(g, 'view_rate_limit', None)
+    login_recaptcha = False
+    if current_limit is not None:
+        window_stats = limiter.limiter.get_window_stats(*current_limit)
+        stats_diff = flaskbb_config["AUTH_REQUESTS"] - window_stats[1]
+        login_recaptcha = stats_diff >= flaskbb_config["LOGIN_RECAPTCHA"]
+    return login_recaptcha
+
+
+def registration_enabled(f):
+
+    @wraps(f)
+    def wrapper(*a, **k):
+        if not flaskbb_config["REGISTRATION_ENABLED"]:
+            flash(_("The registration has been disabled."), "info")
+            return redirect_or_next(url_for("forum.index"))
+        return f(*a, **k)
+
+    return wrapper
+
+
+def requires_unactivated(f):
+
+    @wraps(f)
+    def wrapper(*a, **k):
+        if current_user.is_active or not flaskbb_config["ACTIVATE_ACCOUNT"]:
+            flash(_("This account is already activated."), "info")
+            return redirect(url_for('forum.index'))
+        return f(*a, **k)
+    return wrapper