views.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. # -*- coding: utf-8 -*-
  2. """
  3. flaskbb.auth.views
  4. ~~~~~~~~~~~~~~~~~~
  5. This view provides user authentication, registration and a view for
  6. resetting the password of a user if he has lost his password
  7. :copyright: (c) 2014 by the FlaskBB Team.
  8. :license: BSD, see LICENSE for more details.
  9. """
  10. from datetime import datetime
  11. from flask import Blueprint, flash, g, redirect, request, url_for
  12. from flask.views import MethodView
  13. from flask_babelplus import gettext as _
  14. from flask_login import (confirm_login, current_user, login_fresh,
  15. login_required, login_user, logout_user)
  16. from flaskbb.auth.forms import (AccountActivationForm, ForgotPasswordForm,
  17. LoginForm, LoginRecaptchaForm, ReauthForm,
  18. RegisterForm, RequestActivationForm,
  19. ResetPasswordForm)
  20. from flaskbb.email import send_activation_token, send_reset_token
  21. from flaskbb.exceptions import AuthenticationError
  22. from flaskbb.extensions import limiter
  23. from flaskbb.user.models import User
  24. from flaskbb.utils.helpers import (anonymous_required, enforce_recaptcha,
  25. format_timedelta, get_available_languages,
  26. redirect_or_next, register_view,
  27. registration_enabled, render_template,
  28. requires_unactivated)
  29. from flaskbb.utils.settings import flaskbb_config
  30. from flaskbb.utils.tokens import get_token_status
  31. auth = Blueprint("auth", __name__)
  32. @auth.before_request
  33. def check_rate_limiting():
  34. """Check the the rate limits for each request for this blueprint."""
  35. if not flaskbb_config["AUTH_RATELIMIT_ENABLED"]:
  36. return None
  37. return limiter.check()
  38. @auth.errorhandler(429)
  39. def login_rate_limit_error(error):
  40. """Register a custom error handler for a 'Too Many Requests'
  41. (HTTP CODE 429) error."""
  42. return render_template("errors/too_many_logins.html", timeout=error.description)
  43. def login_rate_limit():
  44. """Dynamically load the rate limiting config from the database."""
  45. # [count] [per|/] [n (optional)] [second|minute|hour|day|month|year]
  46. return "{count}/{timeout}minutes".format(
  47. count=flaskbb_config["AUTH_REQUESTS"], timeout=flaskbb_config["AUTH_TIMEOUT"]
  48. )
  49. def login_rate_limit_message():
  50. """Display the amount of time left until the user can access the requested
  51. resource again."""
  52. current_limit = getattr(g, 'view_rate_limit', None)
  53. if current_limit is not None:
  54. window_stats = limiter.limiter.get_window_stats(*current_limit)
  55. reset_time = datetime.utcfromtimestamp(window_stats[0])
  56. timeout = reset_time - datetime.utcnow()
  57. return "{timeout}".format(timeout=format_timedelta(timeout))
  58. # Activate rate limiting on the whole blueprint
  59. limiter.limit(login_rate_limit, error_message=login_rate_limit_message)(auth)
  60. class Logout(MethodView):
  61. decorators = [limiter.exempt, login_required]
  62. def get(self):
  63. logout_user()
  64. flash(_("Logged out"), "success")
  65. return redirect(url_for("forum.index"))
  66. class Login(MethodView):
  67. decorators = [anonymous_required]
  68. def form(self):
  69. if enforce_recaptcha(limiter):
  70. return LoginRecaptchaForm()
  71. return LoginForm()
  72. def get(self):
  73. return render_template("auth/login.html", form=self.form())
  74. def post(self):
  75. form = self.form()
  76. if form.validate_on_submit():
  77. try:
  78. user = User.authenticate(form.login.data, form.password.data)
  79. if not login_user(user, remember=form.remember_me.data):
  80. flash(
  81. _(
  82. "In order to use your account you have to activate it "
  83. "through the link we have sent to your email "
  84. "address."
  85. ), "danger"
  86. )
  87. return redirect_or_next(url_for("forum.index"))
  88. except AuthenticationError:
  89. flash(_("Wrong username or password."), "danger")
  90. return render_template("auth/login.html", form=form)
  91. class Reauth(MethodView):
  92. decorators = [login_required, limiter.exempt]
  93. form = ReauthForm
  94. def get(self):
  95. if not login_fresh():
  96. return render_template("auth/reauth.html", form=self.form())
  97. return redirect_or_next(current_user.url)
  98. def post(self):
  99. form = self.form()
  100. if form.validate_on_submit():
  101. if current_user.check_password(form.password.data):
  102. confirm_login()
  103. flash(_("Reauthenticated."), "success")
  104. return redirect_or_next(current_user.url)
  105. flash(_("Wrong password."), "danger")
  106. return render_template("auth/reauth.html", form=form)
  107. class Register(MethodView):
  108. decorators = [anonymous_required, registration_enabled]
  109. def form(self):
  110. form = RegisterForm()
  111. form.language.choices = get_available_languages()
  112. form.language.default = flaskbb_config['DEFAULT_LANGUAGE']
  113. form.process(request.form) # needed because a default is overriden
  114. return form
  115. def get(self):
  116. return render_template("auth/register.html", form=self.form())
  117. def post(self):
  118. form = self.form()
  119. if form.validate_on_submit():
  120. user = form.save()
  121. if flaskbb_config["ACTIVATE_ACCOUNT"]:
  122. # Any call to an expired model requires a database hit, so
  123. # accessing user.id would cause an DetachedInstanceError.
  124. # This happens because the `user`'s session does no longer exist.
  125. # So we just fire up another query to make sure that the session
  126. # for the newly created user is fresh.
  127. # PS: `db.session.merge(user)` did not work for me.
  128. user = User.query.filter_by(email=user.email).first()
  129. send_activation_token.delay(user)
  130. flash(
  131. _("An account activation email has been sent to %(email)s", email=user.email),
  132. "success"
  133. )
  134. else:
  135. login_user(user)
  136. flash(_("Thanks for registering."), "success")
  137. return redirect_or_next(url_for('forum.index'))
  138. return render_template("auth/register.html", form=form)
  139. class ForgotPassword(MethodView):
  140. decorators = [anonymous_required]
  141. form = ForgotPasswordForm
  142. def get(self):
  143. return render_template("auth/forgot_password.html", form=self.form())
  144. def post(self):
  145. form = self.form()
  146. if form.validate_on_submit():
  147. user = User.query.filter_by(email=form.email.data).first()
  148. if user:
  149. send_reset_token.delay(user)
  150. flash(_("Email sent! Please check your inbox."), "info")
  151. return redirect(url_for("auth.forgot_password"))
  152. else:
  153. flash(
  154. _(
  155. "You have entered an username or email address that is "
  156. "not linked with your account."
  157. ), "danger"
  158. )
  159. return render_template("auth/forgot_password.html", form=form)
  160. class ResetPassword(MethodView):
  161. decorators = [anonymous_required]
  162. form = ResetPasswordForm
  163. def get(self, token):
  164. form = self.form()
  165. form.token.data = token
  166. return render_template("auth/reset_password.html", form=form)
  167. def post(self, token):
  168. form = self.form()
  169. if form.validate_on_submit():
  170. expired, invalid, user = get_token_status(form.token.data, "reset_password")
  171. if invalid:
  172. flash(_("Your password token is invalid."), "danger")
  173. return redirect(url_for("auth.forgot_password"))
  174. if expired:
  175. flash(_("Your password token is expired."), "danger")
  176. return redirect(url_for("auth.forgot_password"))
  177. if user:
  178. user.password = form.password.data
  179. user.save()
  180. flash(_("Your password has been updated."), "success")
  181. return redirect(url_for("auth.login"))
  182. form.token.data = token
  183. return render_template("auth/reset_password.html", form=form)
  184. class RequestActivationToken(MethodView):
  185. decorators = [requires_unactivated]
  186. form = RequestActivationForm
  187. def get(self):
  188. return render_template("auth/request_account_activation.html", form=self.form())
  189. def post(self):
  190. form = self.form()
  191. if form.validate_on_submit():
  192. user = User.query.filter_by(email=form.email.data).first()
  193. send_activation_token.delay(user)
  194. flash(
  195. _("A new account activation token has been sent to "
  196. "your email address."), "success"
  197. )
  198. return redirect(url_for("auth.activate_account"))
  199. return render_template("auth/request_account_activation.html", form=form)
  200. class ActivateAccount(MethodView):
  201. form = AccountActivationForm
  202. decorators = [requires_unactivated]
  203. def get(self, token=None):
  204. expired = invalid = user = None
  205. if token is not None:
  206. expired, invalid, user = get_token_status(token, "activate_account")
  207. if invalid:
  208. flash(_("Your account activation token is invalid."), "danger")
  209. return redirect(url_for("auth.request_activation_token"))
  210. if expired:
  211. flash(_("Your account activation token is expired."), "danger")
  212. return redirect(url_for("auth.request_activation_token"))
  213. if user:
  214. user.activated = True
  215. user.save()
  216. if current_user != user:
  217. logout_user()
  218. login_user(user)
  219. flash(_("Your account has been activated."), "success")
  220. return redirect(url_for("forum.index"))
  221. return render_template("auth/account_activation.html", form=self.form())
  222. def post(self, token=None):
  223. expired = invalid = user = None
  224. form = self.form()
  225. if token is not None:
  226. expired, invalid, user = get_token_status(token, "activate_account")
  227. elif form.validate_on_submit():
  228. expired, invalid, user = get_token_status(form.token.data, "activate_account")
  229. if invalid:
  230. flash(_("Your account activation token is invalid."), "danger")
  231. return redirect(url_for("auth.request_activation_token"))
  232. if expired:
  233. flash(_("Your account activation token is expired."), "danger")
  234. return redirect(url_for("auth.request_activation_token"))
  235. if user:
  236. user.activated = True
  237. user.save()
  238. if current_user != user:
  239. logout_user()
  240. login_user(user)
  241. flash(_("Your account has been activated."), "success")
  242. return redirect(url_for("forum.index"))
  243. return render_template("auth/account_activation.html", form=form)
  244. register_view(auth, routes=['/logout'], view_func=Logout.as_view('logout'))
  245. register_view(auth, routes=['/login'], view_func=Login.as_view('login'))
  246. register_view(auth, routes=['/reauth'], view_func=Reauth.as_view('reauth'))
  247. register_view(auth, routes=['/register'], view_func=Register.as_view('register'))
  248. register_view(
  249. auth, routes=['/reset-password'], view_func=ForgotPassword.as_view('forgot_password')
  250. )
  251. register_view(
  252. auth, routes=['/reset-password/<token>'], view_func=ResetPassword.as_view('reset_password')
  253. )
  254. register_view(
  255. auth,
  256. routes=['/activate'],
  257. view_func=RequestActivationToken.as_view('request_activation_token')
  258. )
  259. register_view(
  260. auth,
  261. routes=['/activate/confirm', '/activate/confirm/<token>'],
  262. view_func=ActivateAccount.as_view('activate_account')
  263. )