django-pfx 1.4.dev22__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.dev22 → django-pfx-1.4.dev24}/PKG-INFO +1 -1
  2. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/django_pfx.egg-info/PKG-INFO +1 -1
  3. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/django_pfx.egg-info/SOURCES.txt +2 -0
  4. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/default_settings.py +1 -0
  5. django-pfx-1.4.dev24/pfx/pfxcore/locale/fr/LC_MESSAGES/django.mo +0 -0
  6. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/locale/fr/LC_MESSAGES/django.po +89 -24
  7. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/models/otp_user_mixin.py +27 -2
  8. django-pfx-1.4.dev24/pfx/pfxcore/templates/registration/otp_code_email.txt +12 -0
  9. django-pfx-1.4.dev24/pfx/pfxcore/templates/registration/otp_code_subject.txt +3 -0
  10. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/urls.py +2 -1
  11. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/views/__init__.py +1 -0
  12. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/views/authentication_views.py +108 -5
  13. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/tests/tests/test_auth_api.py +116 -0
  14. django-pfx-1.4.dev22/pfx/pfxcore/locale/fr/LC_MESSAGES/django.mo +0 -0
  15. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/.gitignore +0 -0
  16. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/.gitlab-ci.yml +0 -0
  17. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/.pre-commit-config.yaml +0 -0
  18. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/LICENSE +0 -0
  19. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/MANIFEST.in +0 -0
  20. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/README.md +0 -0
  21. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/django-admin-test +0 -0
  22. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/django_pfx.egg-info/dependency_links.txt +0 -0
  23. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/django_pfx.egg-info/requires.txt +0 -0
  24. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/django_pfx.egg-info/top_level.txt +0 -0
  25. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/doc/Makefile +0 -0
  26. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/doc/conf.py +0 -0
  27. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/doc/index.rst +0 -0
  28. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/doc/source/api.views.rst +0 -0
  29. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/doc/source/authentication.md +0 -0
  30. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/doc/source/decorator.md +0 -0
  31. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/doc/source/generate_openapi.md +0 -0
  32. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/doc/source/getting_started.md +0 -0
  33. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/doc/source/internationalisation.md +0 -0
  34. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/doc/source/model.md +0 -0
  35. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/doc/source/pfx_views.md +0 -0
  36. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/doc/source/profiling.md +0 -0
  37. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/doc/source/settings.md +0 -0
  38. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/doc/source/testing.md +0 -0
  39. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/img/pfx.png +0 -0
  40. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/img/pfx.svg +0 -0
  41. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/__init__.py +0 -0
  42. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/__init__.py +0 -0
  43. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/apidoc/__init__.py +0 -0
  44. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/apidoc/parameters.py +0 -0
  45. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/apidoc/schema.py +0 -0
  46. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/apidoc/tags.py +0 -0
  47. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/apps.py +0 -0
  48. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/decorator/__init__.py +0 -0
  49. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/decorator/rest.py +0 -0
  50. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/exceptions.py +0 -0
  51. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/fields.py +0 -0
  52. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/http/__init__.py +0 -0
  53. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/http/json_response.py +0 -0
  54. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/management/__init__.py +0 -0
  55. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/management/commands/__init__.py +0 -0
  56. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/management/commands/makeapidoc.py +0 -0
  57. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/management/commands/profile.py +0 -0
  58. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/middleware/__init__.py +0 -0
  59. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/middleware/authentication.py +0 -0
  60. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/middleware/locale.py +0 -0
  61. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/middleware/profiling.py +0 -0
  62. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/models/__init__.py +0 -0
  63. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/models/abstract_pfx_base_user.py +0 -0
  64. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/models/cache_mixins.py +0 -0
  65. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/models/login_ban.py +0 -0
  66. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/models/not_null_fields.py +0 -0
  67. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/models/pfx_models.py +0 -0
  68. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/models/user_filtered_queryset_mixin.py +0 -0
  69. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/serializers/__init__.py +0 -0
  70. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/serializers/json.py +0 -0
  71. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/settings.py +0 -0
  72. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/shortcuts.py +0 -0
  73. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/storage/__init__.py +0 -0
  74. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/storage/s3_storage.py +0 -0
  75. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/templates/registration/password_reset_email.txt +0 -0
  76. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/templates/registration/password_reset_subject.txt +0 -0
  77. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/templates/registration/welcome_email.txt +0 -0
  78. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/templates/registration/welcome_subject.txt +0 -0
  79. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/test.py +0 -0
  80. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/views/fields.py +0 -0
  81. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/views/filters_views.py +0 -0
  82. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/views/locale_views.py +0 -0
  83. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/__init__.py +0 -0
  84. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/date_format.py +0 -0
  85. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/groups.py +0 -0
  86. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/list_count.py +0 -0
  87. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/list_items.py +0 -0
  88. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/list_mode.py +0 -0
  89. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/list_order.py +0 -0
  90. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/list_search.py +0 -0
  91. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/media_redirect.py +0 -0
  92. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/meta_fields.py +0 -0
  93. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/meta_filters.py +0 -0
  94. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/meta_orders.py +0 -0
  95. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/subset.py +0 -0
  96. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/subset_limit.py +0 -0
  97. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/subset_offset.py +0 -0
  98. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/subset_page.py +0 -0
  99. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/subset_page_size.py +0 -0
  100. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/views/parameters/subset_page_subset.py +0 -0
  101. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pfx/pfxcore/views/rest_views.py +0 -0
  102. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/pyproject.toml +0 -0
  103. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/requirements.txt +0 -0
  104. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/runtest.py +0 -0
  105. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/serve-doc +0 -0
  106. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/setup.cfg +0 -0
  107. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/setup.py +0 -0
  108. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/tests/__init__.py +0 -0
  109. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/tests/locale/fr/LC_MESSAGES/django.po +0 -0
  110. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/tests/models.py +0 -0
  111. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/tests/settings/__init__.py +0 -0
  112. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/tests/settings/ci.py +0 -0
  113. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/tests/settings/common.py +0 -0
  114. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/tests/settings/dev.py +0 -0
  115. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/tests/settings/dev_custom_example.py +0 -0
  116. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/tests/settings/dev_default.py +0 -0
  117. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/tests/tests/__init__.py +0 -0
  118. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/tests/tests/basic_api_errors.py +0 -0
  119. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/tests/tests/basic_api_test.py +0 -0
  120. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/tests/tests/test_api_doc.py +0 -0
  121. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/tests/tests/test_body_mixin.py +0 -0
  122. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/tests/tests/test_cache.py +0 -0
  123. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/tests/tests/test_client.py +0 -0
  124. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/tests/tests/test_fields.py +0 -0
  125. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/tests/tests/test_filters.py +0 -0
  126. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/tests/tests/test_locale_api.py +0 -0
  127. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/tests/tests/test_perm_tests.py +0 -0
  128. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/tests/tests/test_perms_api.py +0 -0
  129. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/tests/tests/test_profiling_middleware.py +0 -0
  130. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/tests/tests/test_shortcuts.py +0 -0
  131. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/tests/tests/test_timezone_middleware.py +0 -0
  132. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/tests/tests/test_tools.py +0 -0
  133. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/tests/tests/test_user_queryset.py +0 -0
  134. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/tests/tests/test_view_decorators.py +0 -0
  135. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/tests/tests/test_view_fields.py +0 -0
  136. {django-pfx-1.4.dev22 → django-pfx-1.4.dev24}/tests/urls.py +0 -0
  137. {django-pfx-1.4.dev22 → 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.dev22
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.dev22
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
@@ -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
@@ -1,6 +1,7 @@
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,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"
@@ -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,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
- 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
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 %}
@@ -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
  )
@@ -417,7 +417,7 @@ class AuthenticationView(
417
417
  raise UnauthorizedError()
418
418
 
419
419
  @method_decorator(never_cache)
420
- @rest_api("/otp/activate", public=False, method="put")
420
+ @rest_api("/otp/activate", public=False, method="put", priority_doc=52)
421
421
  def otp_activate(self, *args, **kwargs):
422
422
  """Entrypoint for :code:`PUT /otp/activate` route.
423
423
 
@@ -453,7 +453,7 @@ class AuthenticationView(
453
453
 
454
454
  @method_decorator(never_cache)
455
455
  @rest_api(
456
- "/otp/confirm", public=False, method="put",
456
+ "/otp/confirm", public=False, method="put", priority_doc=53,
457
457
  response_schema='message_schema')
458
458
  def otp_confirm(self, *args, **kwargs):
459
459
  """Entrypoint for :code:`PUT /otp/confirm` route.
@@ -488,7 +488,7 @@ class AuthenticationView(
488
488
  return JsonResponse(dict(message=_("Invalid OTP code")), status=422)
489
489
 
490
490
  @method_decorator(never_cache)
491
- @rest_api("/otp/disable", public=False, method="put")
491
+ @rest_api("/otp/disable", public=False, method="put", priority_doc=54)
492
492
  def otp_disable(self, *args, **kwargs):
493
493
  """Entrypoint for :code:`PUT /otp/disable` route.
494
494
 
@@ -523,7 +523,7 @@ class AuthenticationView(
523
523
 
524
524
  @method_decorator(never_cache)
525
525
  @rest_api(
526
- "/otp/login", public=True, method="post",
526
+ "/otp/login", public=True, method="post", priority_doc=50,
527
527
  response_schema='login_schema')
528
528
  def otp_login(self, *args, **kwargs):
529
529
  """Entrypoint for :code:`PUT /otp/login` route.
@@ -557,7 +557,6 @@ class AuthenticationView(
557
557
  description: If the token is missing, invalid or expired.
558
558
  403:
559
559
  description: If the OTP is disabled for this user.
560
-
561
560
  """
562
561
  data = self.deserialize_body()
563
562
  token = data.get('token')
@@ -779,3 +778,107 @@ class ForgottenPasswordView(SendMessageTokenMixin, BodyMixin, BaseRestView):
779
778
  'you will receive an email from us with '
780
779
  'instructions to reset your password.')
781
780
  })
781
+
782
+
783
+ @rest_view("/auth/otp")
784
+ class OtpEmailView(BodyMixin, JWTTokenDecodeMixin, BaseRestView):
785
+ """View for forgotten password service."""
786
+ #: The email template.
787
+ email_template_name = 'registration/otp_code_email.txt'
788
+ #: The email subject template.
789
+ subject_template_name = 'registration/otp_code_subject.txt'
790
+ #: The HTML email template.
791
+ html_email_template_name = None
792
+ #: Extra email context.
793
+ extra_email_context = None
794
+ #: The from value of the email.
795
+ from_email = None
796
+ #: The email field of user model.
797
+ email_field = 'email'
798
+ #: The language field of user model
799
+ language_field = 'language'
800
+ #: The ApiDoc tags.
801
+ tags = [AUTHENTICATION_TAG]
802
+
803
+ @method_decorator(never_cache)
804
+ @rest_api(
805
+ "/email", public=True, method="post", priority_doc=51,
806
+ response_schema='message_schema')
807
+ def sent_email(self, *args, **kwargs):
808
+ """Entrypoint for :code:`POST /email` route.
809
+
810
+ Request a new OTP code by email.
811
+
812
+ :returns: The JSON response
813
+ :rtype: :class:`JsonResponse`
814
+ ---
815
+ post:
816
+ summary: Send OTP email
817
+ description: Request a new OTP code by email.
818
+ requestBody:
819
+ content:
820
+ application/json:
821
+ schema:
822
+ properties:
823
+ token:
824
+ type: string
825
+ description: a valid JWT user token. This
826
+ is required only if the user is not
827
+ already connected (with a Bearer token
828
+ or a Cookie).
829
+ responses:
830
+ 401:
831
+ description: If the token is missing, invalid or expired.
832
+ 403:
833
+ description: If the OTP is disabled for this user.
834
+ """
835
+ if self.request.user.is_anonymous:
836
+ data = self.deserialize_body()
837
+ try:
838
+ user, __, __ = self.decode_jwt(
839
+ data.get('token'), otp_login=True)
840
+ except Exception as e:
841
+ logger.exception(e)
842
+ raise UnauthorizedError()
843
+ else:
844
+ user = self.request.user
845
+ if not isinstance(user, OtpUserMixin):
846
+ logger.error("User must inherit OtpUserMixin to activate OTP")
847
+ raise NotFoundError()
848
+ if not user.otp_secret_token:
849
+ raise ForbiddenError()
850
+ self.send_otp_message(user)
851
+ return JsonResponse({
852
+ 'message': _('A new authentication code has been sent by email.')})
853
+
854
+ def send_otp_message(self, user):
855
+ """Send an email to a user with an OTP code.
856
+
857
+ :param user: The user
858
+ """
859
+ from django.utils import translation
860
+ lang = str(getattr(user, self.language_field, settings.LANGUAGE_CODE))
861
+
862
+ otp_code = user.get_hotp_code()
863
+ data = {
864
+ 'target_user': user,
865
+ 'otp_code': otp_code,
866
+ 'otp_validity': settings.PFX_HOTP_CODE_VALIDITY,
867
+ 'site_name': settings.PFX_SITE_NAME,
868
+ 'user': user,
869
+ **(self.extra_email_context or {})
870
+ }
871
+ with translation.override(lang):
872
+ subject = loader.render_to_string(self.subject_template_name, data)
873
+ # Email subject *must not* contain newlines
874
+ subject = ''.join(subject.splitlines())
875
+ body = loader.render_to_string(self.email_template_name, data)
876
+ email_message = EmailMultiAlternatives(
877
+ subject, body, self.from_email,
878
+ [getattr(user, self.email_field)])
879
+ if self.html_email_template_name is not None:
880
+ html_email = loader.render_to_string(
881
+ self.html_email_template_name, data)
882
+ email_message.attach_alternative(
883
+ html_email, 'text/html')
884
+ 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 = int(code_match.group(1))
1291
+
1292
+ with freeze_time("2023-05-01 08:15:01"):
1293
+ response = self.client.post('/api/auth/otp/login', dict(
1294
+ token=otp_token,
1295
+ otp_code=otp_code))
1296
+ self.assertRC(response, 422)
1297
+
1298
+ with freeze_time("2023-05-01 08:15:00"):
1299
+ response = self.client.post('/api/auth/otp/login', dict(
1300
+ token=otp_token,
1301
+ otp_code=otp_code))
1302
+ self.assertRC(response, 200)
1303
+ self.assertJEExists(response, 'token')
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)
File without changes
File without changes
File without changes
File without changes
File without changes