django-pfx 1.4.dev22__tar.gz → 1.4.dev26__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.dev22 → django-pfx-1.4.dev26}/PKG-INFO +1 -1
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/django_pfx.egg-info/PKG-INFO +1 -1
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/django_pfx.egg-info/SOURCES.txt +2 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/default_settings.py +1 -0
- django-pfx-1.4.dev26/pfx/pfxcore/locale/fr/LC_MESSAGES/django.mo +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/locale/fr/LC_MESSAGES/django.po +89 -24
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/middleware/authentication.py +27 -10
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/models/otp_user_mixin.py +27 -2
- django-pfx-1.4.dev26/pfx/pfxcore/templates/registration/otp_code_email.txt +12 -0
- django-pfx-1.4.dev26/pfx/pfxcore/templates/registration/otp_code_subject.txt +3 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/urls.py +2 -1
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/views/__init__.py +1 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/views/authentication_views.py +117 -13
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/tests/tests/test_auth_api.py +116 -0
- django-pfx-1.4.dev22/pfx/pfxcore/locale/fr/LC_MESSAGES/django.mo +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/.gitignore +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/.gitlab-ci.yml +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/.pre-commit-config.yaml +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/LICENSE +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/MANIFEST.in +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/README.md +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/django-admin-test +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/django_pfx.egg-info/dependency_links.txt +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/django_pfx.egg-info/requires.txt +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/django_pfx.egg-info/top_level.txt +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/doc/Makefile +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/doc/conf.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/doc/index.rst +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/doc/source/api.views.rst +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/doc/source/authentication.md +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/doc/source/decorator.md +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/doc/source/generate_openapi.md +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/doc/source/getting_started.md +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/doc/source/internationalisation.md +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/doc/source/model.md +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/doc/source/pfx_views.md +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/doc/source/profiling.md +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/doc/source/settings.md +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/doc/source/testing.md +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/img/pfx.png +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/img/pfx.svg +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/__init__.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/__init__.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/apidoc/__init__.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/apidoc/parameters.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/apidoc/schema.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/apidoc/tags.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/apps.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/decorator/__init__.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/decorator/rest.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/exceptions.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/fields.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/http/__init__.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/http/json_response.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/management/__init__.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/management/commands/__init__.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/management/commands/makeapidoc.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/management/commands/profile.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/middleware/__init__.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/middleware/locale.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/middleware/profiling.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/models/__init__.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/models/abstract_pfx_base_user.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/models/cache_mixins.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/models/login_ban.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/models/not_null_fields.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/models/pfx_models.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/models/user_filtered_queryset_mixin.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/serializers/__init__.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/serializers/json.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/settings.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/shortcuts.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/storage/__init__.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/storage/s3_storage.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/templates/registration/password_reset_email.txt +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/templates/registration/password_reset_subject.txt +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/templates/registration/welcome_email.txt +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/templates/registration/welcome_subject.txt +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/test.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/views/fields.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/views/filters_views.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/views/locale_views.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/views/parameters/__init__.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/views/parameters/date_format.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/views/parameters/groups.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/views/parameters/list_count.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/views/parameters/list_items.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/views/parameters/list_mode.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/views/parameters/list_order.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/views/parameters/list_search.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/views/parameters/media_redirect.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/views/parameters/meta_fields.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/views/parameters/meta_filters.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/views/parameters/meta_orders.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/views/parameters/subset.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/views/parameters/subset_limit.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/views/parameters/subset_offset.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/views/parameters/subset_page.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/views/parameters/subset_page_size.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/views/parameters/subset_page_subset.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/views/rest_views.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pyproject.toml +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/requirements.txt +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/runtest.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/serve-doc +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/setup.cfg +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/setup.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/tests/__init__.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/tests/locale/fr/LC_MESSAGES/django.po +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/tests/models.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/tests/settings/__init__.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/tests/settings/ci.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/tests/settings/common.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/tests/settings/dev.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/tests/settings/dev_custom_example.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/tests/settings/dev_default.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/tests/tests/__init__.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/tests/tests/basic_api_errors.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/tests/tests/basic_api_test.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/tests/tests/test_api_doc.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/tests/tests/test_body_mixin.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/tests/tests/test_cache.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/tests/tests/test_client.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/tests/tests/test_fields.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/tests/tests/test_filters.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/tests/tests/test_locale_api.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/tests/tests/test_perm_tests.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/tests/tests/test_perms_api.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/tests/tests/test_profiling_middleware.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/tests/tests/test_shortcuts.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/tests/tests/test_timezone_middleware.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/tests/tests/test_tools.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/tests/tests/test_user_queryset.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/tests/tests/test_view_decorators.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/tests/tests/test_view_fields.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/tests/urls.py +0 -0
- {django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/tests/views.py +0 -0
|
@@ -72,6 +72,8 @@ pfx/pfxcore/serializers/__init__.py
|
|
|
72
72
|
pfx/pfxcore/serializers/json.py
|
|
73
73
|
pfx/pfxcore/storage/__init__.py
|
|
74
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
|
|
75
77
|
pfx/pfxcore/templates/registration/password_reset_email.txt
|
|
76
78
|
pfx/pfxcore/templates/registration/password_reset_subject.txt
|
|
77
79
|
pfx/pfxcore/templates/registration/welcome_email.txt
|
|
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"
|
|
@@ -30,13 +30,29 @@ class JWTTokenDecodeMixin:
|
|
|
30
30
|
return user
|
|
31
31
|
|
|
32
32
|
@classmethod
|
|
33
|
-
def
|
|
33
|
+
def decode_jwt_header(cls, token):
|
|
34
34
|
try:
|
|
35
35
|
headers = jwt.get_unverified_header(token)
|
|
36
36
|
if 'pfx_user_pk' not in headers:
|
|
37
37
|
raise jwt.InvalidTokenError(
|
|
38
38
|
"Missing pfx_user_pk in token headers")
|
|
39
|
-
|
|
39
|
+
return headers['pfx_user_pk']
|
|
40
|
+
except (jwt.ExpiredSignatureError,
|
|
41
|
+
jwt.InvalidTokenError, jwt.InvalidSignatureError,
|
|
42
|
+
DecodeError) as e:
|
|
43
|
+
# Log these exceptions only in debug mode
|
|
44
|
+
logger.debug(e, exc_info=True)
|
|
45
|
+
raise
|
|
46
|
+
except Exception as e:
|
|
47
|
+
# Always logs unexpected exceptions
|
|
48
|
+
logger.exception(e)
|
|
49
|
+
raise
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def decode_jwt(cls, token, otp_login=False):
|
|
53
|
+
user_pk = cls.decode_jwt_header(token)
|
|
54
|
+
try:
|
|
55
|
+
user = cls.get_cached_user(user_pk)
|
|
40
56
|
decoded = jwt.decode(
|
|
41
57
|
token,
|
|
42
58
|
user.get_user_jwt_signature_key() + settings.PFX_SECRET_KEY,
|
|
@@ -49,11 +65,12 @@ class JWTTokenDecodeMixin:
|
|
|
49
65
|
"This token is reserved for OTP login")
|
|
50
66
|
return user
|
|
51
67
|
except (get_user_model().DoesNotExist, jwt.ExpiredSignatureError,
|
|
52
|
-
jwt.InvalidTokenError, jwt.InvalidSignatureError
|
|
68
|
+
jwt.InvalidTokenError, jwt.InvalidSignatureError,
|
|
69
|
+
DecodeError) as e:
|
|
53
70
|
# Log these exceptions only in debug mode
|
|
54
71
|
logger.debug(e, exc_info=True)
|
|
55
72
|
raise
|
|
56
|
-
except
|
|
73
|
+
except Exception as e:
|
|
57
74
|
# Always logs unexpected exceptions
|
|
58
75
|
logger.exception(e)
|
|
59
76
|
raise
|
|
@@ -72,11 +89,11 @@ class AuthenticationMiddleware(JWTTokenDecodeMixin, MiddlewareMixin):
|
|
|
72
89
|
authorization = request.headers.get('Authorization')
|
|
73
90
|
if authorization:
|
|
74
91
|
try:
|
|
75
|
-
_,
|
|
92
|
+
_, token = authorization.split("Bearer ")
|
|
76
93
|
except ValueError:
|
|
77
|
-
|
|
94
|
+
token = ""
|
|
78
95
|
try:
|
|
79
|
-
request.user = self.decode_jwt(
|
|
96
|
+
request.user = self.decode_jwt(token)
|
|
80
97
|
except Exception:
|
|
81
98
|
request.user = AnonymousUser()
|
|
82
99
|
else:
|
|
@@ -102,10 +119,10 @@ class CookieAuthenticationMiddleware(JWTTokenDecodeMixin, MiddlewareMixin):
|
|
|
102
119
|
"""
|
|
103
120
|
|
|
104
121
|
def process_request(self, request):
|
|
105
|
-
|
|
106
|
-
if
|
|
122
|
+
token = request.COOKIES.get('token', "")
|
|
123
|
+
if token:
|
|
107
124
|
try:
|
|
108
|
-
request.user = self.decode_jwt(
|
|
125
|
+
request.user = self.decode_jwt(token)
|
|
109
126
|
except Exception:
|
|
110
127
|
request.user = AnonymousUser()
|
|
111
128
|
request.delete_cookie = True
|
|
@@ -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,8 +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
|
|
45
59
|
|
|
46
60
|
def get_user_jwt_signature_key(self):
|
|
47
61
|
return super().get_user_jwt_signature_key() + (
|
|
48
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 %}
|
|
@@ -261,9 +261,7 @@ class AuthenticationView(
|
|
|
261
261
|
return jwt.encode(
|
|
262
262
|
payload,
|
|
263
263
|
user.get_user_jwt_signature_key() + settings.PFX_SECRET_KEY,
|
|
264
|
-
headers=dict(
|
|
265
|
-
pfx_user_pk=user.pk,
|
|
266
|
-
username=user.get_username()),
|
|
264
|
+
headers=dict(pfx_user_pk=user.pk),
|
|
267
265
|
algorithm="HS256")
|
|
268
266
|
|
|
269
267
|
def get_extra_payload(self, user):
|
|
@@ -417,7 +415,7 @@ class AuthenticationView(
|
|
|
417
415
|
raise UnauthorizedError()
|
|
418
416
|
|
|
419
417
|
@method_decorator(never_cache)
|
|
420
|
-
@rest_api("/otp/activate", public=False, method="put")
|
|
418
|
+
@rest_api("/otp/activate", public=False, method="put", priority_doc=52)
|
|
421
419
|
def otp_activate(self, *args, **kwargs):
|
|
422
420
|
"""Entrypoint for :code:`PUT /otp/activate` route.
|
|
423
421
|
|
|
@@ -453,7 +451,7 @@ class AuthenticationView(
|
|
|
453
451
|
|
|
454
452
|
@method_decorator(never_cache)
|
|
455
453
|
@rest_api(
|
|
456
|
-
"/otp/confirm", public=False, method="put",
|
|
454
|
+
"/otp/confirm", public=False, method="put", priority_doc=53,
|
|
457
455
|
response_schema='message_schema')
|
|
458
456
|
def otp_confirm(self, *args, **kwargs):
|
|
459
457
|
"""Entrypoint for :code:`PUT /otp/confirm` route.
|
|
@@ -488,7 +486,7 @@ class AuthenticationView(
|
|
|
488
486
|
return JsonResponse(dict(message=_("Invalid OTP code")), status=422)
|
|
489
487
|
|
|
490
488
|
@method_decorator(never_cache)
|
|
491
|
-
@rest_api("/otp/disable", public=False, method="put")
|
|
489
|
+
@rest_api("/otp/disable", public=False, method="put", priority_doc=54)
|
|
492
490
|
def otp_disable(self, *args, **kwargs):
|
|
493
491
|
"""Entrypoint for :code:`PUT /otp/disable` route.
|
|
494
492
|
|
|
@@ -523,7 +521,7 @@ class AuthenticationView(
|
|
|
523
521
|
|
|
524
522
|
@method_decorator(never_cache)
|
|
525
523
|
@rest_api(
|
|
526
|
-
"/otp/login", public=True, method="post",
|
|
524
|
+
"/otp/login", public=True, method="post", priority_doc=50,
|
|
527
525
|
response_schema='login_schema')
|
|
528
526
|
def otp_login(self, *args, **kwargs):
|
|
529
527
|
"""Entrypoint for :code:`PUT /otp/login` route.
|
|
@@ -557,12 +555,15 @@ class AuthenticationView(
|
|
|
557
555
|
description: If the token is missing, invalid or expired.
|
|
558
556
|
403:
|
|
559
557
|
description: If the OTP is disabled for this user.
|
|
560
|
-
|
|
561
558
|
"""
|
|
562
559
|
data = self.deserialize_body()
|
|
563
|
-
token = data.get('token')
|
|
564
|
-
|
|
565
|
-
|
|
560
|
+
token = data.get('token', "")
|
|
561
|
+
try:
|
|
562
|
+
user_pk = self.decode_jwt_header(token)
|
|
563
|
+
ban_key = f"otp_{user_pk}"
|
|
564
|
+
except Exception:
|
|
565
|
+
raise UnauthorizedError()
|
|
566
|
+
ban_dt = LoginBan.objects.is_ban(ban_key)
|
|
566
567
|
if ban_dt:
|
|
567
568
|
return self.login_ban_response(ban_dt)
|
|
568
569
|
try:
|
|
@@ -575,9 +576,9 @@ class AuthenticationView(
|
|
|
575
576
|
if not user.otp_secret_token:
|
|
576
577
|
raise ForbiddenError()
|
|
577
578
|
if user.is_otp_valid(data.get('otp_code')):
|
|
578
|
-
LoginBan.objects.unban(
|
|
579
|
+
LoginBan.objects.unban(ban_key)
|
|
579
580
|
return self._login_success(user, mode, remember_me)
|
|
580
|
-
LoginBan.objects.ban(
|
|
581
|
+
LoginBan.objects.ban(ban_key)
|
|
581
582
|
return self.login_failed_response()
|
|
582
583
|
|
|
583
584
|
def get_user(self, uidb64):
|
|
@@ -779,3 +780,106 @@ class ForgottenPasswordView(SendMessageTokenMixin, BodyMixin, BaseRestView):
|
|
|
779
780
|
'you will receive an email from us with '
|
|
780
781
|
'instructions to reset your password.')
|
|
781
782
|
})
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
@rest_view("/auth/otp")
|
|
786
|
+
class OtpEmailView(BodyMixin, JWTTokenDecodeMixin, BaseRestView):
|
|
787
|
+
"""View for forgotten password service."""
|
|
788
|
+
#: The email template.
|
|
789
|
+
email_template_name = 'registration/otp_code_email.txt'
|
|
790
|
+
#: The email subject template.
|
|
791
|
+
subject_template_name = 'registration/otp_code_subject.txt'
|
|
792
|
+
#: The HTML email template.
|
|
793
|
+
html_email_template_name = None
|
|
794
|
+
#: Extra email context.
|
|
795
|
+
extra_email_context = None
|
|
796
|
+
#: The from value of the email.
|
|
797
|
+
from_email = None
|
|
798
|
+
#: The email field of user model.
|
|
799
|
+
email_field = 'email'
|
|
800
|
+
#: The language field of user model
|
|
801
|
+
language_field = 'language'
|
|
802
|
+
#: The ApiDoc tags.
|
|
803
|
+
tags = [AUTHENTICATION_TAG]
|
|
804
|
+
|
|
805
|
+
@method_decorator(never_cache)
|
|
806
|
+
@rest_api(
|
|
807
|
+
"/email", public=True, method="post", priority_doc=51,
|
|
808
|
+
response_schema='message_schema')
|
|
809
|
+
def sent_email(self, *args, **kwargs):
|
|
810
|
+
"""Entrypoint for :code:`POST /email` route.
|
|
811
|
+
|
|
812
|
+
Request a new OTP code by email.
|
|
813
|
+
|
|
814
|
+
:returns: The JSON response
|
|
815
|
+
:rtype: :class:`JsonResponse`
|
|
816
|
+
---
|
|
817
|
+
post:
|
|
818
|
+
summary: Send OTP email
|
|
819
|
+
description: Request a new OTP code by email.
|
|
820
|
+
requestBody:
|
|
821
|
+
content:
|
|
822
|
+
application/json:
|
|
823
|
+
schema:
|
|
824
|
+
properties:
|
|
825
|
+
token:
|
|
826
|
+
type: string
|
|
827
|
+
description: a valid JWT user token. This
|
|
828
|
+
is required only if the user is not
|
|
829
|
+
already connected (with a Bearer token
|
|
830
|
+
or a Cookie).
|
|
831
|
+
responses:
|
|
832
|
+
401:
|
|
833
|
+
description: If the token is missing, invalid or expired.
|
|
834
|
+
403:
|
|
835
|
+
description: If the OTP is disabled for this user.
|
|
836
|
+
"""
|
|
837
|
+
if self.request.user.is_anonymous:
|
|
838
|
+
data = self.deserialize_body()
|
|
839
|
+
try:
|
|
840
|
+
user, __, __ = self.decode_jwt(
|
|
841
|
+
data.get('token', ""), otp_login=True)
|
|
842
|
+
except Exception:
|
|
843
|
+
raise UnauthorizedError()
|
|
844
|
+
else:
|
|
845
|
+
user = self.request.user
|
|
846
|
+
if not isinstance(user, OtpUserMixin):
|
|
847
|
+
logger.error("User must inherit OtpUserMixin to activate OTP")
|
|
848
|
+
raise NotFoundError()
|
|
849
|
+
if not user.otp_secret_token:
|
|
850
|
+
raise ForbiddenError()
|
|
851
|
+
self.send_otp_message(user)
|
|
852
|
+
return JsonResponse({
|
|
853
|
+
'message': _('A new authentication code has been sent by email.')})
|
|
854
|
+
|
|
855
|
+
def send_otp_message(self, user):
|
|
856
|
+
"""Send an email to a user with an OTP code.
|
|
857
|
+
|
|
858
|
+
:param user: The user
|
|
859
|
+
"""
|
|
860
|
+
from django.utils import translation
|
|
861
|
+
lang = str(getattr(user, self.language_field, settings.LANGUAGE_CODE))
|
|
862
|
+
|
|
863
|
+
otp_code = user.get_hotp_code()
|
|
864
|
+
data = {
|
|
865
|
+
'target_user': user,
|
|
866
|
+
'otp_code': otp_code,
|
|
867
|
+
'otp_validity': settings.PFX_HOTP_CODE_VALIDITY,
|
|
868
|
+
'site_name': settings.PFX_SITE_NAME,
|
|
869
|
+
'user': user,
|
|
870
|
+
**(self.extra_email_context or {})
|
|
871
|
+
}
|
|
872
|
+
with translation.override(lang):
|
|
873
|
+
subject = loader.render_to_string(self.subject_template_name, data)
|
|
874
|
+
# Email subject *must not* contain newlines
|
|
875
|
+
subject = ''.join(subject.splitlines())
|
|
876
|
+
body = loader.render_to_string(self.email_template_name, data)
|
|
877
|
+
email_message = EmailMultiAlternatives(
|
|
878
|
+
subject, body, self.from_email,
|
|
879
|
+
[getattr(user, self.email_field)])
|
|
880
|
+
if self.html_email_template_name is not None:
|
|
881
|
+
html_email = loader.render_to_string(
|
|
882
|
+
self.html_email_template_name, data)
|
|
883
|
+
email_message.attach_alternative(
|
|
884
|
+
html_email, 'text/html')
|
|
885
|
+
email_message.send()
|
|
@@ -944,6 +944,34 @@ class AuthAPITest(TestAssertMixin, TransactionTestCase):
|
|
|
944
944
|
self.assertIsNone(self.user1.otp_secret_token)
|
|
945
945
|
self.assertIsNone(self.user1.otp_secret_token_tmp)
|
|
946
946
|
|
|
947
|
+
@override_settings(
|
|
948
|
+
PFX_HOTP_CODE_VALIDITY=15,
|
|
949
|
+
PFX_TOKEN_OTP_VALIDITY={'minutes': 30},)
|
|
950
|
+
def test_otp_disable_by_email(self):
|
|
951
|
+
self.enable_otp(self.user1)
|
|
952
|
+
|
|
953
|
+
self.otp_login(self.user1, 'RIGHT PASSWORD')
|
|
954
|
+
|
|
955
|
+
with freeze_time("2023-05-01 08:00:00"):
|
|
956
|
+
response = self.client.post('/api/auth/otp/email', dict())
|
|
957
|
+
self.assertRC(response, 200)
|
|
958
|
+
|
|
959
|
+
self.assertEqual(
|
|
960
|
+
mail.outbox[0].subject,
|
|
961
|
+
f'New authentication code for {settings.PFX_SITE_NAME}')
|
|
962
|
+
code_match = re.search(
|
|
963
|
+
r'Authentication code: (\d{6})', mail.outbox[0].body)
|
|
964
|
+
self.assertIsNotNone(code_match)
|
|
965
|
+
otp_code = int(code_match.group(1))
|
|
966
|
+
|
|
967
|
+
response = self.client.put('/api/auth/otp/disable', dict(
|
|
968
|
+
otp_code=otp_code))
|
|
969
|
+
self.assertRC(response, 200)
|
|
970
|
+
self.assertJE(response, 'message', "OTP is disabled")
|
|
971
|
+
self.user1.refresh_from_db()
|
|
972
|
+
self.assertIsNone(self.user1.otp_secret_token)
|
|
973
|
+
self.assertIsNone(self.user1.otp_secret_token_tmp)
|
|
974
|
+
|
|
947
975
|
def test_otp_disable_bad_code(self):
|
|
948
976
|
self.enable_otp(self.user1)
|
|
949
977
|
|
|
@@ -1195,3 +1223,91 @@ class AuthAPITest(TestAssertMixin, TransactionTestCase):
|
|
|
1195
1223
|
'/api/private/authors',
|
|
1196
1224
|
HTTP_AUTHORIZATION='Bearer ' + login_token)
|
|
1197
1225
|
self.assertRC(response, 200)
|
|
1226
|
+
|
|
1227
|
+
@override_settings(PFX_HOTP_CODE_VALIDITY=15)
|
|
1228
|
+
def test_send_otp_email(self):
|
|
1229
|
+
self.enable_otp(self.user1)
|
|
1230
|
+
|
|
1231
|
+
with freeze_time("2023-05-01 08:00:00"):
|
|
1232
|
+
response = self.client.post('/api/auth/login', dict(
|
|
1233
|
+
username='jrr.tolkien',
|
|
1234
|
+
password='RIGHT PASSWORD'))
|
|
1235
|
+
self.assertRC(response, 200)
|
|
1236
|
+
self.assertJE(response, 'need_otp', True)
|
|
1237
|
+
self.assertJEExists(response, 'token')
|
|
1238
|
+
otp_token = self.get_val(response, 'token')
|
|
1239
|
+
|
|
1240
|
+
response = self.client.post('/api/auth/otp/email', dict(
|
|
1241
|
+
token=otp_token))
|
|
1242
|
+
self.assertRC(response, 200)
|
|
1243
|
+
|
|
1244
|
+
self.assertEqual(
|
|
1245
|
+
mail.outbox[0].subject,
|
|
1246
|
+
f'New authentication code for {settings.PFX_SITE_NAME}')
|
|
1247
|
+
code_match = re.search(
|
|
1248
|
+
r'Authentication code: (\d{6})', mail.outbox[0].body)
|
|
1249
|
+
self.assertIsNotNone(code_match)
|
|
1250
|
+
otp_code = int(code_match.group(1))
|
|
1251
|
+
|
|
1252
|
+
response = self.client.post('/api/auth/otp/login', dict(
|
|
1253
|
+
token=otp_token,
|
|
1254
|
+
otp_code=otp_code))
|
|
1255
|
+
self.assertRC(response, 200)
|
|
1256
|
+
self.assertJEExists(response, 'token')
|
|
1257
|
+
login_token = self.get_val(response, 'token')
|
|
1258
|
+
|
|
1259
|
+
# Login token is accepted
|
|
1260
|
+
response = self.client.get(
|
|
1261
|
+
'/api/private/authors',
|
|
1262
|
+
HTTP_AUTHORIZATION='Bearer ' + login_token)
|
|
1263
|
+
self.assertRC(response, 200)
|
|
1264
|
+
|
|
1265
|
+
@override_settings(
|
|
1266
|
+
PFX_HOTP_CODE_VALIDITY=15,
|
|
1267
|
+
PFX_TOKEN_OTP_VALIDITY={'minutes': 30},)
|
|
1268
|
+
def test_send_otp_email_expiry(self):
|
|
1269
|
+
self.enable_otp(self.user1)
|
|
1270
|
+
|
|
1271
|
+
with freeze_time("2023-05-01 08:00:00"):
|
|
1272
|
+
response = self.client.post('/api/auth/login', dict(
|
|
1273
|
+
username='jrr.tolkien',
|
|
1274
|
+
password='RIGHT PASSWORD'))
|
|
1275
|
+
self.assertRC(response, 200)
|
|
1276
|
+
self.assertJE(response, 'need_otp', True)
|
|
1277
|
+
self.assertJEExists(response, 'token')
|
|
1278
|
+
otp_token = self.get_val(response, 'token')
|
|
1279
|
+
|
|
1280
|
+
response = self.client.post('/api/auth/otp/email', dict(
|
|
1281
|
+
token=otp_token))
|
|
1282
|
+
self.assertRC(response, 200)
|
|
1283
|
+
|
|
1284
|
+
self.assertEqual(
|
|
1285
|
+
mail.outbox[0].subject,
|
|
1286
|
+
f'New authentication code for {settings.PFX_SITE_NAME}')
|
|
1287
|
+
code_match = re.search(
|
|
1288
|
+
r'Authentication code: (\d{6})', mail.outbox[0].body)
|
|
1289
|
+
self.assertIsNotNone(code_match)
|
|
1290
|
+
otp_code = code_match.group(1)
|
|
1291
|
+
|
|
1292
|
+
with freeze_time("2023-05-01 08:15:00"):
|
|
1293
|
+
response = self.client.post('/api/auth/otp/login', dict(
|
|
1294
|
+
token=otp_token,
|
|
1295
|
+
otp_code=otp_code))
|
|
1296
|
+
self.assertRC(response, 200)
|
|
1297
|
+
self.assertJEExists(response, 'token')
|
|
1298
|
+
|
|
1299
|
+
with freeze_time("2023-05-01 08:15:01"):
|
|
1300
|
+
response = self.client.post('/api/auth/otp/login', dict(
|
|
1301
|
+
token=otp_token,
|
|
1302
|
+
otp_code=otp_code))
|
|
1303
|
+
self.assertRC(response, 422)
|
|
1304
|
+
|
|
1305
|
+
@override_settings(
|
|
1306
|
+
PFX_HOTP_CODE_VALIDITY=15,
|
|
1307
|
+
PFX_TOKEN_OTP_VALIDITY={'minutes': 30},)
|
|
1308
|
+
def test_send_otp_email_without_token(self):
|
|
1309
|
+
self.enable_otp(self.user1)
|
|
1310
|
+
|
|
1311
|
+
with freeze_time("2023-05-01 08:00:00"):
|
|
1312
|
+
response = self.client.post('/api/auth/otp/email', dict())
|
|
1313
|
+
self.assertRC(response, 401)
|
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/models/user_filtered_queryset_mixin.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/templates/registration/welcome_email.txt
RENAMED
|
File without changes
|
{django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/templates/registration/welcome_subject.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/views/parameters/media_redirect.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/views/parameters/subset_page_size.py
RENAMED
|
File without changes
|
{django-pfx-1.4.dev22 → django-pfx-1.4.dev26}/pfx/pfxcore/views/parameters/subset_page_subset.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|