django-pfx 1.7.3.dev26__tar.gz → 1.7.3.dev35__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 (283) hide show
  1. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/PKG-INFO +1 -1
  2. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/django_pfx.egg-info/PKG-INFO +1 -1
  3. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/django_pfx.egg-info/SOURCES.txt +2 -0
  4. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/django_pfx.egg-info/scm_file_list.json +2 -0
  5. django_pfx-1.7.3.dev35/django_pfx.egg-info/scm_version.json +8 -0
  6. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/doc/source/api.views.rst +5 -0
  7. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/doc/source/authentication.md +72 -9
  8. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/exceptions.py +4 -5
  9. django_pfx-1.7.3.dev35/pfx/pfxcore/locale/fr/LC_MESSAGES/django.mo +0 -0
  10. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/locale/fr/LC_MESSAGES/django.po +70 -77
  11. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/mfa/base.py +8 -0
  12. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/models/mfa_user_mixin.py +8 -1
  13. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/views/__init__.py +1 -0
  14. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/views/authentication_views.py +13 -424
  15. django_pfx-1.7.3.dev35/pfx/pfxcore/views/mfa_authentication_views.py +523 -0
  16. django_pfx-1.7.3.dev35/tests/mfa_urls.py +41 -0
  17. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/tests/test_auth_api.py +19 -0
  18. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/tests/test_fields_one2many.py +25 -0
  19. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/tests/test_mfa_login_api.py +9 -5
  20. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/tests/test_mfa_setup_api.py +152 -0
  21. django_pfx-1.7.3.dev26/django_pfx.egg-info/scm_version.json +0 -8
  22. django_pfx-1.7.3.dev26/pfx/pfxcore/locale/fr/LC_MESSAGES/django.mo +0 -0
  23. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/.gitignore +0 -0
  24. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/.gitlab-ci.yml +0 -0
  25. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/.pre-commit-config.yaml +0 -0
  26. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/LICENSE +0 -0
  27. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/MANIFEST.in +0 -0
  28. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/README.md +0 -0
  29. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/django_pfx.egg-info/dependency_links.txt +0 -0
  30. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/django_pfx.egg-info/requires.txt +0 -0
  31. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/django_pfx.egg-info/top_level.txt +0 -0
  32. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/doc/Makefile +0 -0
  33. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/doc/conf.py +0 -0
  34. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/doc/index.rst +0 -0
  35. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/doc/source/decorator.md +0 -0
  36. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/doc/source/generate_openapi.md +0 -0
  37. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/doc/source/getting_started.md +0 -0
  38. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/doc/source/internationalisation.md +0 -0
  39. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/doc/source/model.md +0 -0
  40. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/doc/source/pfx_views.md +0 -0
  41. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/doc/source/profiling.md +0 -0
  42. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/doc/source/settings.md +0 -0
  43. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/doc/source/testing.md +0 -0
  44. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/img/pfx.png +0 -0
  45. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/img/pfx.svg +0 -0
  46. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/make_messages +0 -0
  47. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/manage.py +0 -0
  48. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/__init__.py +0 -0
  49. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/emails/__init__.py +0 -0
  50. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/emails/apps.py +0 -0
  51. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/emails/email_backend/__init__.py +0 -0
  52. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/emails/email_backend/q_mailjet_email_backend.py +0 -0
  53. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/emails/exceptions.py +0 -0
  54. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/emails/mailjet/__init__.py +0 -0
  55. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/emails/mailjet/mailjet.py +0 -0
  56. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/emails/mailjet/message_result.py +0 -0
  57. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/emails/migrations/0001_initial.py +0 -0
  58. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/emails/migrations/0002_alter_email_headers.py +0 -0
  59. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/emails/migrations/0003_alter_email_options.py +0 -0
  60. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/emails/migrations/__init__.py +0 -0
  61. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/emails/models/__init__.py +0 -0
  62. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/emails/models/email.py +0 -0
  63. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/emails/providers/__init__.py +0 -0
  64. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/emails/providers/email_django_q_executor.py +0 -0
  65. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/emails/shortcuts.py +0 -0
  66. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/emails/tasks.py +0 -0
  67. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/emails/templates/emails/base_template.html +0 -0
  68. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/emails/templates/emails/base_template.txt +0 -0
  69. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/emails/templates/emails/base_template_subject.txt +0 -0
  70. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/emails/urls.py +0 -0
  71. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/emails/views/__init__.py +0 -0
  72. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/emails/views/email_rest_view.py +0 -0
  73. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/__init__.py +0 -0
  74. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/apidoc/__init__.py +0 -0
  75. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/apidoc/parameters.py +0 -0
  76. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/apidoc/schema.py +0 -0
  77. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/apidoc/tags.py +0 -0
  78. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/apps.py +0 -0
  79. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/decorator/__init__.py +0 -0
  80. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/decorator/rest.py +0 -0
  81. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/default_settings.py +0 -0
  82. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/fields/__init__.py +0 -0
  83. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/fields/decimal_field.py +0 -0
  84. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/fields/media_field.py +0 -0
  85. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/fields/minutes_duration_field.py +0 -0
  86. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/fields/rich_text_field.py +0 -0
  87. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/http/__init__.py +0 -0
  88. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/http/json_response.py +0 -0
  89. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/management/__init__.py +0 -0
  90. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/management/commands/__init__.py +0 -0
  91. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/management/commands/makeapidoc.py +0 -0
  92. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/management/commands/profile.py +0 -0
  93. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/mfa/__init__.py +0 -0
  94. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/mfa/authenticator.py +0 -0
  95. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/mfa/email.py +0 -0
  96. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/mfa/legacy.py +0 -0
  97. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/mfa/sms.py +0 -0
  98. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/middleware/__init__.py +0 -0
  99. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/middleware/authentication.py +0 -0
  100. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/middleware/locale.py +0 -0
  101. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/middleware/profiling.py +0 -0
  102. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/middleware/request_too_big_middleware.py +0 -0
  103. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/middleware/ws_cookie_authentication_middleware.py +0 -0
  104. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/migrations/0001_initial.py +0 -0
  105. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/migrations/0002_pfxpermissionsuser.py +0 -0
  106. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/migrations/0003_delete_pfxpermissionsuser.py +0 -0
  107. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/migrations/0004_alter_loginban_failed_counter_and_more.py +0 -0
  108. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/migrations/0005_mfadevice_mfadevice_mfa_device_unique_backend.py +0 -0
  109. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/migrations/__init__.py +0 -0
  110. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/migrations/operations/__init__.py +0 -0
  111. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/migrations/operations/permissions.py +0 -0
  112. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/models/__init__.py +0 -0
  113. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/models/abstract_pfx_base_user.py +0 -0
  114. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/models/attachment_mixin.py +0 -0
  115. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/models/cache_mixins.py +0 -0
  116. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/models/login_ban.py +0 -0
  117. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/models/mfa_device.py +0 -0
  118. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/models/not_null_fields.py +0 -0
  119. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/models/ordered_model_mixin.py +0 -0
  120. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/models/pfx_models.py +0 -0
  121. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/models/pfx_user.py +0 -0
  122. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/models/user_filtered_queryset_mixin.py +0 -0
  123. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/serializers/__init__.py +0 -0
  124. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/serializers/json.py +0 -0
  125. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/settings.py +0 -0
  126. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/shortcuts.py +0 -0
  127. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/sms/__init__.py +0 -0
  128. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/sms/backends/__init__.py +0 -0
  129. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/sms/backends/base.py +0 -0
  130. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/sms/backends/console.py +0 -0
  131. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/storage/__init__.py +0 -0
  132. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/storage/exceptions.py +0 -0
  133. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/storage/local_storage.py +0 -0
  134. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/storage/s3_storage.py +0 -0
  135. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/templates/registration/otp_code_email.txt +0 -0
  136. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/templates/registration/otp_code_subject.txt +0 -0
  137. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/templates/registration/password_reset_email.txt +0 -0
  138. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/templates/registration/password_reset_subject.txt +0 -0
  139. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/templates/registration/welcome_email.txt +0 -0
  140. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/templates/registration/welcome_subject.txt +0 -0
  141. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/test.py +0 -0
  142. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/urls.py +0 -0
  143. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/views/fields.py +0 -0
  144. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/views/filters_views.py +0 -0
  145. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/views/locale_views.py +0 -0
  146. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/views/media_rest_view_mixin.py +0 -0
  147. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/views/ordered_rest_view_mixin.py +0 -0
  148. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/views/parameters/__init__.py +0 -0
  149. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/views/parameters/date_format.py +0 -0
  150. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/views/parameters/groups.py +0 -0
  151. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/views/parameters/list_count.py +0 -0
  152. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/views/parameters/list_items.py +0 -0
  153. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/views/parameters/list_mode.py +0 -0
  154. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/views/parameters/list_order.py +0 -0
  155. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/views/parameters/list_search.py +0 -0
  156. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/views/parameters/media_redirect.py +0 -0
  157. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/views/parameters/meta_fields.py +0 -0
  158. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/views/parameters/meta_filters.py +0 -0
  159. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/views/parameters/meta_orders.py +0 -0
  160. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/views/parameters/subset.py +0 -0
  161. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/views/parameters/subset_limit.py +0 -0
  162. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/views/parameters/subset_offset.py +0 -0
  163. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/views/parameters/subset_page.py +0 -0
  164. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/views/parameters/subset_page_size.py +0 -0
  165. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/views/parameters/subset_page_subset.py +0 -0
  166. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/pfxcore/views/rest_views.py +0 -0
  167. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/settings/__init__.py +0 -0
  168. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/settings/dev.py +0 -0
  169. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/userjobs/__init__.py +0 -0
  170. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/userjobs/apps.py +0 -0
  171. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/userjobs/locale/fr/LC_MESSAGES/django.mo +0 -0
  172. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/userjobs/locale/fr/LC_MESSAGES/django.po +0 -0
  173. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/userjobs/migrations/0001_initial.py +0 -0
  174. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/userjobs/migrations/0002_alter_userjob_options_alter_userjob_auto_queued_and_more.py +0 -0
  175. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/userjobs/migrations/0003_clean_scheduled_tasks.py +0 -0
  176. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/userjobs/migrations/__init__.py +0 -0
  177. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/userjobs/models/__init__.py +0 -0
  178. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/userjobs/models/user_job.py +0 -0
  179. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/userjobs/models/user_job_attachment.py +0 -0
  180. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/userjobs/providers/__init__.py +0 -0
  181. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/userjobs/providers/user_job_django_q_executor.py +0 -0
  182. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/userjobs/providers/user_job_websocket_channel_provider.py +0 -0
  183. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/userjobs/tasks.py +0 -0
  184. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/userjobs/urls.py +0 -0
  185. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/userjobs/views/__init__.py +0 -0
  186. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/userjobs/views/user_job_attachment_view.py +0 -0
  187. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/userjobs/views/user_job_view.py +0 -0
  188. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pfx/userjobs/views/user_websocket_consumer.py +0 -0
  189. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/pyproject.toml +0 -0
  190. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/requirements.txt +0 -0
  191. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/serve-doc +0 -0
  192. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/setup.cfg +0 -0
  193. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/setup.py +0 -0
  194. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/__init__.py +0 -0
  195. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/apps.py +0 -0
  196. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/channel_group_send_calls.py +0 -0
  197. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/locale/fr/LC_MESSAGES/django.po +0 -0
  198. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/migrations/0001_initial.py +0 -0
  199. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/migrations/0002_alter_book_cover.py +0 -0
  200. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/migrations/0003_book_local_file.py +0 -0
  201. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/migrations/0004_mfausermixin_fields.py +0 -0
  202. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/migrations/0005_mfausermixin_fields_fix.py +0 -0
  203. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/migrations/0006_rename_otp_enabled_user_mfa_authenticator_enabled_and_more.py +0 -0
  204. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/migrations/0007_attachment.py +0 -0
  205. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/migrations/0008_remove_user_hotp_count_remove_user_hotp_expiry_and_more.py +0 -0
  206. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/migrations/__init__.py +0 -0
  207. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/models.py +0 -0
  208. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/settings/__init__.py +0 -0
  209. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/settings/ci.py +0 -0
  210. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/settings/common.py +0 -0
  211. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/settings/dev.py +0 -0
  212. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/settings/dev_custom_example.py +0 -0
  213. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/settings/dev_default.py +0 -0
  214. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/tests/__init__.py +0 -0
  215. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/tests/basic_api_errors.py +0 -0
  216. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/tests/basic_api_test.py +0 -0
  217. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/tests/mfa_test_utils.py +0 -0
  218. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/tests/test_api_doc.py +0 -0
  219. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/tests/test_api_doc_search.py +0 -0
  220. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/tests/test_body_mixin.py +0 -0
  221. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/tests/test_cache.py +0 -0
  222. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/tests/test_client.py +0 -0
  223. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/tests/test_emails_email_api.py +0 -0
  224. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/tests/test_emails_email_model.py +0 -0
  225. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/tests/test_emails_q_mailjet_email_backend.py +0 -0
  226. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/tests/test_emails_send_email_message.py +0 -0
  227. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/tests/test_fields_choices.py +0 -0
  228. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/tests/test_fields_date.py +0 -0
  229. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/tests/test_fields_decimal.py +0 -0
  230. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/tests/test_fields_minutes_duration.py +0 -0
  231. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/tests/test_fields_rich_text.py +0 -0
  232. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/tests/test_fields_time.py +0 -0
  233. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/tests/test_filters.py +0 -0
  234. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/tests/test_locale_api.py +0 -0
  235. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/tests/test_mfa_legacy.py +0 -0
  236. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/tests/test_ordered_rest_view_mixin.py +0 -0
  237. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/tests/test_perm_tests.py +0 -0
  238. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/tests/test_permissions.py +0 -0
  239. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/tests/test_perms_api.py +0 -0
  240. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/tests/test_post_migrate_groups_update.py +0 -0
  241. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/tests/test_profiling_middleware.py +0 -0
  242. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/tests/test_settings.py +0 -0
  243. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/tests/test_shortcuts.py +0 -0
  244. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/tests/test_timezone_middleware.py +0 -0
  245. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/tests/test_tools.py +0 -0
  246. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/tests/test_user_job_api.py +0 -0
  247. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/tests/test_user_job_messages.py +0 -0
  248. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/tests/test_user_job_model.py +0 -0
  249. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/tests/test_user_queryset.py +0 -0
  250. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/tests/test_user_websocket.py +0 -0
  251. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/tests/test_view_decorators.py +0 -0
  252. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/tests/test_view_fields.py +0 -0
  253. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/urls.py +0 -0
  254. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests/views.py +0 -0
  255. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests_base_user/__init__.py +0 -0
  256. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests_base_user/migrations/0001_initial.py +0 -0
  257. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests_base_user/migrations/__init__.py +0 -0
  258. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests_base_user/settings/__init__.py +0 -0
  259. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests_base_user/settings/ci.py +0 -0
  260. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests_base_user/settings/common.py +0 -0
  261. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests_base_user/settings/dev.py +0 -0
  262. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests_base_user/settings/dev_custom_example.py +0 -0
  263. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests_base_user/settings/dev_default.py +0 -0
  264. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests_base_user/tests/__init__.py +0 -0
  265. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests_base_user/tests/test_api.py +0 -0
  266. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests_base_user/tests/test_auth_api.py +0 -0
  267. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests_base_user/urls.py +0 -0
  268. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests_base_user/views.py +0 -0
  269. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests_custom_user/__init__.py +0 -0
  270. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests_custom_user/migrations/0001_initial.py +0 -0
  271. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests_custom_user/migrations/__init__.py +0 -0
  272. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests_custom_user/models.py +0 -0
  273. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests_custom_user/settings/__init__.py +0 -0
  274. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests_custom_user/settings/ci.py +0 -0
  275. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests_custom_user/settings/common.py +0 -0
  276. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests_custom_user/settings/dev.py +0 -0
  277. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests_custom_user/settings/dev_custom_example.py +0 -0
  278. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests_custom_user/settings/dev_default.py +0 -0
  279. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests_custom_user/tests/__init__.py +0 -0
  280. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests_custom_user/tests/test_api.py +0 -0
  281. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests_custom_user/tests/test_auth_api.py +0 -0
  282. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests_custom_user/urls.py +0 -0
  283. {django_pfx-1.7.3.dev26 → django_pfx-1.7.3.dev35}/tests_custom_user/views.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-pfx
3
- Version: 1.7.3.dev26
3
+ Version: 1.7.3.dev35
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.4
2
2
  Name: django-pfx
3
- Version: 1.7.3.dev26
3
+ Version: 1.7.3.dev35
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
@@ -140,6 +140,7 @@ pfx/pfxcore/views/fields.py
140
140
  pfx/pfxcore/views/filters_views.py
141
141
  pfx/pfxcore/views/locale_views.py
142
142
  pfx/pfxcore/views/media_rest_view_mixin.py
143
+ pfx/pfxcore/views/mfa_authentication_views.py
143
144
  pfx/pfxcore/views/ordered_rest_view_mixin.py
144
145
  pfx/pfxcore/views/rest_views.py
145
146
  pfx/pfxcore/views/parameters/__init__.py
@@ -185,6 +186,7 @@ pfx/userjobs/views/user_websocket_consumer.py
185
186
  tests/__init__.py
186
187
  tests/apps.py
187
188
  tests/channel_group_send_calls.py
189
+ tests/mfa_urls.py
188
190
  tests/models.py
189
191
  tests/urls.py
190
192
  tests/views.py
@@ -14,6 +14,7 @@
14
14
  "setup.py",
15
15
  "setup.cfg",
16
16
  "tests/channel_group_send_calls.py",
17
+ "tests/mfa_urls.py",
17
18
  "tests/urls.py",
18
19
  "tests/views.py",
19
20
  "tests/__init__.py",
@@ -124,6 +125,7 @@
124
125
  "pfx/pfxcore/middleware/profiling.py",
125
126
  "pfx/pfxcore/middleware/request_too_big_middleware.py",
126
127
  "pfx/pfxcore/views/authentication_views.py",
128
+ "pfx/pfxcore/views/mfa_authentication_views.py",
127
129
  "pfx/pfxcore/views/filters_views.py",
128
130
  "pfx/pfxcore/views/media_rest_view_mixin.py",
129
131
  "pfx/pfxcore/views/locale_views.py",
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "1.7.2",
3
+ "distance": 35,
4
+ "node": "gd7bce9634adc6a4594cbaad209c36133aba3e845",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-07-14"
8
+ }
@@ -151,6 +151,11 @@ Base services
151
151
  :undoc-members:
152
152
  :show-inheritance:
153
153
 
154
+ .. autoclass:: pfx.pfxcore.views.MFAAuthenticationView
155
+ :members:
156
+ :undoc-members:
157
+ :show-inheritance:
158
+
154
159
  .. autoclass:: pfx.pfxcore.views.SignupView
155
160
  :members:
156
161
  :undoc-members:
@@ -126,6 +126,31 @@ class MyUser(MFAUserMixin, AbstractPFXBaseUser):
126
126
  pass
127
127
  ```
128
128
 
129
+ Finally, the MFA endpoints (OTP login, challenge and backend management) are provided by
130
+ {class}`pfx.pfxcore.views.MFAAuthenticationView`, a subclass of
131
+ {class}`pfx.pfxcore.views.AuthenticationView`. Register it **instead of** the plain
132
+ `AuthenticationView` in your urls:
133
+
134
+ ```python
135
+ from django.urls import path, include
136
+ from pfx.pfxcore import register_views
137
+ from pfx.pfxcore.views import MFAAuthenticationView
138
+
139
+ urlpatterns = [
140
+ path('api/', include(register_views(MFAAuthenticationView))),
141
+ ]
142
+ ```
143
+
144
+ With the default `AuthenticationView`, the `/auth/otp/login` and `/auth/mfa/*` endpoints are not
145
+ registered and login never returns `need_otp` — MFA is effectively disabled regardless of the user
146
+ mixin or settings.
147
+
148
+ ```{note}
149
+ **Migrating from a previous version.** The MFA endpoints used to live on `AuthenticationView`; they
150
+ have moved to {class}`pfx.pfxcore.views.MFAAuthenticationView`. If you rely on MFA, register that
151
+ view (see above) in place of `AuthenticationView`.
152
+ ```
153
+
129
154
  ```{note}
130
155
  **Migrating from a previous version.** The MFA state used to live directly on the user table
131
156
  (`otp_secret_token`, `mfa_*_enabled`, `sms_phone_number`, …) with a single shared secret. It now
@@ -147,9 +172,12 @@ removed.
147
172
  {class}`pfx.pfxcore.mfa.MFABackend` subclass and add its dotted path to the list; to override a
148
173
  built-in, list your subclass (keeping the same `id`) in place of the built-in path.
149
174
  * `PFX_MFA_FORCE`: If `True`, MFA is required for all users — a `mfa_setup_required` flag is returned
150
- at login when the user has not yet dismissed the setup screen (optional, default `False`).
175
+ at login as long as the user has neither configured a backend nor dismissed the setup screen
176
+ (optional, default `False`). It is cleared as soon as any backend is confirmed (email only is
177
+ enough) or the user calls `PUT /auth/mfa/dismiss`.
151
178
  A forced user who has not enrolled yet still receives an email code challenge at first login
152
- (the email backend is always available).
179
+ (the email backend is always available). While MFA is forced, the email backend cannot be
180
+ disabled (nor can MFA be disabled globally); the other backends can still be disabled.
153
181
  * `PFX_SMS_BACKEND`: Dotted path to the SMS backend class (optional, default
154
182
  `'pfx.pfxcore.sms.backends.console.ConsoleSMSBackend'`).
155
183
  Implement {class}`pfx.pfxcore.sms.backends.base.BaseSMSBackend` to integrate a real SMS provider.
@@ -178,7 +206,7 @@ In cookie mode, the JWT token is saved in an HTTP-only cookie.
178
206
  alt Authentication success
179
207
  API->>App: 200 OK + cookie
180
208
  else Authentication failed
181
- API->>App: 401 Unautorized
209
+ API->>App: 422 Unprocessable Entity
182
210
  end
183
211
 
184
212
  ```
@@ -195,14 +223,14 @@ In cookie mode, the JWT token is saved in an HTTP-only cookie.
195
223
 
196
224
  **Responses :**
197
225
 
198
- * `HTTP 401` if the credentials are incorrect
226
+ * `HTTP 422` if the credentials are incorrect (a global `__all__` error)
199
227
  * `HTTP 200` with the following body
200
228
 
201
229
  | Field | Description |
202
230
  |--------------------|------------------------------------------------------------------------|
203
231
  | token | the jwt token (only if mode is `jwt`) |
204
232
  | user | the user object |
205
- | mfa_setup_required | `true` if MFA is forced and the user has not yet dismissed setup screen |
233
+ | mfa_setup_required | `true` if MFA is forced and the user has neither configured a backend nor dismissed the setup screen |
206
234
 
207
235
 
208
236
  ### Login + MFA
@@ -228,10 +256,10 @@ triggers a challenge for out-of-band backends (email / SMS).
228
256
  API->>App: 200 OK
229
257
  note left of API: JWT token in cookie or in body <br/> + user in body
230
258
  else OTP failed
231
- API->>App: 401 Unauthorized
259
+ API->>App: 422 Unprocessable Entity
232
260
  end
233
261
  else Login failed
234
- API->>App: 401 Unauthorized
262
+ API->>App: 422 Unprocessable Entity
235
263
  end
236
264
 
237
265
  ```
@@ -248,7 +276,7 @@ triggers a challenge for out-of-band backends (email / SMS).
248
276
 
249
277
  **Responses :**
250
278
 
251
- * `HTTP 401` if the credentials are incorrect
279
+ * `HTTP 422` if the credentials are incorrect (a global `__all__` error)
252
280
  * `HTTP 200` with the following body when MFA is required (`need_otp=true`)
253
281
 
254
282
  | Field | Description |
@@ -298,7 +326,7 @@ the highest-priority available backend is used.
298
326
  **Responses :**
299
327
 
300
328
  * `HTTP 401` if the temporary jwt token is incorrect
301
- * `HTTP 422` if the otp code is incorrect
329
+ * `HTTP 422` if the otp code is incorrect (an `otp_code` field error)
302
330
  * `HTTP 200` with the following body
303
331
 
304
332
  | Field | Description |
@@ -318,6 +346,14 @@ The **disable** step deactivates the backend after verifying an OTP code from **
318
346
  backend (e.g. you can disable SMS with an authenticator or email code). Disabling the email (base)
319
347
  backend disables MFA entirely. A dedicated `PUT /auth/mfa/disable` disables all backends at once.
320
348
 
349
+ To obtain that code on an out-of-band backend (email/SMS), an **authenticated** user can request a
350
+ challenge with `PUT /auth/mfa/{email|sms}/challenge` — it sends a code using the same delivery
351
+ service as the login challenge (authenticator codes are generated locally and need no request).
352
+
353
+ When MFA is forced (`PFX_MFA_FORCE`), the email backend must stay active: disabling it — or calling
354
+ `PUT /auth/mfa/disable` — is rejected with `HTTP 400`. The other backends (`authenticator`, `sms`)
355
+ can still be disabled individually.
356
+
321
357
  ```{note}
322
358
  To change an OOB target (e.g. a new phone number), disable the backend then enroll it again with
323
359
  the new value. The email backend's target is always the user's account email and is changed through
@@ -359,6 +395,19 @@ authenticated user.
359
395
  }
360
396
  ```
361
397
 
398
+ #### Request a challenge code (authenticated)
399
+
400
+ **Request:** `PUT` `/auth/mfa/{email|sms}/challenge`
401
+
402
+ Sends a challenge code on a **confirmed** out-of-band backend for the authenticated user, so they
403
+ can authorise a sensitive operation such as disabling a backend. Uses the same delivery service as
404
+ the login challenge. No request body.
405
+
406
+ **Responses:**
407
+
408
+ * `HTTP 403` if the backend is not out-of-band or is not enabled for the user
409
+ * `HTTP 200` with a confirmation message
410
+
362
411
  #### Authenticator (TOTP)
363
412
 
364
413
  **Request:** `GET` `/auth/mfa/authenticator/setup`
@@ -454,6 +503,7 @@ backends are removed too). Requires a valid OTP code from any confirmed backend.
454
503
 
455
504
  **Responses:**
456
505
 
506
+ * `HTTP 400` if MFA is forced (email cannot be disabled)
457
507
  * `HTTP 422` if the code is invalid
458
508
  * `HTTP 200` with a new JWT token and updated user object
459
509
 
@@ -529,9 +579,22 @@ Disables MFA entirely (all backends). Requires a valid OTP code from any confirm
529
579
 
530
580
  **Responses:**
531
581
 
582
+ * `HTTP 400` if MFA is forced (MFA cannot be disabled)
532
583
  * `HTTP 422` if the code is invalid
533
584
  * `HTTP 200` with a new JWT token and updated user object
534
585
 
586
+ ---
587
+
588
+ **Request:** `PUT` `/auth/mfa/dismiss`
589
+
590
+ Dismisses the MFA setup screen when MFA is forced: the user states they do not want to configure
591
+ any backend beyond email. This clears `mfa_setup_required` without enabling an extra backend (email
592
+ stays the fallback). No request body and no OTP code required.
593
+
594
+ **Responses:**
595
+
596
+ * `HTTP 200` with a new JWT token and updated user object (`mfa_setup_required` is now `false`)
597
+
535
598
 
536
599
  ### Logout
537
600
  A service that deletes the authentication cookie if it exists.
@@ -26,6 +26,8 @@ class APIError(Exception):
26
26
 
27
27
  self.status = status
28
28
  if validation_error:
29
+ # For 400 error (not validation 422 errors)
30
+ # allow to send debug validations errors in the api return
29
31
  errors = []
30
32
  errors_dict = validation_error_dict(validation_error)
31
33
  for k, v in errors_dict.items():
@@ -77,11 +79,8 @@ class RelatedModelNotFoundAPIError(ModelValidationAPIError):
77
79
  status=status, delete_cookie=delete_cookie, **kwargs)
78
80
 
79
81
 
80
- class AuthenticationError(APIError):
81
- def __init__(self, message=None, status=422, **kwargs):
82
- super().__init__(
83
- f(message or _("Login failed")),
84
- status=status, **kwargs)
82
+ class AuthenticationError(ModelValidationAPIError):
83
+ pass
85
84
 
86
85
 
87
86
  class UnauthorizedError(APIError):
@@ -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: 2026-06-23 10:26+0200\n"
10
+ "POT-Creation-Date: 2026-07-08 16:13+0200\n"
11
11
  "PO-Revision-Date: 2021-06-22 23:31+0200\n"
12
12
  "Last-Translator: \n"
13
13
  "Language-Team: \n"
@@ -28,28 +28,24 @@ msgstr ""
28
28
  "Une erreur système s’est produite. Notre équipe d’assistance en a été "
29
29
  "informée."
30
30
 
31
- #: exceptions.py:53 exceptions.py:76
31
+ #: exceptions.py:55 exceptions.py:78
32
32
  #, python-brace-format
33
33
  msgid "{model} not found."
34
34
  msgstr "{model} non trouvé."
35
35
 
36
- #: exceptions.py:60
36
+ #: exceptions.py:62
37
37
  msgid "JSON Malformed {}"
38
38
  msgstr "JSON malformé {}"
39
39
 
40
- #: exceptions.py:83
41
- msgid "Login failed"
42
- msgstr "La connexion a échoué"
43
-
44
- #: exceptions.py:90
40
+ #: exceptions.py:89
45
41
  msgid "Unauthorized"
46
42
  msgstr "Non autorisé"
47
43
 
48
- #: exceptions.py:97
44
+ #: exceptions.py:96
49
45
  msgid "Forbidden"
50
46
  msgstr "Interdit"
51
47
 
52
- #: exceptions.py:104
48
+ #: exceptions.py:103
53
49
  msgid "Resource not found"
54
50
  msgstr "Ressource non trouvée"
55
51
 
@@ -103,22 +99,20 @@ msgid "Login bans"
103
99
  msgstr "Login bannis"
104
100
 
105
101
  #: models/mfa_device.py:20
106
- #, fuzzy
107
- #| msgid "Username"
108
102
  msgid "User"
109
- msgstr "Nom d’utilisateur"
103
+ msgstr "Utilisateur"
110
104
 
111
105
  #: models/mfa_device.py:22
112
106
  msgid "Backend"
113
- msgstr ""
107
+ msgstr "Backend"
114
108
 
115
109
  #: models/mfa_device.py:24
116
110
  msgid "Secret"
117
- msgstr ""
111
+ msgstr "Secret"
118
112
 
119
113
  #: models/mfa_device.py:26
120
114
  msgid "Confirmed"
121
- msgstr ""
115
+ msgstr "Confirmé"
122
116
 
123
117
  #: models/mfa_device.py:28
124
118
  msgid "HOTP count"
@@ -130,21 +124,21 @@ msgstr "Expiration HOTP"
130
124
 
131
125
  #: models/mfa_device.py:34
132
126
  msgid "Target"
133
- msgstr ""
127
+ msgstr "Cible"
134
128
 
135
129
  #: models/mfa_device.py:37
136
130
  msgid "MFA device"
137
- msgstr ""
131
+ msgstr "Appareil MFA"
138
132
 
139
133
  #: models/mfa_device.py:38
140
134
  msgid "MFA devices"
141
- msgstr ""
135
+ msgstr "Appareils MFA"
142
136
 
143
137
  #: models/mfa_user_mixin.py:20
144
138
  msgid "MFA signature key"
145
- msgstr ""
139
+ msgstr "Clé de signature MFA"
146
140
 
147
- #: models/mfa_user_mixin.py:23
141
+ #: models/mfa_user_mixin.py:23 views/mfa_authentication_views.py:523
148
142
  msgid "MFA setup dismissed"
149
143
  msgstr "Configuration MFA annulée"
150
144
 
@@ -172,7 +166,7 @@ msgstr "Numéro de téléphone (SMS)"
172
166
  msgid "MFA setup required"
173
167
  msgstr "Configuration MFA requise"
174
168
 
175
- #: models/mfa_user_mixin.py:75
169
+ #: models/mfa_user_mixin.py:82
176
170
  msgid "Is MFA forced"
177
171
  msgstr "MFA est forcé"
178
172
 
@@ -282,7 +276,11 @@ msgstr "Bienvenue sur %(site_name)s."
282
276
  msgid "Welcome on %(site_name)s"
283
277
  msgstr "Bienvenue sur %(site_name)s"
284
278
 
285
- #: views/authentication_views.py:95
279
+ #: views/authentication_views.py:80
280
+ msgid "Login failed"
281
+ msgstr "La connexion a échoué"
282
+
283
+ #: views/authentication_views.py:89
286
284
  #, python-brace-format
287
285
  msgid ""
288
286
  "Your connection is temporarily disabled after several unsuccessful attempts, "
@@ -291,78 +289,31 @@ msgstr ""
291
289
  "Votre connexion est temporairement désactivée après plusieurs tentatives "
292
290
  "infructueuses, veuillez réessayer dans {seconds} secondes."
293
291
 
294
- #: views/authentication_views.py:191
292
+ #: views/authentication_views.py:181
295
293
  msgid "Successful login"
296
294
  msgstr "Connexion réussie"
297
295
 
298
- #: views/authentication_views.py:293
299
- msgid "This field is required."
300
- msgstr "Ce champ est requis."
301
-
302
- #: views/authentication_views.py:299 views/authentication_views.py:419
303
- msgid "Code sent."
304
- msgstr "Code envoyé."
305
-
306
- #: views/authentication_views.py:402
307
- #, fuzzy
308
- #| msgid "MFA enabled"
309
- msgid "MFA must be enabled first"
310
- msgstr "MFA activé"
311
-
312
- #: views/authentication_views.py:405
313
- #, fuzzy
314
- #| msgid "Email MFA is already enabled"
315
- msgid "This MFA backend is already enabled"
316
- msgstr "MFA par email déjà activé."
317
-
318
- #: views/authentication_views.py:416
319
- msgid "Phone number is required."
320
- msgstr "Le numéro de téléphone est requis."
321
-
322
- #: views/authentication_views.py:456
323
- #, fuzzy
324
- #| msgid "MFA enabled"
325
- msgid "MFA backend enabled"
326
- msgstr "MFA activé"
327
-
328
- #: views/authentication_views.py:458 views/authentication_views.py:493
329
- #: views/authentication_views.py:531
330
- msgid "Invalid code"
331
- msgstr "Code invalide"
332
-
333
- #: views/authentication_views.py:497
334
- #, fuzzy
335
- #| msgid "MFA enabled"
336
- msgid "MFA backend disabled"
337
- msgstr "MFA activé"
338
-
339
- #: views/authentication_views.py:535
340
- #, fuzzy
341
- #| msgid "SMS MFA disabled"
342
- msgid "MFA disabled"
343
- msgstr "MFA par SMS désactivé"
344
-
345
- #: views/authentication_views.py:598 views/authentication_views.py:766
296
+ #: views/authentication_views.py:255 views/authentication_views.py:423
346
297
  msgid "password updated successfully"
347
298
  msgstr "le mot de passe a été mis à jour avec succès"
348
299
 
349
- #: views/authentication_views.py:603
300
+ #: views/authentication_views.py:260
350
301
  msgid "Incorrect password"
351
302
  msgstr "Mot de passe incorrect"
352
303
 
353
- #: views/authentication_views.py:606 views/authentication_views.py:775
304
+ #: views/authentication_views.py:263 views/authentication_views.py:432
354
305
  msgid "Empty password is not allowed"
355
306
  msgstr "Un mot de passe vide n’est pas autorisé"
356
307
 
357
- #: views/authentication_views.py:695
308
+ #: views/authentication_views.py:352
358
309
  msgid "User and token are valid"
359
310
  msgstr "L’utilisateur et le token sont valides"
360
311
 
361
- #: views/authentication_views.py:697
312
+ #: views/authentication_views.py:354
362
313
  msgid "User or token is invalid"
363
314
  msgstr "L’utilisateur ou le token est invalide"
364
315
 
365
- #: views/authentication_views.py:1042
316
+ #: views/authentication_views.py:631
366
317
  msgid ""
367
318
  "If the email address you entered is correct, you will receive an email from "
368
319
  "us with instructions to reset your password."
@@ -380,6 +331,48 @@ msgstr "Valeur invalide pour le filtre {filter}"
380
331
  msgid "Unexpected storage error"
381
332
  msgstr "Erreur de stockage inattendue"
382
333
 
334
+ #: views/mfa_authentication_views.py:161 views/mfa_authentication_views.py:407
335
+ #: views/mfa_authentication_views.py:446 views/mfa_authentication_views.py:488
336
+ msgid "Invalid code"
337
+ msgstr "Code invalide"
338
+
339
+ #: views/mfa_authentication_views.py:208
340
+ msgid "This field is required."
341
+ msgstr "Ce champ est requis."
342
+
343
+ #: views/mfa_authentication_views.py:214 views/mfa_authentication_views.py:288
344
+ #: views/mfa_authentication_views.py:368
345
+ msgid "Code sent."
346
+ msgstr "Code envoyé."
347
+
348
+ #: views/mfa_authentication_views.py:351
349
+ msgid "MFA must be enabled first"
350
+ msgstr "Le MFA doit d’abord être activé"
351
+
352
+ #: views/mfa_authentication_views.py:354
353
+ msgid "This MFA backend is already enabled"
354
+ msgstr "Ce backend MFA est déjà activé"
355
+
356
+ #: views/mfa_authentication_views.py:365
357
+ msgid "Phone number is required."
358
+ msgstr "Le numéro de téléphone est requis."
359
+
360
+ #: views/mfa_authentication_views.py:405
361
+ msgid "MFA backend enabled"
362
+ msgstr "Backend MFA activé"
363
+
364
+ #: views/mfa_authentication_views.py:441 views/mfa_authentication_views.py:483
365
+ msgid "MFA is required and cannot be disabled"
366
+ msgstr "Le MFA est requis et ne peut pas être désactivé"
367
+
368
+ #: views/mfa_authentication_views.py:450
369
+ msgid "MFA backend disabled"
370
+ msgstr "Backend MFA désactivé"
371
+
372
+ #: views/mfa_authentication_views.py:492
373
+ msgid "MFA disabled"
374
+ msgstr "MFA désactivé"
375
+
383
376
  #: views/ordered_rest_view_mixin.py:32
384
377
  #, python-brace-format
385
378
  msgid "object parameter is mandatory for move={move}."
@@ -52,6 +52,14 @@ class MFABackend:
52
52
  """Confirm the activation with a code. Returns ``True`` on success."""
53
53
  raise NotImplementedError
54
54
 
55
+ def can_disable(self, user):
56
+ """Return ``True`` if the user is allowed to disable this backend.
57
+
58
+ The base backend (email) cannot be disabled while MFA is forced: it
59
+ must stay available so a forced user can always receive a login code.
60
+ Other backends can always be disabled."""
61
+ return not (self.is_base and user.mfa_forced)
62
+
55
63
  def disable(self, user):
56
64
  """Disable this backend by removing its device."""
57
65
  device = self.get_device(user)
@@ -70,7 +70,14 @@ class MFAUserMixin(models.Model):
70
70
 
71
71
  @rest_property(_("MFA setup required"), "BooleanField")
72
72
  def mfa_setup_required(self):
73
- return self.mfa_forced and not self.mfa_setup_dismissed
73
+ """Return True if the forced user must still choose a MFA setup.
74
+
75
+ Cleared once the user either configured a backend (any confirmed
76
+ device, e.g. email only) or explicitly dismissed the setup screen."""
77
+ return (
78
+ self.mfa_forced
79
+ and not self.mfa_setup_dismissed
80
+ and not self.mfa_devices.filter(confirmed=True).exists())
74
81
 
75
82
  @rest_property(_("Is MFA forced"), "BooleanField")
76
83
  def mfa_forced(self):
@@ -12,6 +12,7 @@ from .media_rest_view_mixin import (
12
12
  MediaRestViewMixin,
13
13
  get_media_field_response,
14
14
  )
15
+ from .mfa_authentication_views import MFAAuthenticationView
15
16
  from .ordered_rest_view_mixin import OrderedRestViewMixin
16
17
  from .rest_views import (
17
18
  BaseRestView,