django-pfx 1.4.dev18__tar.gz → 1.4.dev24__tar.gz
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.
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/PKG-INFO +1 -1
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/django_pfx.egg-info/PKG-INFO +1 -1
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/django_pfx.egg-info/SOURCES.txt +4 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/default_settings.py +5 -0
- django-pfx-1.4.dev24/pfx/pfxcore/locale/fr/LC_MESSAGES/django.mo +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/locale/fr/LC_MESSAGES/django.po +89 -24
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/middleware/authentication.py +13 -9
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/models/__init__.py +2 -0
- django-pfx-1.4.dev24/pfx/pfxcore/models/abstract_pfx_base_user.py +15 -0
- django-pfx-1.4.dev24/pfx/pfxcore/models/login_ban.py +60 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/models/otp_user_mixin.py +31 -2
- django-pfx-1.4.dev24/pfx/pfxcore/templates/registration/otp_code_email.txt +12 -0
- django-pfx-1.4.dev24/pfx/pfxcore/templates/registration/otp_code_subject.txt +3 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/urls.py +2 -1
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/views/__init__.py +1 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/views/authentication_views.py +153 -12
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/models.py +3 -2
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/tests/test_auth_api.py +332 -46
- django-pfx-1.4.dev18/pfx/pfxcore/locale/fr/LC_MESSAGES/django.mo +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/.gitignore +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/.gitlab-ci.yml +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/.pre-commit-config.yaml +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/LICENSE +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/MANIFEST.in +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/README.md +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/django-admin-test +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/django_pfx.egg-info/dependency_links.txt +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/django_pfx.egg-info/requires.txt +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/django_pfx.egg-info/top_level.txt +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/doc/Makefile +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/doc/conf.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/doc/index.rst +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/doc/source/api.views.rst +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/doc/source/authentication.md +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/doc/source/decorator.md +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/doc/source/generate_openapi.md +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/doc/source/getting_started.md +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/doc/source/internationalisation.md +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/doc/source/model.md +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/doc/source/pfx_views.md +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/doc/source/profiling.md +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/doc/source/settings.md +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/doc/source/testing.md +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/img/pfx.png +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/img/pfx.svg +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/__init__.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/__init__.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/apidoc/__init__.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/apidoc/parameters.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/apidoc/schema.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/apidoc/tags.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/apps.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/decorator/__init__.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/decorator/rest.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/exceptions.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/fields.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/http/__init__.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/http/json_response.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/management/__init__.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/management/commands/__init__.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/management/commands/makeapidoc.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/management/commands/profile.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/middleware/__init__.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/middleware/locale.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/middleware/profiling.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/models/cache_mixins.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/models/not_null_fields.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/models/pfx_models.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/models/user_filtered_queryset_mixin.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/serializers/__init__.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/serializers/json.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/settings.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/shortcuts.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/storage/__init__.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/storage/s3_storage.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/templates/registration/password_reset_email.txt +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/templates/registration/password_reset_subject.txt +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/templates/registration/welcome_email.txt +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/templates/registration/welcome_subject.txt +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/test.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/views/fields.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/views/filters_views.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/views/locale_views.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/__init__.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/date_format.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/groups.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/list_count.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/list_items.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/list_mode.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/list_order.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/list_search.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/media_redirect.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/meta_fields.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/meta_filters.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/meta_orders.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/subset.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/subset_limit.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/subset_offset.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/subset_page.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/subset_page_size.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/subset_page_subset.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/views/rest_views.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pyproject.toml +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/requirements.txt +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/runtest.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/serve-doc +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/setup.cfg +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/setup.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/__init__.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/locale/fr/LC_MESSAGES/django.po +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/settings/__init__.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/settings/ci.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/settings/common.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/settings/dev.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/settings/dev_custom_example.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/settings/dev_default.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/tests/__init__.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/tests/basic_api_errors.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/tests/basic_api_test.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/tests/test_api_doc.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/tests/test_body_mixin.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/tests/test_cache.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/tests/test_client.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/tests/test_fields.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/tests/test_filters.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/tests/test_locale_api.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/tests/test_perm_tests.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/tests/test_perms_api.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/tests/test_profiling_middleware.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/tests/test_shortcuts.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/tests/test_timezone_middleware.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/tests/test_tools.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/tests/test_user_queryset.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/tests/test_view_decorators.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/tests/test_view_fields.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/urls.py +0 -0
- {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/views.py +0 -0
|
@@ -61,7 +61,9 @@ pfx/pfxcore/middleware/authentication.py
|
|
|
61
61
|
pfx/pfxcore/middleware/locale.py
|
|
62
62
|
pfx/pfxcore/middleware/profiling.py
|
|
63
63
|
pfx/pfxcore/models/__init__.py
|
|
64
|
+
pfx/pfxcore/models/abstract_pfx_base_user.py
|
|
64
65
|
pfx/pfxcore/models/cache_mixins.py
|
|
66
|
+
pfx/pfxcore/models/login_ban.py
|
|
65
67
|
pfx/pfxcore/models/not_null_fields.py
|
|
66
68
|
pfx/pfxcore/models/otp_user_mixin.py
|
|
67
69
|
pfx/pfxcore/models/pfx_models.py
|
|
@@ -70,6 +72,8 @@ pfx/pfxcore/serializers/__init__.py
|
|
|
70
72
|
pfx/pfxcore/serializers/json.py
|
|
71
73
|
pfx/pfxcore/storage/__init__.py
|
|
72
74
|
pfx/pfxcore/storage/s3_storage.py
|
|
75
|
+
pfx/pfxcore/templates/registration/otp_code_email.txt
|
|
76
|
+
pfx/pfxcore/templates/registration/otp_code_subject.txt
|
|
73
77
|
pfx/pfxcore/templates/registration/password_reset_email.txt
|
|
74
78
|
pfx/pfxcore/templates/registration/password_reset_subject.txt
|
|
75
79
|
pfx/pfxcore/templates/registration/welcome_email.txt
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
PFX_TOKEN_SHORT_VALIDITY = {'hours': 12}
|
|
2
2
|
PFX_TOKEN_LONG_VALIDITY = {'days': 30}
|
|
3
3
|
PFX_TOKEN_OTP_VALIDITY = {'minutes': 15}
|
|
4
|
+
PFX_HOTP_CODE_VALIDITY = 15
|
|
4
5
|
|
|
5
6
|
PFX_COOKIE_DOMAIN = None
|
|
6
7
|
PFX_COOKIE_SECURE = True
|
|
7
8
|
PFX_COOKIE_SAMESITE = 'None'
|
|
8
9
|
|
|
10
|
+
PFX_LOGIN_BAN_FAILED_NUMBER = 5
|
|
11
|
+
PFX_LOGIN_BAN_SECONDS_START = 60
|
|
12
|
+
PFX_LOGIN_BAN_SECONDS_STEP = 60
|
|
13
|
+
|
|
9
14
|
PFX_OPENAPI_PATH = "doc/api/"
|
|
10
15
|
PFX_OPENAPI_FILENAME = "openapi"
|
|
11
16
|
PFX_OPENAPI_TEMPLATE = {}
|
|
Binary file
|
|
@@ -7,7 +7,7 @@ msgid ""
|
|
|
7
7
|
msgstr ""
|
|
8
8
|
"Project-Id-Version: \n"
|
|
9
9
|
"Report-Msgid-Bugs-To: \n"
|
|
10
|
-
"POT-Creation-Date: 2024-
|
|
10
|
+
"POT-Creation-Date: 2024-04-08 13:51+0200\n"
|
|
11
11
|
"PO-Revision-Date: 2021-06-22 23:31+0200\n"
|
|
12
12
|
"Last-Translator: \n"
|
|
13
13
|
"Language-Team: \n"
|
|
@@ -59,14 +59,42 @@ msgstr ""
|
|
|
59
59
|
"Format non valide, il peut s’agir d’un nombre en heures, « 1:05 », « :05 », "
|
|
60
60
|
"« 1h 5m », « 1.5h » ou « 30m »."
|
|
61
61
|
|
|
62
|
-
#: models/
|
|
62
|
+
#: models/login_ban.py:45
|
|
63
|
+
msgid "Username"
|
|
64
|
+
msgstr "Nom d’utilisateur"
|
|
65
|
+
|
|
66
|
+
#: models/login_ban.py:46
|
|
67
|
+
msgid "Failed counter"
|
|
68
|
+
msgstr "Compteur d’échec"
|
|
69
|
+
|
|
70
|
+
#: models/login_ban.py:47
|
|
71
|
+
msgid "Last failed"
|
|
72
|
+
msgstr "Dernier échec"
|
|
73
|
+
|
|
74
|
+
#: models/login_ban.py:52
|
|
75
|
+
msgid "Login ban"
|
|
76
|
+
msgstr "Login banni"
|
|
77
|
+
|
|
78
|
+
#: models/login_ban.py:53
|
|
79
|
+
msgid "Login bans"
|
|
80
|
+
msgstr "Login bannis"
|
|
81
|
+
|
|
82
|
+
#: models/otp_user_mixin.py:10
|
|
63
83
|
msgid "OTP secret token"
|
|
64
84
|
msgstr "Jeton secret OTP"
|
|
65
85
|
|
|
66
|
-
#: models/otp_user_mixin.py:
|
|
86
|
+
#: models/otp_user_mixin.py:13
|
|
67
87
|
msgid "Temporary OTP secret token"
|
|
68
88
|
msgstr "Jeton secret OTP temporaire"
|
|
69
89
|
|
|
90
|
+
#: models/otp_user_mixin.py:14
|
|
91
|
+
msgid "HOTP count"
|
|
92
|
+
msgstr "Compte HOTP"
|
|
93
|
+
|
|
94
|
+
#: models/otp_user_mixin.py:15
|
|
95
|
+
msgid "HOTP expiry"
|
|
96
|
+
msgstr "Expiration HOTP"
|
|
97
|
+
|
|
70
98
|
#: models/pfx_models.py:14
|
|
71
99
|
#, python-format
|
|
72
100
|
msgid "%(model_name)s with this %(field_labels)s already exists."
|
|
@@ -92,6 +120,41 @@ msgstr "{key} doit être une date."
|
|
|
92
120
|
msgid "{key} must be “true”, “false”, “1”, “0” or empty."
|
|
93
121
|
msgstr "{key} doit être « true », « false », « 1 », « 0 » ou vide"
|
|
94
122
|
|
|
123
|
+
#: templates/registration/otp_code_email.txt:2
|
|
124
|
+
#, python-format
|
|
125
|
+
msgid ""
|
|
126
|
+
"You're receiving this email because you requested an authentication code at "
|
|
127
|
+
"%(site_name)s."
|
|
128
|
+
msgstr ""
|
|
129
|
+
"Vous recevez cet e-mail parce que vous avez demandé un code d’authentication "
|
|
130
|
+
"sur %(site_name)s."
|
|
131
|
+
|
|
132
|
+
#: templates/registration/otp_code_email.txt:4
|
|
133
|
+
msgid "Authentication code:"
|
|
134
|
+
msgstr "Code d’authentication :"
|
|
135
|
+
|
|
136
|
+
#: templates/registration/otp_code_email.txt:6
|
|
137
|
+
#, python-format
|
|
138
|
+
msgid "This code is valid for %(otp_validity)s minutes."
|
|
139
|
+
msgstr "Ce code est valable durant %(otp_validity)s minutes."
|
|
140
|
+
|
|
141
|
+
#: templates/registration/otp_code_email.txt:8
|
|
142
|
+
#: templates/registration/password_reset_email.txt:10
|
|
143
|
+
#: templates/registration/welcome_email.txt:10
|
|
144
|
+
msgid "Thanks for using our site!"
|
|
145
|
+
msgstr "Merci d'utiliser notre site !"
|
|
146
|
+
|
|
147
|
+
#: templates/registration/otp_code_email.txt:10
|
|
148
|
+
#: templates/registration/password_reset_email.txt:12
|
|
149
|
+
#: templates/registration/welcome_email.txt:12
|
|
150
|
+
#, python-format
|
|
151
|
+
msgid "The %(site_name)s team"
|
|
152
|
+
msgstr "L'équipe de %(site_name)s"
|
|
153
|
+
|
|
154
|
+
#: templates/registration/otp_code_subject.txt:2
|
|
155
|
+
msgid "New authentication code for %(site_name)s"
|
|
156
|
+
msgstr "Nouveau code d’authentication pour %(site_name)s"
|
|
157
|
+
|
|
95
158
|
#: templates/registration/password_reset_email.txt:2
|
|
96
159
|
#, python-format
|
|
97
160
|
msgid ""
|
|
@@ -113,17 +176,6 @@ msgstr ""
|
|
|
113
176
|
msgid "Your username, in case you've forgotten:"
|
|
114
177
|
msgstr "Votre nom d'utilisateur, au cas où vous l'auriez oublié :"
|
|
115
178
|
|
|
116
|
-
#: templates/registration/password_reset_email.txt:10
|
|
117
|
-
#: templates/registration/welcome_email.txt:10
|
|
118
|
-
msgid "Thanks for using our site!"
|
|
119
|
-
msgstr "Merci d'utiliser notre site !"
|
|
120
|
-
|
|
121
|
-
#: templates/registration/password_reset_email.txt:12
|
|
122
|
-
#: templates/registration/welcome_email.txt:12
|
|
123
|
-
#, python-format
|
|
124
|
-
msgid "The %(site_name)s team"
|
|
125
|
-
msgstr "L'équipe de %(site_name)s"
|
|
126
|
-
|
|
127
179
|
#: templates/registration/password_reset_subject.txt:2
|
|
128
180
|
#, python-format
|
|
129
181
|
msgid "Password reset on %(site_name)s"
|
|
@@ -139,43 +191,52 @@ msgstr "Bienvenue sur %(site_name)s."
|
|
|
139
191
|
msgid "Welcome on %(site_name)s"
|
|
140
192
|
msgstr "Bienvenue sur %(site_name)s"
|
|
141
193
|
|
|
142
|
-
#: views/authentication_views.py:
|
|
194
|
+
#: views/authentication_views.py:82
|
|
195
|
+
#, python-brace-format
|
|
196
|
+
msgid ""
|
|
197
|
+
"Your connection is temporarily disabled after several unsuccessful attempts, "
|
|
198
|
+
"please retry in {seconds} seconds."
|
|
199
|
+
msgstr ""
|
|
200
|
+
"Votre connexion est temporairement désactivée après plusieurs tentatives "
|
|
201
|
+
"infructueuses, veuillez réessayer dans {seconds} secondes."
|
|
202
|
+
|
|
203
|
+
#: views/authentication_views.py:244 views/authentication_views.py:407
|
|
143
204
|
msgid "password updated successfully"
|
|
144
205
|
msgstr "le mot de passe a été mis à jour avec succès"
|
|
145
206
|
|
|
146
|
-
#: views/authentication_views.py:
|
|
207
|
+
#: views/authentication_views.py:249
|
|
147
208
|
msgid "Incorrect password"
|
|
148
209
|
msgstr "Mot de passe incorrect"
|
|
149
210
|
|
|
150
|
-
#: views/authentication_views.py:
|
|
211
|
+
#: views/authentication_views.py:252 views/authentication_views.py:416
|
|
151
212
|
msgid "Empty password is not allowed"
|
|
152
213
|
msgstr "Un mot de passe vide n’est pas autorisé"
|
|
153
214
|
|
|
154
|
-
#: views/authentication_views.py:
|
|
215
|
+
#: views/authentication_views.py:341
|
|
155
216
|
msgid "User and token are valid"
|
|
156
217
|
msgstr "L'utilisateur et le token sont valides"
|
|
157
218
|
|
|
158
|
-
#: views/authentication_views.py:
|
|
219
|
+
#: views/authentication_views.py:343
|
|
159
220
|
msgid "User or token is invalid"
|
|
160
221
|
msgstr "L'utilisateur ou le token est invalide"
|
|
161
222
|
|
|
162
|
-
#: views/authentication_views.py:
|
|
223
|
+
#: views/authentication_views.py:449
|
|
163
224
|
msgid "OTP is already activated"
|
|
164
225
|
msgstr "L'OTP est déjà activé"
|
|
165
226
|
|
|
166
|
-
#: views/authentication_views.py:
|
|
227
|
+
#: views/authentication_views.py:487
|
|
167
228
|
msgid "OTP is activated"
|
|
168
229
|
msgstr "L'OTP est activé"
|
|
169
230
|
|
|
170
|
-
#: views/authentication_views.py:
|
|
231
|
+
#: views/authentication_views.py:488 views/authentication_views.py:522
|
|
171
232
|
msgid "Invalid OTP code"
|
|
172
233
|
msgstr "Code OTP invalide"
|
|
173
234
|
|
|
174
|
-
#: views/authentication_views.py:
|
|
235
|
+
#: views/authentication_views.py:521
|
|
175
236
|
msgid "OTP is disabled"
|
|
176
237
|
msgstr "L'OTP est désactivé"
|
|
177
238
|
|
|
178
|
-
#: views/authentication_views.py:
|
|
239
|
+
#: views/authentication_views.py:777
|
|
179
240
|
msgid ""
|
|
180
241
|
"If the email address you entered is correct, you will receive an email from "
|
|
181
242
|
"us with instructions to reset your password."
|
|
@@ -184,6 +245,10 @@ msgstr ""
|
|
|
184
245
|
"un courrier électronique de notre part contenant des instructions pour "
|
|
185
246
|
"réinitialiser votre mot de passe."
|
|
186
247
|
|
|
248
|
+
#: views/authentication_views.py:845
|
|
249
|
+
msgid "A new authentication code has been sent by email."
|
|
250
|
+
msgstr "Un nouveau code d'authentification a été envoyé par e-mail."
|
|
251
|
+
|
|
187
252
|
#: views/filters_views.py:79
|
|
188
253
|
#, python-brace-format
|
|
189
254
|
msgid "Invalid value for {filter} filter"
|
|
@@ -32,25 +32,29 @@ class JWTTokenDecodeMixin:
|
|
|
32
32
|
@classmethod
|
|
33
33
|
def decode_jwt(cls, token, otp_login=False):
|
|
34
34
|
try:
|
|
35
|
+
headers = jwt.get_unverified_header(token)
|
|
36
|
+
if 'pfx_user_pk' not in headers:
|
|
37
|
+
raise jwt.InvalidTokenError(
|
|
38
|
+
"Missing pfx_user_pk in token headers")
|
|
39
|
+
user = cls.get_cached_user(headers['pfx_user_pk'])
|
|
35
40
|
decoded = jwt.decode(
|
|
36
|
-
token,
|
|
41
|
+
token,
|
|
42
|
+
user.get_user_jwt_signature_key() + settings.PFX_SECRET_KEY,
|
|
37
43
|
options=dict(require=["exp"]),
|
|
38
44
|
algorithms="HS256")
|
|
39
|
-
user = cls.get_cached_user(decoded['pfx_user_pk'])
|
|
40
45
|
if 'otp_login' in decoded:
|
|
41
46
|
if otp_login:
|
|
42
47
|
return user, *decoded['otp_login']
|
|
43
48
|
raise jwt.InvalidTokenError(
|
|
44
49
|
"This token is reserved for OTP login")
|
|
45
50
|
return user
|
|
46
|
-
except get_user_model().DoesNotExist
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
logger.
|
|
50
|
-
raise
|
|
51
|
-
except jwt.ExpiredSignatureError:
|
|
51
|
+
except (get_user_model().DoesNotExist, jwt.ExpiredSignatureError,
|
|
52
|
+
jwt.InvalidTokenError, jwt.InvalidSignatureError) as e:
|
|
53
|
+
# Log these exceptions only in debug mode
|
|
54
|
+
logger.debug(e, exc_info=True)
|
|
52
55
|
raise
|
|
53
|
-
except Exception as e:
|
|
56
|
+
except (DecodeError, Exception) as e:
|
|
57
|
+
# Always logs unexpected exceptions
|
|
54
58
|
logger.exception(e)
|
|
55
59
|
raise
|
|
56
60
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from django.contrib.auth.models import AbstractBaseUser
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class AbstractPFXBaseUser(AbstractBaseUser):
|
|
5
|
+
class Meta:
|
|
6
|
+
abstract = True
|
|
7
|
+
|
|
8
|
+
def get_user_jwt_signature_key(self):
|
|
9
|
+
"""
|
|
10
|
+
Return a user secret to sign JWT token.
|
|
11
|
+
|
|
12
|
+
If not empty, the JWT token validity depends on all values
|
|
13
|
+
user to build the return string.
|
|
14
|
+
"""
|
|
15
|
+
return self.password
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from datetime import timedelta
|
|
2
|
+
|
|
3
|
+
from django.db import models
|
|
4
|
+
from django.utils import timezone
|
|
5
|
+
from django.utils.translation import gettext_lazy as _
|
|
6
|
+
|
|
7
|
+
from pfx.pfxcore.settings import PFXSettings
|
|
8
|
+
|
|
9
|
+
settings = PFXSettings()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LoginBanQuerySet(models.QuerySet):
|
|
13
|
+
def is_ban(self, username):
|
|
14
|
+
if not username or settings.PFX_LOGIN_BAN_FAILED_NUMBER == 0:
|
|
15
|
+
return False
|
|
16
|
+
try:
|
|
17
|
+
ban = self.get(username=username)
|
|
18
|
+
except LoginBan.DoesNotExist:
|
|
19
|
+
return False
|
|
20
|
+
if ban.failed_counter % settings.PFX_LOGIN_BAN_FAILED_NUMBER == 0:
|
|
21
|
+
seconds = settings.PFX_LOGIN_BAN_SECONDS_START + (
|
|
22
|
+
settings.PFX_LOGIN_BAN_SECONDS_STEP * (ban.failed_counter - 1))
|
|
23
|
+
ban_time = ban.last_failed + timedelta(seconds=seconds)
|
|
24
|
+
now = timezone.now()
|
|
25
|
+
if now < ban_time:
|
|
26
|
+
return ban_time - now
|
|
27
|
+
return False
|
|
28
|
+
|
|
29
|
+
def ban(self, username):
|
|
30
|
+
if not username:
|
|
31
|
+
return
|
|
32
|
+
try:
|
|
33
|
+
ban = self.get(username=username)
|
|
34
|
+
ban.save()
|
|
35
|
+
except LoginBan.DoesNotExist:
|
|
36
|
+
LoginBan.objects.create(username=username)
|
|
37
|
+
|
|
38
|
+
def unban(self, username):
|
|
39
|
+
if not username:
|
|
40
|
+
return
|
|
41
|
+
self.filter(username=username).delete()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class LoginBan(models.Model):
|
|
45
|
+
username = models.CharField(_("Username"), max_length=150, unique=True)
|
|
46
|
+
failed_counter = models.IntegerField(_("Failed counter"))
|
|
47
|
+
last_failed = models.DateTimeField(_("Last failed"), auto_now=True)
|
|
48
|
+
|
|
49
|
+
objects = LoginBanQuerySet.as_manager()
|
|
50
|
+
|
|
51
|
+
class Meta:
|
|
52
|
+
verbose_name = _("Login ban")
|
|
53
|
+
verbose_name_plural = _("Login bans")
|
|
54
|
+
|
|
55
|
+
def __str__(self):
|
|
56
|
+
return self.username
|
|
57
|
+
|
|
58
|
+
def save(self, *args, **kwargs):
|
|
59
|
+
self.failed_counter = (self.failed_counter or 0) + 1
|
|
60
|
+
return super().save(*args, **kwargs)
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
from
|
|
1
|
+
from datetime import timedelta
|
|
2
|
+
|
|
2
3
|
from django.db import models
|
|
4
|
+
from django.utils import timezone
|
|
3
5
|
from django.utils.translation import gettext_lazy as _
|
|
4
6
|
|
|
7
|
+
from pfx.pfxcore.settings import PFXSettings
|
|
8
|
+
|
|
9
|
+
settings = PFXSettings()
|
|
10
|
+
|
|
5
11
|
|
|
6
12
|
class OtpUserMixin(models.Model):
|
|
7
13
|
otp_secret_token = models.CharField(
|
|
@@ -9,6 +15,8 @@ class OtpUserMixin(models.Model):
|
|
|
9
15
|
blank=True, unique=True)
|
|
10
16
|
otp_secret_token_tmp = models.CharField(
|
|
11
17
|
_("Temporary OTP secret token"), max_length=32, null=True, blank=True)
|
|
18
|
+
hotp_count = models.IntegerField(_("HOTP count"), default=0)
|
|
19
|
+
hotp_expiry = models.DateTimeField(_("HOTP expiry"), default=timezone.now)
|
|
12
20
|
|
|
13
21
|
class Meta:
|
|
14
22
|
abstract = True
|
|
@@ -41,4 +49,25 @@ class OtpUserMixin(models.Model):
|
|
|
41
49
|
def is_otp_valid(self, otp_code, tmp=False):
|
|
42
50
|
import pyotp
|
|
43
51
|
totp = pyotp.parse_uri(self.get_otp_setup_uri(tmp=tmp))
|
|
44
|
-
|
|
52
|
+
valid = totp.verify(otp_code)
|
|
53
|
+
if not valid and timezone.now() <= self.hotp_expiry:
|
|
54
|
+
hotp = pyotp.hotp.HOTP(
|
|
55
|
+
tmp and self.otp_secret_token_tmp or
|
|
56
|
+
self.otp_secret_token)
|
|
57
|
+
return hotp.verify(otp_code, self.hotp_count)
|
|
58
|
+
return valid
|
|
59
|
+
|
|
60
|
+
def get_user_jwt_signature_key(self):
|
|
61
|
+
return super().get_user_jwt_signature_key() + (
|
|
62
|
+
self.otp_secret_token or "")
|
|
63
|
+
|
|
64
|
+
def get_hotp_code(self):
|
|
65
|
+
import pyotp
|
|
66
|
+
if not self.otp_secret_token:
|
|
67
|
+
raise Exception("OTP disabled")
|
|
68
|
+
self.hotp_count += 1
|
|
69
|
+
self.hotp_expiry = timezone.now() + timedelta(
|
|
70
|
+
minutes=settings.PFX_HOTP_CODE_VALIDITY)
|
|
71
|
+
self.save(update_fields=[
|
|
72
|
+
'hotp_count', 'hotp_expiry'])
|
|
73
|
+
return pyotp.hotp.HOTP(self.otp_secret_token).at(self.hotp_count)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{% load i18n %}{% autoescape off %}
|
|
2
|
+
{% blocktrans %}You're receiving this email because you requested an authentication code at {{ site_name }}.{% endblocktrans %}
|
|
3
|
+
|
|
4
|
+
{% trans "Authentication code:" %} {{ otp_code }}
|
|
5
|
+
|
|
6
|
+
{% blocktrans %}This code is valid for {{ otp_validity }} minutes.{% endblocktrans %}
|
|
7
|
+
|
|
8
|
+
{% trans "Thanks for using our site!" %}
|
|
9
|
+
|
|
10
|
+
{% blocktrans %}The {{ site_name }} team{% endblocktrans %}
|
|
11
|
+
|
|
12
|
+
{% endautoescape %}
|