FlaskBB 2.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- celery_worker.py +18 -0
- flaskbb/__init__.py +19 -0
- flaskbb/app.py +551 -0
- flaskbb/auth/__init__.py +7 -0
- flaskbb/auth/forms.py +170 -0
- flaskbb/auth/plugins.py +119 -0
- flaskbb/auth/services/__init__.py +56 -0
- flaskbb/auth/services/activation.py +60 -0
- flaskbb/auth/services/authentication.py +188 -0
- flaskbb/auth/services/factories.py +52 -0
- flaskbb/auth/services/password.py +67 -0
- flaskbb/auth/services/reauthentication.py +87 -0
- flaskbb/auth/services/registration.py +261 -0
- flaskbb/auth/views.py +501 -0
- flaskbb/cli/__init__.py +24 -0
- flaskbb/cli/db.py +40 -0
- flaskbb/cli/main.py +705 -0
- flaskbb/cli/plugins.py +214 -0
- flaskbb/cli/themes.py +109 -0
- flaskbb/cli/translations.py +105 -0
- flaskbb/cli/users.py +113 -0
- flaskbb/cli/utils.py +203 -0
- flaskbb/configs/__init__.py +0 -0
- flaskbb/configs/config.cfg.template +281 -0
- flaskbb/configs/default.py +291 -0
- flaskbb/configs/testing.py +60 -0
- flaskbb/core/__init__.py +0 -0
- flaskbb/core/auth/__init__.py +7 -0
- flaskbb/core/auth/activation.py +60 -0
- flaskbb/core/auth/authentication.py +301 -0
- flaskbb/core/auth/password.py +54 -0
- flaskbb/core/auth/registration.py +112 -0
- flaskbb/core/changesets.py +96 -0
- flaskbb/core/exceptions.py +85 -0
- flaskbb/core/tokens.py +142 -0
- flaskbb/core/user/__init__.py +0 -0
- flaskbb/core/user/update.py +93 -0
- flaskbb/deprecation.py +106 -0
- flaskbb/display/__init__.py +0 -0
- flaskbb/display/navigation.py +109 -0
- flaskbb/email.py +89 -0
- flaskbb/exceptions.py +28 -0
- flaskbb/extensions.py +83 -0
- flaskbb/fixtures/__init__.py +0 -0
- flaskbb/fixtures/groups.py +137 -0
- flaskbb/fixtures/settings.py +325 -0
- flaskbb/forum/__init__.py +3 -0
- flaskbb/forum/forms.py +212 -0
- flaskbb/forum/locals.py +59 -0
- flaskbb/forum/models.py +1664 -0
- flaskbb/forum/utils.py +39 -0
- flaskbb/forum/views.py +1327 -0
- flaskbb/management/__init__.py +20 -0
- flaskbb/management/forms.py +516 -0
- flaskbb/management/models.py +150 -0
- flaskbb/management/plugins.py +46 -0
- flaskbb/management/views.py +1480 -0
- flaskbb/markup.py +148 -0
- flaskbb/migrations/201501082314_8ad96e49dc6_init.py +321 -0
- flaskbb/migrations/201503222157_514ca0a3282c_private_messages.py +99 -0
- flaskbb/migrations/201504082225_127be3fb000_added_m2m_forumgroups_table.py +37 -0
- flaskbb/migrations/201606061345_221d918aa9f0_add_user_authentication_infos.py +35 -0
- flaskbb/migrations/201606210939_d9530a529b3f_add_timezone_awareness_for_datetime.py +244 -0
- flaskbb/migrations/201611190919_d87cea4e995d_remove_timezone_info_from_birthday_field.py +38 -0
- flaskbb/migrations/201705041144_933bd7d807c4_add_more_non_nullables.py +192 -0
- flaskbb/migrations/201706300917_881dd22cab94_add_date_modified_to_conversations.py +50 -0
- flaskbb/migrations/201709041519_d0ffadc3ea48_add_hidden_columns.py +127 -0
- flaskbb/migrations/201711180953_7c3fcf8a3335_add_plugin_tables.py +66 -0
- flaskbb/migrations/201802021027_af3f5579c84d_add_cascades.py +483 -0
- flaskbb/migrations/201802282131_232e68a03aa2_change_emoji_shortcodes_to_characters.py +949 -0
- flaskbb/migrations/201803012138_5945d8081a95_remove_conversations.py +55 -0
- flaskbb/migrations/__init__.py +0 -0
- flaskbb/migrations/script.py.mako +24 -0
- flaskbb/plugins/__init__.py +10 -0
- flaskbb/plugins/manager.py +243 -0
- flaskbb/plugins/models.py +139 -0
- flaskbb/plugins/spec.py +1141 -0
- flaskbb/plugins/utils.py +86 -0
- flaskbb/static/app.css +19 -0
- flaskbb/static/app.css.map +1 -0
- flaskbb/static/app.js +2 -0
- flaskbb/static/app.js.map +1 -0
- flaskbb/static/avatar.svg +4 -0
- flaskbb/static/avatar100x100.png +0 -0
- flaskbb/static/avatar150x150.png +0 -0
- flaskbb/static/avatar400x400.png +0 -0
- flaskbb/static/avatar80x80.png +0 -0
- flaskbb/static/fa-regular-400.eot +0 -0
- flaskbb/static/fa-regular-400.svg +801 -0
- flaskbb/static/fa-regular-400.ttf +0 -0
- flaskbb/static/fa-regular-400.woff +0 -0
- flaskbb/static/fa-regular-400.woff2 +0 -0
- flaskbb/static/fa-solid-900.eot +0 -0
- flaskbb/static/fa-solid-900.svg +5034 -0
- flaskbb/static/fa-solid-900.ttf +0 -0
- flaskbb/static/fa-solid-900.woff +0 -0
- flaskbb/static/fa-solid-900.woff2 +0 -0
- flaskbb/static/favicon.ico +0 -0
- flaskbb/static/kuyN6Bh.jpg +0 -0
- flaskbb/static/vendors.js +3 -0
- flaskbb/static/vendors.js.LICENSE.txt +7 -0
- flaskbb/static/vendors.js.map +1 -0
- flaskbb/templates/_macros/form.html +218 -0
- flaskbb/templates/_macros/navigation.html +83 -0
- flaskbb/templates/_macros/pagination.html +109 -0
- flaskbb/templates/_macros/utils.html +11 -0
- flaskbb/templates/_partials/confirm_dialog.html +17 -0
- flaskbb/templates/_partials/editor_help.html +60 -0
- flaskbb/templates/_partials/flashed_messages.html +19 -0
- flaskbb/templates/auth/account_activation.html +22 -0
- flaskbb/templates/auth/forgot_password.html +27 -0
- flaskbb/templates/auth/login.html +35 -0
- flaskbb/templates/auth/reauth.html +22 -0
- flaskbb/templates/auth/register.html +34 -0
- flaskbb/templates/auth/request_account_activation.html +23 -0
- flaskbb/templates/auth/reset_password.html +25 -0
- flaskbb/templates/email/activate_account.html +10 -0
- flaskbb/templates/email/activate_account.txt +11 -0
- flaskbb/templates/email/reset_password.html +9 -0
- flaskbb/templates/email/reset_password.txt +11 -0
- flaskbb/templates/errors/forbidden_page.html +14 -0
- flaskbb/templates/errors/page_not_found.html +14 -0
- flaskbb/templates/errors/server_error.html +13 -0
- flaskbb/templates/errors/too_many_logins.html +14 -0
- flaskbb/templates/forum/_forum_meta.html +11 -0
- flaskbb/templates/forum/_forum_row.html +73 -0
- flaskbb/templates/forum/category.html +16 -0
- flaskbb/templates/forum/category_layout.html +124 -0
- flaskbb/templates/forum/edit_forum.html +109 -0
- flaskbb/templates/forum/forum.html +72 -0
- flaskbb/templates/forum/index.html +32 -0
- flaskbb/templates/forum/memberlist.html +62 -0
- flaskbb/templates/forum/new_post.html +49 -0
- flaskbb/templates/forum/new_topic.html +50 -0
- flaskbb/templates/forum/online_users.html +25 -0
- flaskbb/templates/forum/report_post.html +23 -0
- flaskbb/templates/forum/search_form.html +27 -0
- flaskbb/templates/forum/search_result.html +340 -0
- flaskbb/templates/forum/topic.html +244 -0
- flaskbb/templates/forum/topic_controls.html +119 -0
- flaskbb/templates/forum/topic_horizontal.html +201 -0
- flaskbb/templates/forum/topictracker.html +121 -0
- flaskbb/templates/layout.html +298 -0
- flaskbb/templates/management/banned_users.html +129 -0
- flaskbb/templates/management/category_form.html +46 -0
- flaskbb/templates/management/forum_form.html +51 -0
- flaskbb/templates/management/forums.html +191 -0
- flaskbb/templates/management/group_form.html +62 -0
- flaskbb/templates/management/groups.html +97 -0
- flaskbb/templates/management/management_layout.html +23 -0
- flaskbb/templates/management/overview.html +178 -0
- flaskbb/templates/management/plugins.html +96 -0
- flaskbb/templates/management/reports.html +116 -0
- flaskbb/templates/management/settings.html +74 -0
- flaskbb/templates/management/user_form.html +65 -0
- flaskbb/templates/management/users.html +165 -0
- flaskbb/templates/user/all_posts.html +48 -0
- flaskbb/templates/user/all_topics.html +56 -0
- flaskbb/templates/user/change_email.html +19 -0
- flaskbb/templates/user/change_password.html +19 -0
- flaskbb/templates/user/change_user_details.html +28 -0
- flaskbb/templates/user/general_settings.html +18 -0
- flaskbb/templates/user/profile.html +47 -0
- flaskbb/templates/user/profile_layout.html +124 -0
- flaskbb/templates/user/settings_layout.html +32 -0
- flaskbb/themes/aurora/Makefile +21 -0
- flaskbb/themes/aurora/README.md +41 -0
- flaskbb/themes/aurora/build_emoji_set.py +32 -0
- flaskbb/themes/aurora/info.json +11 -0
- flaskbb/themes/aurora/package-lock.json +4904 -0
- flaskbb/themes/aurora/package.json +60 -0
- flaskbb/themes/aurora/preview.png +0 -0
- flaskbb/themes/aurora/src/app/confirm_modal.js +45 -0
- flaskbb/themes/aurora/src/app/editor.js +142 -0
- flaskbb/themes/aurora/src/app/emoji.js +3515 -0
- flaskbb/themes/aurora/src/app/flaskbb.js +271 -0
- flaskbb/themes/aurora/src/app/utils.js +11 -0
- flaskbb/themes/aurora/src/app.js +26 -0
- flaskbb/themes/aurora/src/assets/avatar.svg +4 -0
- flaskbb/themes/aurora/src/assets/avatar100x100.png +0 -0
- flaskbb/themes/aurora/src/assets/avatar150x150.png +0 -0
- flaskbb/themes/aurora/src/assets/avatar400x400.png +0 -0
- flaskbb/themes/aurora/src/assets/avatar80x80.png +0 -0
- flaskbb/themes/aurora/src/assets/favicon.ico +0 -0
- flaskbb/themes/aurora/src/scss/_button.scss +39 -0
- flaskbb/themes/aurora/src/scss/_category.scss +67 -0
- flaskbb/themes/aurora/src/scss/_forum.scss +56 -0
- flaskbb/themes/aurora/src/scss/_management.scss +205 -0
- flaskbb/themes/aurora/src/scss/_misc.scss +181 -0
- flaskbb/themes/aurora/src/scss/_navigation.scss +130 -0
- flaskbb/themes/aurora/src/scss/_page.scss +51 -0
- flaskbb/themes/aurora/src/scss/_profile.scss +121 -0
- flaskbb/themes/aurora/src/scss/_pygments.scss +65 -0
- flaskbb/themes/aurora/src/scss/_text.scss +43 -0
- flaskbb/themes/aurora/src/scss/_topic.scss +172 -0
- flaskbb/themes/aurora/src/scss/_variables.scss +122 -0
- flaskbb/themes/aurora/src/scss/styles.scss +26 -0
- flaskbb/themes/aurora/tsconfig.json +24 -0
- flaskbb/themes/aurora/webpack.common.js +132 -0
- flaskbb/themes/aurora/webpack.dev.js +7 -0
- flaskbb/themes/aurora/webpack.prod.js +7 -0
- flaskbb/tokens/__init__.py +12 -0
- flaskbb/tokens/serializer.py +93 -0
- flaskbb/tokens/verifiers.py +34 -0
- flaskbb/translations/da/LC_MESSAGES/messages.po +3324 -0
- flaskbb/translations/de/LC_MESSAGES/messages.po +3359 -0
- flaskbb/translations/en/LC_MESSAGES/messages.po +4126 -0
- flaskbb/translations/es/LC_MESSAGES/messages.po +3363 -0
- flaskbb/translations/fr/LC_MESSAGES/messages.po +3398 -0
- flaskbb/translations/nb_NO/LC_MESSAGES/messages.po +3493 -0
- flaskbb/translations/pl/LC_MESSAGES/messages.po +3350 -0
- flaskbb/translations/pt/LC_MESSAGES/messages.po +3488 -0
- flaskbb/translations/pt_BR/LC_MESSAGES/messages.po +3350 -0
- flaskbb/translations/ru/LC_MESSAGES/messages.po +3335 -0
- flaskbb/translations/sv_SE/LC_MESSAGES/messages.po +3347 -0
- flaskbb/translations/ta/LC_MESSAGES/messages.po +3528 -0
- flaskbb/translations/uk/LC_MESSAGES/messages.po +3505 -0
- flaskbb/translations/zh_CN/LC_MESSAGES/messages.po +3325 -0
- flaskbb/translations/zh_TW/LC_MESSAGES/messages.po +3324 -0
- flaskbb/user/__init__.py +19 -0
- flaskbb/user/forms.py +131 -0
- flaskbb/user/models.py +527 -0
- flaskbb/user/plugins.py +87 -0
- flaskbb/user/services/__init__.py +0 -0
- flaskbb/user/services/factories.py +89 -0
- flaskbb/user/services/update.py +88 -0
- flaskbb/user/services/validators.py +107 -0
- flaskbb/user/views.py +235 -0
- flaskbb/utils/__init__.py +3 -0
- flaskbb/utils/alembic.py +79 -0
- flaskbb/utils/database.py +189 -0
- flaskbb/utils/datastructures.py +31 -0
- flaskbb/utils/forms.py +209 -0
- flaskbb/utils/helpers.py +940 -0
- flaskbb/utils/http.py +81 -0
- flaskbb/utils/populate.py +445 -0
- flaskbb/utils/queries.py +154 -0
- flaskbb/utils/requirements.py +352 -0
- flaskbb/utils/search.py +143 -0
- flaskbb/utils/settings.py +55 -0
- flaskbb/utils/translations.py +229 -0
- flaskbb-2.2.0.dist-info/METADATA +120 -0
- flaskbb-2.2.0.dist-info/RECORD +247 -0
- flaskbb-2.2.0.dist-info/WHEEL +4 -0
- flaskbb-2.2.0.dist-info/entry_points.txt +2 -0
- flaskbb-2.2.0.dist-info/licenses/LICENSE +33 -0
- wsgi.py +11 -0
flaskbb/auth/forms.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
flaskbb.auth.forms
|
|
4
|
+
~~~~~~~~~~~~~~~~~~
|
|
5
|
+
|
|
6
|
+
It provides the forms that are needed for the auth views.
|
|
7
|
+
|
|
8
|
+
:copyright: (c) 2014 by the FlaskBB Team.
|
|
9
|
+
:license: BSD, see LICENSE for more details.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
|
|
14
|
+
from flask_babelplus import lazy_gettext as _
|
|
15
|
+
from wtforms import (
|
|
16
|
+
BooleanField,
|
|
17
|
+
HiddenField,
|
|
18
|
+
PasswordField,
|
|
19
|
+
SelectField,
|
|
20
|
+
StringField,
|
|
21
|
+
SubmitField,
|
|
22
|
+
)
|
|
23
|
+
from wtforms.validators import (
|
|
24
|
+
DataRequired,
|
|
25
|
+
Email,
|
|
26
|
+
EqualTo,
|
|
27
|
+
InputRequired,
|
|
28
|
+
regexp,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
from flaskbb.utils.forms import FlaskBBForm, add_recaptcha_field
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
USERNAME_RE = r"^[\w.+-]+$"
|
|
36
|
+
is_valid_username = regexp(
|
|
37
|
+
USERNAME_RE, message=_("You can only use letters, numbers or dashes.")
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@add_recaptcha_field()
|
|
42
|
+
class LoginForm(FlaskBBForm):
|
|
43
|
+
login = StringField(
|
|
44
|
+
_("Username or Email address"),
|
|
45
|
+
validators=[
|
|
46
|
+
DataRequired(message=_("Please enter your username or email address."))
|
|
47
|
+
],
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
password = PasswordField(
|
|
51
|
+
_("Password"),
|
|
52
|
+
validators=[DataRequired(message=_("Please enter your password."))],
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
remember_me = BooleanField(_("Remember me"), default=False)
|
|
56
|
+
|
|
57
|
+
submit = SubmitField(_("Login"))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@add_recaptcha_field(only_on_ratelimit=True)
|
|
61
|
+
class RegisterForm(FlaskBBForm):
|
|
62
|
+
username = StringField(
|
|
63
|
+
_("Username"),
|
|
64
|
+
validators=[
|
|
65
|
+
DataRequired(message=_("A valid username is required")),
|
|
66
|
+
is_valid_username,
|
|
67
|
+
],
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
email = StringField(
|
|
71
|
+
_("Email address"),
|
|
72
|
+
validators=[
|
|
73
|
+
DataRequired(message=_("A valid email address is required.")),
|
|
74
|
+
Email(message=_("Invalid email address.")),
|
|
75
|
+
],
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
password = PasswordField(
|
|
79
|
+
_("Password"),
|
|
80
|
+
validators=[
|
|
81
|
+
InputRequired(),
|
|
82
|
+
EqualTo("confirm_password", message=_("Passwords must match.")),
|
|
83
|
+
],
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
confirm_password = PasswordField(_("Confirm password"))
|
|
87
|
+
|
|
88
|
+
language = SelectField(_("Language"))
|
|
89
|
+
|
|
90
|
+
accept_tos = BooleanField(
|
|
91
|
+
_("I accept the Terms of Service"),
|
|
92
|
+
validators=[DataRequired(message=_("Please accept the TOS."))],
|
|
93
|
+
default=True,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
submit = SubmitField(_("Register"))
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class ReauthForm(FlaskBBForm):
|
|
100
|
+
password = PasswordField(
|
|
101
|
+
_("Password"),
|
|
102
|
+
validators=[DataRequired(message=_("Please enter your password."))],
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
submit = SubmitField(_("Refresh Login"))
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@add_recaptcha_field()
|
|
109
|
+
class ForgotPasswordForm(FlaskBBForm):
|
|
110
|
+
email = StringField(
|
|
111
|
+
_("Email address"),
|
|
112
|
+
validators=[
|
|
113
|
+
DataRequired(message=_("A valid email address is required.")),
|
|
114
|
+
Email(),
|
|
115
|
+
],
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
submit = SubmitField(_("Request Password"))
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class ResetPasswordForm(FlaskBBForm):
|
|
122
|
+
token = HiddenField("Token")
|
|
123
|
+
|
|
124
|
+
email = StringField(
|
|
125
|
+
_("Email address"),
|
|
126
|
+
validators=[
|
|
127
|
+
DataRequired(message=_("A valid email address is required.")),
|
|
128
|
+
Email(),
|
|
129
|
+
],
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
password = PasswordField(
|
|
133
|
+
_("Password"),
|
|
134
|
+
validators=[
|
|
135
|
+
InputRequired(),
|
|
136
|
+
EqualTo("confirm_password", message=_("Passwords must match.")),
|
|
137
|
+
],
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
confirm_password = PasswordField(_("Confirm password"))
|
|
141
|
+
|
|
142
|
+
submit = SubmitField(_("Reset password"))
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class RequestActivationForm(FlaskBBForm):
|
|
146
|
+
username = StringField(
|
|
147
|
+
_("Username"),
|
|
148
|
+
validators=[
|
|
149
|
+
DataRequired(message=_("A valid username is required.")),
|
|
150
|
+
is_valid_username,
|
|
151
|
+
],
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
email = StringField(
|
|
155
|
+
_("Email address"),
|
|
156
|
+
validators=[
|
|
157
|
+
DataRequired(message=_("A valid email address is required.")),
|
|
158
|
+
Email(message=_("Invalid email address.")),
|
|
159
|
+
],
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
submit = SubmitField(_("Send Confirmation Mail"))
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class AccountActivationForm(FlaskBBForm):
|
|
166
|
+
token = StringField(
|
|
167
|
+
_("Email confirmation token"),
|
|
168
|
+
validators=[DataRequired(_("Please enter the token we have sent to you."))],
|
|
169
|
+
)
|
|
170
|
+
submit = SubmitField(_("Confirm Email"))
|
flaskbb/auth/plugins.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
flaskbb.auth.plugins
|
|
4
|
+
~~~~~~~~~~~~~~~~~~~~
|
|
5
|
+
Plugin implementations for FlaskBB auth hooks
|
|
6
|
+
|
|
7
|
+
:copyright: (c) 2014-2018 the FlaskBB Team.
|
|
8
|
+
:license: BSD, see LICENSE for more details
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from flask import flash, redirect, url_for
|
|
12
|
+
from flask_login import current_user, logout_user
|
|
13
|
+
|
|
14
|
+
from ..core.auth.authentication import ForceLogout
|
|
15
|
+
from ..extensions import db
|
|
16
|
+
from ..user.models import User
|
|
17
|
+
from ..utils.settings import flaskbb_config
|
|
18
|
+
from . import impl
|
|
19
|
+
from .services.authentication import (
|
|
20
|
+
BlockUnactivatedUser,
|
|
21
|
+
ClearFailedLogins,
|
|
22
|
+
DefaultFlaskBBAuthProvider,
|
|
23
|
+
MarkFailedLogin,
|
|
24
|
+
)
|
|
25
|
+
from .services.factories import account_activator_factory
|
|
26
|
+
from .services.reauthentication import (
|
|
27
|
+
ClearFailedLoginsOnReauth,
|
|
28
|
+
DefaultFlaskBBReauthProvider,
|
|
29
|
+
MarkFailedReauth,
|
|
30
|
+
)
|
|
31
|
+
from .services.registration import (
|
|
32
|
+
AutoActivateUserPostProcessor,
|
|
33
|
+
AutologinPostProcessor,
|
|
34
|
+
EmailUniquenessValidator,
|
|
35
|
+
SendActivationPostProcessor,
|
|
36
|
+
UsernameRequirements,
|
|
37
|
+
UsernameUniquenessValidator,
|
|
38
|
+
UsernameValidator,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@impl(trylast=True)
|
|
43
|
+
def flaskbb_authenticate(identifier, secret):
|
|
44
|
+
return DefaultFlaskBBAuthProvider().authenticate(identifier, secret)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@impl(tryfirst=True)
|
|
48
|
+
def flaskbb_post_authenticate(user):
|
|
49
|
+
handlers = [ClearFailedLogins()]
|
|
50
|
+
|
|
51
|
+
if flaskbb_config["ACTIVATE_ACCOUNT"]:
|
|
52
|
+
handlers.append(BlockUnactivatedUser())
|
|
53
|
+
|
|
54
|
+
for handler in handlers:
|
|
55
|
+
handler.handle_post_auth(user)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@impl
|
|
59
|
+
def flaskbb_authentication_failed(identifier):
|
|
60
|
+
MarkFailedLogin().handle_authentication_failure(identifier)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@impl(trylast=True)
|
|
64
|
+
def flaskbb_reauth_attempt(user, secret):
|
|
65
|
+
return DefaultFlaskBBReauthProvider().reauthenticate(user, secret)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@impl
|
|
69
|
+
def flaskbb_reauth_failed(user):
|
|
70
|
+
MarkFailedReauth().handle_reauth_failure(user)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@impl
|
|
74
|
+
def flaskbb_post_reauth(user):
|
|
75
|
+
ClearFailedLoginsOnReauth().handle_post_reauth(user)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@impl
|
|
79
|
+
def flaskbb_errorhandlers(app):
|
|
80
|
+
@app.errorhandler(ForceLogout)
|
|
81
|
+
def handle_force_logout(error):
|
|
82
|
+
if current_user:
|
|
83
|
+
logout_user()
|
|
84
|
+
if error.reason:
|
|
85
|
+
flash(error.reason, "danger")
|
|
86
|
+
return redirect(url_for("forum.index"))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@impl
|
|
90
|
+
def flaskbb_gather_registration_validators():
|
|
91
|
+
blacklist = [
|
|
92
|
+
w.strip() for w in flaskbb_config["AUTH_USERNAME_BLACKLIST"].split(",")
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
requirements = UsernameRequirements(
|
|
96
|
+
min=flaskbb_config["AUTH_USERNAME_MIN_LENGTH"],
|
|
97
|
+
max=flaskbb_config["AUTH_USERNAME_MAX_LENGTH"],
|
|
98
|
+
blacklist=blacklist,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return [
|
|
102
|
+
EmailUniquenessValidator(User),
|
|
103
|
+
UsernameUniquenessValidator(User),
|
|
104
|
+
UsernameValidator(requirements),
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@impl
|
|
109
|
+
def flaskbb_registration_post_processor(user):
|
|
110
|
+
handlers = []
|
|
111
|
+
|
|
112
|
+
if flaskbb_config["ACTIVATE_ACCOUNT"]:
|
|
113
|
+
handlers.append(SendActivationPostProcessor(account_activator_factory()))
|
|
114
|
+
else:
|
|
115
|
+
handlers.append(AutologinPostProcessor())
|
|
116
|
+
handlers.append(AutoActivateUserPostProcessor(db, flaskbb_config))
|
|
117
|
+
|
|
118
|
+
for handler in handlers:
|
|
119
|
+
handler.post_process(user)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
flaskbb.auth.services
|
|
4
|
+
~~~~~~~~~~~~~~~~~~~~~
|
|
5
|
+
Public module of implemenations of auth related services
|
|
6
|
+
in FlaskBB. If you are developing a plugin or extending
|
|
7
|
+
FlaskBB, you should import from this module rather than
|
|
8
|
+
submodules.
|
|
9
|
+
|
|
10
|
+
:copyright: (c) 2014-2018 the FlaskBB Team.
|
|
11
|
+
:license: BSD, see LICENSE for more details
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from .activation import AccountActivator
|
|
15
|
+
from .authentication import (
|
|
16
|
+
BlockTooManyFailedLogins,
|
|
17
|
+
BlockUnactivatedUser,
|
|
18
|
+
DefaultFlaskBBAuthProvider,
|
|
19
|
+
FailedLoginConfiguration,
|
|
20
|
+
MarkFailedLogin,
|
|
21
|
+
PluginAuthenticationManager,
|
|
22
|
+
)
|
|
23
|
+
from .factories import (
|
|
24
|
+
account_activator_factory,
|
|
25
|
+
authentication_manager_factory,
|
|
26
|
+
reauthentication_manager_factory,
|
|
27
|
+
registration_service_factory,
|
|
28
|
+
reset_service_factory,
|
|
29
|
+
)
|
|
30
|
+
from .password import ResetPasswordService
|
|
31
|
+
from .registration import (
|
|
32
|
+
EmailUniquenessValidator,
|
|
33
|
+
UsernameRequirements,
|
|
34
|
+
UsernameUniquenessValidator,
|
|
35
|
+
UsernameValidator,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
__all__ = (
|
|
39
|
+
"AccountActivator",
|
|
40
|
+
"account_activator_factory",
|
|
41
|
+
"authentication_manager_factory",
|
|
42
|
+
"BlockTooManyFailedLogins",
|
|
43
|
+
"BlockUnactivatedUser",
|
|
44
|
+
"DefaultFlaskBBAuthProvider",
|
|
45
|
+
"EmailUniquenessValidator",
|
|
46
|
+
"FailedLoginConfiguration",
|
|
47
|
+
"MarkFailedLogin",
|
|
48
|
+
"PluginAuthenticationManager",
|
|
49
|
+
"reauthentication_manager_factory",
|
|
50
|
+
"registration_service_factory",
|
|
51
|
+
"ResetPasswordService",
|
|
52
|
+
"reset_service_factory",
|
|
53
|
+
"UsernameRequirements",
|
|
54
|
+
"UsernameUniquenessValidator",
|
|
55
|
+
"UsernameValidator",
|
|
56
|
+
)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
flaskbb.auth.services.activation
|
|
4
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
5
|
+
Handlers for activating accounts in FlaskBB
|
|
6
|
+
|
|
7
|
+
:copyright: (c) 2014-2018 the FlaskBB Team
|
|
8
|
+
:license: BSD, see LICENSE for more details
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from flask_babelplus import gettext as _
|
|
12
|
+
|
|
13
|
+
from ...core.auth.activation import AccountActivator as _AccountActivator
|
|
14
|
+
from ...core.exceptions import ValidationError
|
|
15
|
+
from ...core.tokens import Token, TokenActions, TokenError
|
|
16
|
+
from ...email import send_activation_token
|
|
17
|
+
from ...extensions import db
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AccountActivator(_AccountActivator):
|
|
21
|
+
"""
|
|
22
|
+
Default account activator for FlaskBB, handles the activation
|
|
23
|
+
process through email.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, token_serializer, users):
|
|
27
|
+
self.token_serializer = token_serializer
|
|
28
|
+
self.users = users
|
|
29
|
+
|
|
30
|
+
def initiate_account_activation(self, email: str):
|
|
31
|
+
user = db.session.execute(
|
|
32
|
+
db.select(self.users).filter_by(email=email)
|
|
33
|
+
).scalar_one_or_none()
|
|
34
|
+
|
|
35
|
+
if user is None:
|
|
36
|
+
raise ValidationError("email", _("Entered email doesn't exist"))
|
|
37
|
+
|
|
38
|
+
if user.activated:
|
|
39
|
+
raise ValidationError("email", _("Account is already activated"))
|
|
40
|
+
|
|
41
|
+
token = self.token_serializer.dumps(
|
|
42
|
+
Token(user_id=user.id, operation=TokenActions.ACTIVATE_ACCOUNT)
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
send_activation_token.delay(
|
|
46
|
+
token=token, username=user.username, email=user.email
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def activate_account(self, token):
|
|
50
|
+
token = self.token_serializer.loads(token)
|
|
51
|
+
if token.operation != TokenActions.ACTIVATE_ACCOUNT:
|
|
52
|
+
raise TokenError.invalid()
|
|
53
|
+
user = db.session.execute(
|
|
54
|
+
db.select(self.users).filter_by(id=token.user_id)
|
|
55
|
+
).scalar_one_or_none()
|
|
56
|
+
if user is None:
|
|
57
|
+
raise TokenError.invalid()
|
|
58
|
+
if user.activated:
|
|
59
|
+
raise ValidationError("activated", _("Account is already activated"))
|
|
60
|
+
user.activated = True
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
flaskbb.auth.services.authentication
|
|
4
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
5
|
+
|
|
6
|
+
Authentication providers, handlers and post-processors
|
|
7
|
+
in FlaskBB
|
|
8
|
+
|
|
9
|
+
:copyright: (c) 2014-2018 the FlaskBB Team.
|
|
10
|
+
:license: BSD, see LICENSE for more details
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from typing import TYPE_CHECKING
|
|
16
|
+
|
|
17
|
+
import attr
|
|
18
|
+
from flask_babelplus import gettext as _
|
|
19
|
+
from pytz import UTC
|
|
20
|
+
from werkzeug.security import check_password_hash
|
|
21
|
+
|
|
22
|
+
from ...core.auth.authentication import (
|
|
23
|
+
AuthenticationFailureHandler,
|
|
24
|
+
AuthenticationManager,
|
|
25
|
+
AuthenticationProvider,
|
|
26
|
+
PostAuthenticationHandler,
|
|
27
|
+
StopAuthentication,
|
|
28
|
+
)
|
|
29
|
+
from ...extensions import db
|
|
30
|
+
from ...user.models import User
|
|
31
|
+
from ...utils.helpers import time_utcnow
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from flask_sqlalchemy.session import Session
|
|
35
|
+
|
|
36
|
+
from flaskbb.plugins.manager import FlaskBBPluginManager
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger(__name__)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@attr.s(frozen=True)
|
|
42
|
+
class FailedLoginConfiguration(object):
|
|
43
|
+
"""
|
|
44
|
+
Used to configure how many failed logins are accepted until an account
|
|
45
|
+
is temporarily locked out and how long to temporarily lock the account
|
|
46
|
+
out for.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
limit = attr.ib()
|
|
50
|
+
lockout_window = attr.ib()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class BlockTooManyFailedLogins(AuthenticationProvider):
|
|
54
|
+
"""
|
|
55
|
+
Pre authentication check to block a login from an account that has too
|
|
56
|
+
many failed login attempts in place.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(self, configuration):
|
|
60
|
+
self.configuration = configuration
|
|
61
|
+
|
|
62
|
+
def authenticate(self, identifier, secret):
|
|
63
|
+
user = db.session.execute(
|
|
64
|
+
db.select(User).filter(
|
|
65
|
+
db.or_(User.username == identifier, User.email == identifier)
|
|
66
|
+
)
|
|
67
|
+
).scalar_one_or_none()
|
|
68
|
+
|
|
69
|
+
if user is not None:
|
|
70
|
+
attempts = user.login_attempts
|
|
71
|
+
last_attempt = user.last_failed_login or datetime.min.replace(tzinfo=UTC)
|
|
72
|
+
reached_attempt_limit = attempts >= self.configuration.limit
|
|
73
|
+
inside_lockout = (
|
|
74
|
+
last_attempt + self.configuration.lockout_window
|
|
75
|
+
) >= time_utcnow()
|
|
76
|
+
|
|
77
|
+
if reached_attempt_limit and inside_lockout:
|
|
78
|
+
raise StopAuthentication(
|
|
79
|
+
_(
|
|
80
|
+
"Your account is currently locked out due to too many "
|
|
81
|
+
"failed login attempts"
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class DefaultFlaskBBAuthProvider(AuthenticationProvider):
|
|
87
|
+
"""
|
|
88
|
+
This is the default username/email and password authentication checker,
|
|
89
|
+
locates the user based on the identifer passed -- either username or email
|
|
90
|
+
-- and compares the supplied password to the hash connected to the matching
|
|
91
|
+
user (if any).
|
|
92
|
+
|
|
93
|
+
Offers protection against timing attacks that would rely on the difference
|
|
94
|
+
in response time from not matching a password hash.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
def authenticate(self, identifier: str, secret: str):
|
|
98
|
+
user = db.session.execute(
|
|
99
|
+
db.select(User).filter(
|
|
100
|
+
db.or_(User.username == identifier, User.email == identifier)
|
|
101
|
+
)
|
|
102
|
+
).scalar_one_or_none()
|
|
103
|
+
|
|
104
|
+
if user is not None:
|
|
105
|
+
if check_password_hash(user.password, secret):
|
|
106
|
+
return user
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
check_password_hash("dummy password", secret)
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class MarkFailedLogin(AuthenticationFailureHandler):
|
|
114
|
+
"""
|
|
115
|
+
Failure handler that marks the login attempt on the user and sets the
|
|
116
|
+
last failed date when it happened.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
def handle_authentication_failure(self, identifier: str):
|
|
120
|
+
user = db.session.execute(
|
|
121
|
+
db.select(User).filter(
|
|
122
|
+
db.or_(User.username == identifier, User.email == identifier)
|
|
123
|
+
)
|
|
124
|
+
).scalar_one_or_none()
|
|
125
|
+
|
|
126
|
+
if user is not None:
|
|
127
|
+
user.login_attempts += 1
|
|
128
|
+
user.last_failed_login = time_utcnow()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class BlockUnactivatedUser(PostAuthenticationHandler):
|
|
132
|
+
"""
|
|
133
|
+
Post auth handler that will block a user that has managed to pass the
|
|
134
|
+
authentication check but has not actually activated their account yet.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
def handle_post_auth(self, user: "User"):
|
|
138
|
+
if not user.activated: # pragma: no branch
|
|
139
|
+
raise StopAuthentication(
|
|
140
|
+
_(
|
|
141
|
+
"In order to use your account you have to "
|
|
142
|
+
"activate it through the link we have sent to "
|
|
143
|
+
"your email address."
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class ClearFailedLogins(PostAuthenticationHandler):
|
|
149
|
+
"""
|
|
150
|
+
Post auth handler that clears all failed login attempts from a user's
|
|
151
|
+
account.
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
def handle_post_auth(self, user: "User"):
|
|
155
|
+
user.login_attempts = 0
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class PluginAuthenticationManager(AuthenticationManager):
|
|
159
|
+
"""
|
|
160
|
+
Authentication manager relying on plugin hooks to manage the authentication
|
|
161
|
+
process. This is the default authentication manager for FlaskBB.
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
def __init__(self, plugin_manager: "FlaskBBPluginManager", session: Session):
|
|
165
|
+
self.plugin_manager = plugin_manager
|
|
166
|
+
self.session = session
|
|
167
|
+
|
|
168
|
+
def authenticate(self, identifier: str, secret: str):
|
|
169
|
+
try:
|
|
170
|
+
user = self.plugin_manager.hook.flaskbb_authenticate(
|
|
171
|
+
identifier=identifier, secret=secret
|
|
172
|
+
)
|
|
173
|
+
if user is None:
|
|
174
|
+
raise StopAuthentication(_("Wrong username or password."))
|
|
175
|
+
self.plugin_manager.hook.flaskbb_post_authenticate(user=user)
|
|
176
|
+
return user
|
|
177
|
+
except StopAuthentication:
|
|
178
|
+
self.plugin_manager.hook.flaskbb_authentication_failed(
|
|
179
|
+
identifier=identifier
|
|
180
|
+
)
|
|
181
|
+
raise
|
|
182
|
+
finally:
|
|
183
|
+
try:
|
|
184
|
+
self.session.commit()
|
|
185
|
+
except Exception:
|
|
186
|
+
logger.exception("Exception while processing login")
|
|
187
|
+
self.session.rollback()
|
|
188
|
+
raise
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
flaskbb.auth.services.factories
|
|
4
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
5
|
+
Factory functions for various FlaskBB auth services
|
|
6
|
+
|
|
7
|
+
These factories are provisional.
|
|
8
|
+
|
|
9
|
+
:copyright: 2014-2018 the FlaskBB Team.
|
|
10
|
+
:license: BSD, see LICENSE for more details
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from datetime import timedelta
|
|
14
|
+
|
|
15
|
+
from flask import current_app
|
|
16
|
+
|
|
17
|
+
from ...extensions import db, pluggy
|
|
18
|
+
from ...tokens import FlaskBBTokenSerializer
|
|
19
|
+
from ...tokens.verifiers import EmailMatchesUserToken
|
|
20
|
+
from ...user.models import User
|
|
21
|
+
from .activation import AccountActivator
|
|
22
|
+
from .authentication import PluginAuthenticationManager
|
|
23
|
+
from .password import ResetPasswordService
|
|
24
|
+
from .reauthentication import PluginReauthenticationManager
|
|
25
|
+
from .registration import RegistrationService
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def registration_service_factory():
|
|
29
|
+
return RegistrationService(pluggy, User, db)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def reset_service_factory():
|
|
33
|
+
token_serializer = FlaskBBTokenSerializer(
|
|
34
|
+
current_app.config["SECRET_KEY"], expiry=timedelta(hours=1)
|
|
35
|
+
)
|
|
36
|
+
verifiers = [EmailMatchesUserToken(User)]
|
|
37
|
+
return ResetPasswordService(token_serializer, User, token_verifiers=verifiers)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def account_activator_factory():
|
|
41
|
+
token_serializer = FlaskBBTokenSerializer(
|
|
42
|
+
current_app.config["SECRET_KEY"], expiry=timedelta(hours=1)
|
|
43
|
+
)
|
|
44
|
+
return AccountActivator(token_serializer, User)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def authentication_manager_factory():
|
|
48
|
+
return PluginAuthenticationManager(pluggy, db.session)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def reauthentication_manager_factory():
|
|
52
|
+
return PluginReauthenticationManager(pluggy, db.session)
|