views.py 15 KB

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