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.
Files changed (137) hide show
  1. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/PKG-INFO +1 -1
  2. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/django_pfx.egg-info/PKG-INFO +1 -1
  3. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/django_pfx.egg-info/SOURCES.txt +4 -0
  4. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/default_settings.py +5 -0
  5. django-pfx-1.4.dev24/pfx/pfxcore/locale/fr/LC_MESSAGES/django.mo +0 -0
  6. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/locale/fr/LC_MESSAGES/django.po +89 -24
  7. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/middleware/authentication.py +13 -9
  8. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/models/__init__.py +2 -0
  9. django-pfx-1.4.dev24/pfx/pfxcore/models/abstract_pfx_base_user.py +15 -0
  10. django-pfx-1.4.dev24/pfx/pfxcore/models/login_ban.py +60 -0
  11. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/models/otp_user_mixin.py +31 -2
  12. django-pfx-1.4.dev24/pfx/pfxcore/templates/registration/otp_code_email.txt +12 -0
  13. django-pfx-1.4.dev24/pfx/pfxcore/templates/registration/otp_code_subject.txt +3 -0
  14. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/urls.py +2 -1
  15. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/views/__init__.py +1 -0
  16. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/views/authentication_views.py +153 -12
  17. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/models.py +3 -2
  18. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/tests/test_auth_api.py +332 -46
  19. django-pfx-1.4.dev18/pfx/pfxcore/locale/fr/LC_MESSAGES/django.mo +0 -0
  20. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/.gitignore +0 -0
  21. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/.gitlab-ci.yml +0 -0
  22. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/.pre-commit-config.yaml +0 -0
  23. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/LICENSE +0 -0
  24. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/MANIFEST.in +0 -0
  25. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/README.md +0 -0
  26. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/django-admin-test +0 -0
  27. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/django_pfx.egg-info/dependency_links.txt +0 -0
  28. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/django_pfx.egg-info/requires.txt +0 -0
  29. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/django_pfx.egg-info/top_level.txt +0 -0
  30. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/doc/Makefile +0 -0
  31. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/doc/conf.py +0 -0
  32. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/doc/index.rst +0 -0
  33. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/doc/source/api.views.rst +0 -0
  34. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/doc/source/authentication.md +0 -0
  35. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/doc/source/decorator.md +0 -0
  36. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/doc/source/generate_openapi.md +0 -0
  37. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/doc/source/getting_started.md +0 -0
  38. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/doc/source/internationalisation.md +0 -0
  39. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/doc/source/model.md +0 -0
  40. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/doc/source/pfx_views.md +0 -0
  41. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/doc/source/profiling.md +0 -0
  42. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/doc/source/settings.md +0 -0
  43. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/doc/source/testing.md +0 -0
  44. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/img/pfx.png +0 -0
  45. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/img/pfx.svg +0 -0
  46. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/__init__.py +0 -0
  47. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/__init__.py +0 -0
  48. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/apidoc/__init__.py +0 -0
  49. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/apidoc/parameters.py +0 -0
  50. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/apidoc/schema.py +0 -0
  51. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/apidoc/tags.py +0 -0
  52. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/apps.py +0 -0
  53. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/decorator/__init__.py +0 -0
  54. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/decorator/rest.py +0 -0
  55. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/exceptions.py +0 -0
  56. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/fields.py +0 -0
  57. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/http/__init__.py +0 -0
  58. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/http/json_response.py +0 -0
  59. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/management/__init__.py +0 -0
  60. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/management/commands/__init__.py +0 -0
  61. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/management/commands/makeapidoc.py +0 -0
  62. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/management/commands/profile.py +0 -0
  63. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/middleware/__init__.py +0 -0
  64. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/middleware/locale.py +0 -0
  65. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/middleware/profiling.py +0 -0
  66. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/models/cache_mixins.py +0 -0
  67. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/models/not_null_fields.py +0 -0
  68. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/models/pfx_models.py +0 -0
  69. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/models/user_filtered_queryset_mixin.py +0 -0
  70. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/serializers/__init__.py +0 -0
  71. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/serializers/json.py +0 -0
  72. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/settings.py +0 -0
  73. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/shortcuts.py +0 -0
  74. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/storage/__init__.py +0 -0
  75. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/storage/s3_storage.py +0 -0
  76. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/templates/registration/password_reset_email.txt +0 -0
  77. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/templates/registration/password_reset_subject.txt +0 -0
  78. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/templates/registration/welcome_email.txt +0 -0
  79. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/templates/registration/welcome_subject.txt +0 -0
  80. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/test.py +0 -0
  81. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/views/fields.py +0 -0
  82. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/views/filters_views.py +0 -0
  83. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/views/locale_views.py +0 -0
  84. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/__init__.py +0 -0
  85. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/date_format.py +0 -0
  86. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/groups.py +0 -0
  87. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/list_count.py +0 -0
  88. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/list_items.py +0 -0
  89. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/list_mode.py +0 -0
  90. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/list_order.py +0 -0
  91. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/list_search.py +0 -0
  92. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/media_redirect.py +0 -0
  93. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/meta_fields.py +0 -0
  94. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/meta_filters.py +0 -0
  95. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/meta_orders.py +0 -0
  96. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/subset.py +0 -0
  97. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/subset_limit.py +0 -0
  98. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/subset_offset.py +0 -0
  99. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/subset_page.py +0 -0
  100. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/subset_page_size.py +0 -0
  101. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/subset_page_subset.py +0 -0
  102. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pfx/pfxcore/views/rest_views.py +0 -0
  103. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/pyproject.toml +0 -0
  104. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/requirements.txt +0 -0
  105. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/runtest.py +0 -0
  106. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/serve-doc +0 -0
  107. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/setup.cfg +0 -0
  108. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/setup.py +0 -0
  109. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/__init__.py +0 -0
  110. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/locale/fr/LC_MESSAGES/django.po +0 -0
  111. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/settings/__init__.py +0 -0
  112. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/settings/ci.py +0 -0
  113. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/settings/common.py +0 -0
  114. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/settings/dev.py +0 -0
  115. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/settings/dev_custom_example.py +0 -0
  116. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/settings/dev_default.py +0 -0
  117. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/tests/__init__.py +0 -0
  118. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/tests/basic_api_errors.py +0 -0
  119. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/tests/basic_api_test.py +0 -0
  120. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/tests/test_api_doc.py +0 -0
  121. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/tests/test_body_mixin.py +0 -0
  122. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/tests/test_cache.py +0 -0
  123. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/tests/test_client.py +0 -0
  124. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/tests/test_fields.py +0 -0
  125. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/tests/test_filters.py +0 -0
  126. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/tests/test_locale_api.py +0 -0
  127. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/tests/test_perm_tests.py +0 -0
  128. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/tests/test_perms_api.py +0 -0
  129. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/tests/test_profiling_middleware.py +0 -0
  130. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/tests/test_shortcuts.py +0 -0
  131. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/tests/test_timezone_middleware.py +0 -0
  132. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/tests/test_tools.py +0 -0
  133. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/tests/test_user_queryset.py +0 -0
  134. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/tests/test_view_decorators.py +0 -0
  135. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/tests/test_view_fields.py +0 -0
  136. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/urls.py +0 -0
  137. {django-pfx-1.4.dev18 → django-pfx-1.4.dev24}/tests/views.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: django-pfx
3
- Version: 1.4.dev18
3
+ Version: 1.4.dev24
4
4
  Summary: Django PFX is a toolkit designed to streamline the development of RESTful APIs using the Django framework.
5
5
  Author: Hervé Martinet
6
6
  Author-email: herve.martinet@gmail.com
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: django-pfx
3
- Version: 1.4.dev18
3
+ Version: 1.4.dev24
4
4
  Summary: Django PFX is a toolkit designed to streamline the development of RESTful APIs using the Django framework.
5
5
  Author: Hervé Martinet
6
6
  Author-email: herve.martinet@gmail.com
@@ -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 = {}
@@ -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-03-28 11:03+0100\n"
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/otp_user_mixin.py:8
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:11
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:221 views/authentication_views.py:378
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:226
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:229 views/authentication_views.py:387
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:313
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:315
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:420
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:458
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:459 views/authentication_views.py:493
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:492
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:740
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, settings.PFX_SECRET_KEY,
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
- raise
48
- except DecodeError as e:
49
- logger.exception(e)
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: # pragma: no cover
56
+ except (DecodeError, Exception) as e:
57
+ # Always logs unexpected exceptions
54
58
  logger.exception(e)
55
59
  raise
56
60
 
@@ -1,4 +1,6 @@
1
+ from .abstract_pfx_base_user import AbstractPFXBaseUser
1
2
  from .cache_mixins import CacheableMixin, CacheDependsMixin
3
+ from .login_ban import LoginBan
2
4
  from .not_null_fields import (
3
5
  NotNullCharField,
4
6
  NotNullTextField,
@@ -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 django.conf import settings
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
- return totp.verify(otp_code)
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 %}
@@ -0,0 +1,3 @@
1
+ {% load i18n %}{% autoescape off %}
2
+ {% blocktrans %}New authentication code for {{ site_name }}{% endblocktrans %}
3
+ {% endautoescape %}
@@ -4,4 +4,5 @@ urlpatterns = register_views(
4
4
  views.LocaleRestView,
5
5
  views.AuthenticationView,
6
6
  views.SignupView,
7
- views.ForgottenPasswordView)
7
+ views.ForgottenPasswordView,
8
+ views.OtpEmailView)
@@ -1,6 +1,7 @@
1
1
  from .authentication_views import (
2
2
  AuthenticationView,
3
3
  ForgottenPasswordView,
4
+ OtpEmailView,
4
5
  SendMessageTokenMixin,
5
6
  SignupView,
6
7
  )