views.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  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. import logging
  11. from datetime import datetime
  12. from flask import Blueprint, current_app, flash, g, redirect, request, url_for
  13. from flask.views import MethodView
  14. from flask_babelplus import gettext as _
  15. from flask_login import (confirm_login, current_user, login_fresh,
  16. login_required, login_user, logout_user)
  17. from flaskbb.auth.forms import (ForgotPasswordForm, LoginForm,
  18. LoginRecaptchaForm, ReauthForm, RegisterForm,
  19. RequestActivationForm, ResetPasswordForm)
  20. from flaskbb.exceptions import AuthenticationError
  21. from flaskbb.extensions import db, limiter
  22. from flaskbb.user.models import User
  23. from flaskbb.utils.helpers import (anonymous_required, enforce_recaptcha,
  24. format_timedelta, get_available_languages,
  25. redirect_or_next, register_view,
  26. registration_enabled, render_template,
  27. requires_unactivated)
  28. from flaskbb.utils.settings import flaskbb_config
  29. from ..core.exceptions import StopValidation, ValidationError
  30. from ..core.tokens import TokenError
  31. from ..core.auth.registration import UserRegistrationInfo
  32. from .plugins import impl
  33. from .services import (account_activator_factory, registration_service_factory,
  34. reset_service_factory)
  35. logger = logging.getLogger(__name__)
  36. class Logout(MethodView):
  37. decorators = [limiter.exempt, login_required]
  38. def get(self):
  39. logout_user()
  40. flash(_("Logged out"), "success")
  41. return redirect(url_for("forum.index"))
  42. class Login(MethodView):
  43. decorators = [anonymous_required]
  44. def form(self):
  45. if enforce_recaptcha(limiter):
  46. return LoginRecaptchaForm()
  47. return LoginForm()
  48. def get(self):
  49. return render_template("auth/login.html", form=self.form())
  50. def post(self):
  51. form = self.form()
  52. if form.validate_on_submit():
  53. try:
  54. user = User.authenticate(form.login.data, form.password.data)
  55. if not login_user(user, remember=form.remember_me.data):
  56. flash(
  57. _(
  58. "In order to use your account you have to "
  59. "activate it through the link we have sent to "
  60. "your email address."
  61. ), "danger"
  62. )
  63. return redirect_or_next(url_for("forum.index"))
  64. except AuthenticationError:
  65. flash(_("Wrong username or password."), "danger")
  66. return render_template("auth/login.html", form=form)
  67. class Reauth(MethodView):
  68. decorators = [login_required, limiter.exempt]
  69. form = ReauthForm
  70. def get(self):
  71. if not login_fresh():
  72. return render_template("auth/reauth.html", form=self.form())
  73. return redirect_or_next(current_user.url)
  74. def post(self):
  75. form = self.form()
  76. if form.validate_on_submit():
  77. if current_user.check_password(form.password.data):
  78. confirm_login()
  79. flash(_("Reauthenticated."), "success")
  80. return redirect_or_next(current_user.url)
  81. flash(_("Wrong password."), "danger")
  82. return render_template("auth/reauth.html", form=form)
  83. class Register(MethodView):
  84. decorators = [anonymous_required, registration_enabled]
  85. def __init__(self, registration_service_factory):
  86. self.registration_service_factory = registration_service_factory
  87. def form(self):
  88. form = RegisterForm()
  89. form.language.choices = get_available_languages()
  90. form.language.default = flaskbb_config['DEFAULT_LANGUAGE']
  91. form.process(request.form) # needed because a default is overriden
  92. return form
  93. def get(self):
  94. return render_template("auth/register.html", form=self.form())
  95. def post(self):
  96. form = self.form()
  97. if form.validate_on_submit():
  98. registration_info = UserRegistrationInfo(
  99. username=form.username.data,
  100. password=form.password.data,
  101. group=4,
  102. email=form.email.data,
  103. language=form.language.data
  104. )
  105. service = self.registration_service_factory()
  106. try:
  107. service.register(registration_info)
  108. except StopValidation as e:
  109. form.populate_errors(e.reasons)
  110. return render_template("auth/register.html", form=form)
  111. else:
  112. try:
  113. db.session.commit()
  114. except Exception: # noqa
  115. logger.exception("Database error while resetting password")
  116. flash(
  117. _(
  118. "Could not process registration due"
  119. "to an unrecoverable error"
  120. ), "danger"
  121. )
  122. return render_template("auth/register.html", form=form)
  123. current_app.pluggy.hook.flaskbb_event_user_registered(
  124. username=registration_info.username
  125. )
  126. return redirect_or_next(url_for('forum.index'))
  127. return render_template("auth/register.html", form=form)
  128. class ForgotPassword(MethodView):
  129. decorators = [anonymous_required]
  130. form = ForgotPasswordForm
  131. def __init__(self, password_reset_service_factory):
  132. self.password_reset_service_factory = password_reset_service_factory
  133. def get(self):
  134. return render_template("auth/forgot_password.html", form=self.form())
  135. def post(self):
  136. form = self.form()
  137. if form.validate_on_submit():
  138. try:
  139. self.password_reset_service_factory(
  140. ).initiate_password_reset(form.email.data)
  141. except ValidationError:
  142. flash(
  143. _(
  144. "You have entered an username or email address that "
  145. "is not linked with your account."
  146. ), "danger"
  147. )
  148. else:
  149. flash(_("Email sent! Please check your inbox."), "info")
  150. return redirect(url_for("auth.forgot_password"))
  151. return render_template("auth/forgot_password.html", form=form)
  152. class ResetPassword(MethodView):
  153. decorators = [anonymous_required]
  154. form = ResetPasswordForm
  155. def __init__(self, password_reset_service_factory):
  156. self.password_reset_service_factory = password_reset_service_factory
  157. def get(self, token):
  158. form = self.form()
  159. form.token.data = token
  160. return render_template("auth/reset_password.html", form=form)
  161. def post(self, token):
  162. form = self.form()
  163. if form.validate_on_submit():
  164. try:
  165. service = self.password_reset_service_factory()
  166. service.reset_password(
  167. token, form.email.data, form.password.data
  168. )
  169. db.session.commit()
  170. except TokenError as e:
  171. flash(_(e.reason), 'danger')
  172. return redirect(url_for('auth.forgot_password'))
  173. except StopValidation as e:
  174. form.populate_errors(e.reasons)
  175. form.token.data = token
  176. return render_template("auth/reset_password.html", form=form)
  177. except Exception:
  178. logger.exception("Error when resetting password")
  179. flash(_('Error when resetting password'))
  180. return redirect(url_for('auth.forgot_password'))
  181. flash(_("Your password has been updated."), "success")
  182. return redirect(url_for("auth.login"))
  183. form.token.data = token
  184. return render_template("auth/reset_password.html", form=form)
  185. class RequestActivationToken(MethodView):
  186. decorators = [requires_unactivated]
  187. form = RequestActivationForm
  188. def __init__(self, account_activator_factory):
  189. self.account_activator_factory = account_activator_factory
  190. def get(self):
  191. return render_template(
  192. "auth/request_account_activation.html", form=self.form()
  193. )
  194. def post(self):
  195. form = self.form()
  196. if form.validate_on_submit():
  197. activator = self.account_activator_factory()
  198. try:
  199. activator.initiate_account_activation(form.email.data)
  200. except ValidationError as e:
  201. form.populate_errors([(e.attribute, e.reason)])
  202. else:
  203. flash(
  204. _(
  205. "A new account activation token has been sent to "
  206. "your email address."
  207. ), "success"
  208. )
  209. return redirect(url_for("auth.activate_account"))
  210. return render_template(
  211. "auth/request_account_activation.html", form=form
  212. )
  213. class ActivateAccount(MethodView):
  214. decorators = [requires_unactivated]
  215. def __init__(self, account_activator_factory):
  216. self.account_activator_factory = account_activator_factory
  217. def get(self, token=None):
  218. activator = self.account_activator_factory()
  219. try:
  220. activator.activate_account(token)
  221. except TokenError as e:
  222. flash(_(e.reason), 'danger')
  223. except ValidationError as e:
  224. flash(_(e.reason), 'danger')
  225. return redirect('forum.index')
  226. else:
  227. try:
  228. db.session.commit()
  229. except Exception: # noqa
  230. logger.exception("Database error while activating account")
  231. flash(
  232. _("Could activate account due to an unrecoverable error"),
  233. "danger"
  234. )
  235. return redirect('auth.request_activation_token')
  236. flash(
  237. _("Your account has been activated and you can now login."),
  238. "success"
  239. )
  240. return redirect(url_for("forum.index"))
  241. return render_template("auth/account_activation.html")
  242. @impl(tryfirst=True)
  243. def flaskbb_load_blueprints(app):
  244. auth = Blueprint("auth", __name__)
  245. def login_rate_limit():
  246. """Dynamically load the rate limiting config from the database."""
  247. # [count] [per|/] [n (optional)] [second|minute|hour|day|month|year]
  248. return "{count}/{timeout}minutes".format(
  249. count=flaskbb_config["AUTH_REQUESTS"],
  250. timeout=flaskbb_config["AUTH_TIMEOUT"]
  251. )
  252. def login_rate_limit_message():
  253. """Display the amount of time left until the user can access the requested
  254. resource again."""
  255. current_limit = getattr(g, 'view_rate_limit', None)
  256. if current_limit is not None:
  257. window_stats = limiter.limiter.get_window_stats(*current_limit)
  258. reset_time = datetime.utcfromtimestamp(window_stats[0])
  259. timeout = reset_time - datetime.utcnow()
  260. return "{timeout}".format(timeout=format_timedelta(timeout))
  261. @auth.before_request
  262. def check_rate_limiting():
  263. """Check the the rate limits for each request for this blueprint."""
  264. if not flaskbb_config["AUTH_RATELIMIT_ENABLED"]:
  265. return None
  266. return limiter.check()
  267. @auth.errorhandler(429)
  268. def login_rate_limit_error(error):
  269. """Register a custom error handler for a 'Too Many Requests'
  270. (HTTP CODE 429) error."""
  271. return render_template(
  272. "errors/too_many_logins.html", timeout=error.description
  273. )
  274. # Activate rate limiting on the whole blueprint
  275. limiter.limit(
  276. login_rate_limit, error_message=login_rate_limit_message
  277. )(auth)
  278. register_view(auth, routes=['/logout'], view_func=Logout.as_view('logout'))
  279. register_view(auth, routes=['/login'], view_func=Login.as_view('login'))
  280. register_view(auth, routes=['/reauth'], view_func=Reauth.as_view('reauth'))
  281. register_view(
  282. auth,
  283. routes=['/register'],
  284. view_func=Register.as_view(
  285. 'register',
  286. registration_service_factory=registration_service_factory
  287. )
  288. )
  289. register_view(
  290. auth,
  291. routes=['/reset-password'],
  292. view_func=ForgotPassword.as_view(
  293. 'forgot_password',
  294. password_reset_service_factory=reset_service_factory
  295. )
  296. )
  297. register_view(
  298. auth,
  299. routes=['/reset-password/<token>'],
  300. view_func=ResetPassword.as_view(
  301. 'reset_password',
  302. password_reset_service_factory=reset_service_factory
  303. )
  304. )
  305. register_view(
  306. auth,
  307. routes=['/activate'],
  308. view_func=RequestActivationToken.as_view(
  309. 'request_activation_token',
  310. account_activator_factory=account_activator_factory
  311. )
  312. )
  313. register_view(
  314. auth,
  315. routes=['/activate/confirm', '/activate/confirm/<token>'],
  316. view_func=ActivateAccount.as_view(
  317. 'activate_account',
  318. account_activator_factory=account_activator_factory
  319. )
  320. )
  321. app.register_blueprint(auth, url_prefix=app.config['AUTH_URL_PREFIX'])