views.py 15 KB

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