django-pfx 1.7.2.dev8__tar.gz → 1.7.2.dev12__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 (205) hide show
  1. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/PKG-INFO +1 -1
  2. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/django_pfx.egg-info/PKG-INFO +1 -1
  3. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/django_pfx.egg-info/SOURCES.txt +6 -1
  4. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/doc/source/authentication.md +75 -30
  5. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/default_settings.py +5 -0
  6. django_pfx-1.7.2.dev12/pfx/pfxcore/locale/fr/LC_MESSAGES/django.mo +0 -0
  7. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/locale/fr/LC_MESSAGES/django.po +53 -18
  8. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/models/__init__.py +1 -1
  9. django_pfx-1.7.2.dev8/pfx/pfxcore/models/otp_user_mixin.py → django_pfx-1.7.2.dev12/pfx/pfxcore/models/mfa_user_mixin.py +24 -2
  10. django_pfx-1.7.2.dev12/pfx/pfxcore/sms/__init__.py +9 -0
  11. django_pfx-1.7.2.dev12/pfx/pfxcore/sms/backends/base.py +15 -0
  12. django_pfx-1.7.2.dev12/pfx/pfxcore/sms/backends/console.py +14 -0
  13. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/views/authentication_views.py +225 -17
  14. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/views/fields.py +3 -0
  15. django_pfx-1.7.2.dev12/tests/migrations/0004_mfausermixin_fields.py +43 -0
  16. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_auth_api.py +277 -36
  17. django_pfx-1.7.2.dev12/tests_custom_user/settings/__init__.py +0 -0
  18. django_pfx-1.7.2.dev8/pfx/pfxcore/locale/fr/LC_MESSAGES/django.mo +0 -0
  19. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/.gitignore +0 -0
  20. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/.gitlab-ci.yml +0 -0
  21. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/.pre-commit-config.yaml +0 -0
  22. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/LICENSE +0 -0
  23. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/MANIFEST.in +0 -0
  24. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/README.md +0 -0
  25. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/django_pfx.egg-info/dependency_links.txt +0 -0
  26. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/django_pfx.egg-info/requires.txt +0 -0
  27. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/django_pfx.egg-info/top_level.txt +0 -0
  28. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/doc/Makefile +0 -0
  29. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/doc/conf.py +0 -0
  30. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/doc/index.rst +0 -0
  31. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/doc/source/api.views.rst +0 -0
  32. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/doc/source/decorator.md +0 -0
  33. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/doc/source/generate_openapi.md +0 -0
  34. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/doc/source/getting_started.md +0 -0
  35. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/doc/source/internationalisation.md +0 -0
  36. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/doc/source/model.md +0 -0
  37. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/doc/source/pfx_views.md +0 -0
  38. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/doc/source/profiling.md +0 -0
  39. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/doc/source/settings.md +0 -0
  40. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/doc/source/testing.md +0 -0
  41. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/img/pfx.png +0 -0
  42. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/img/pfx.svg +0 -0
  43. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/make_messages +0 -0
  44. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/manage.py +0 -0
  45. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/__init__.py +0 -0
  46. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/__init__.py +0 -0
  47. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/apidoc/__init__.py +0 -0
  48. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/apidoc/parameters.py +0 -0
  49. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/apidoc/schema.py +0 -0
  50. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/apidoc/tags.py +0 -0
  51. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/apps.py +0 -0
  52. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/decorator/__init__.py +0 -0
  53. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/decorator/rest.py +0 -0
  54. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/exceptions.py +0 -0
  55. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/fields/__init__.py +0 -0
  56. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/fields/decimal_field.py +0 -0
  57. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/fields/media_field.py +0 -0
  58. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/fields/minutes_duration_field.py +0 -0
  59. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/fields/rich_text_field.py +0 -0
  60. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/http/__init__.py +0 -0
  61. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/http/json_response.py +0 -0
  62. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/management/__init__.py +0 -0
  63. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/management/commands/__init__.py +0 -0
  64. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/management/commands/makeapidoc.py +0 -0
  65. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/management/commands/profile.py +0 -0
  66. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/middleware/__init__.py +0 -0
  67. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/middleware/authentication.py +0 -0
  68. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/middleware/locale.py +0 -0
  69. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/middleware/profiling.py +0 -0
  70. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/migrations/0001_initial.py +0 -0
  71. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/migrations/0002_pfxpermissionsuser.py +0 -0
  72. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/migrations/0003_delete_pfxpermissionsuser.py +0 -0
  73. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/migrations/0004_alter_loginban_failed_counter_and_more.py +0 -0
  74. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/migrations/__init__.py +0 -0
  75. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/migrations/operations/__init__.py +0 -0
  76. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/migrations/operations/permissions.py +0 -0
  77. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/models/abstract_pfx_base_user.py +0 -0
  78. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/models/cache_mixins.py +0 -0
  79. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/models/login_ban.py +0 -0
  80. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/models/not_null_fields.py +0 -0
  81. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/models/ordered_model_mixin.py +0 -0
  82. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/models/pfx_models.py +0 -0
  83. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/models/pfx_user.py +0 -0
  84. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/models/user_filtered_queryset_mixin.py +0 -0
  85. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/serializers/__init__.py +0 -0
  86. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/serializers/json.py +0 -0
  87. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/settings.py +0 -0
  88. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/shortcuts.py +0 -0
  89. {django_pfx-1.7.2.dev8/pfx/settings → django_pfx-1.7.2.dev12/pfx/pfxcore/sms/backends}/__init__.py +0 -0
  90. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/storage/__init__.py +0 -0
  91. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/storage/exceptions.py +0 -0
  92. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/storage/local_storage.py +0 -0
  93. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/storage/s3_storage.py +0 -0
  94. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/templates/registration/otp_code_email.txt +0 -0
  95. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/templates/registration/otp_code_subject.txt +0 -0
  96. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/templates/registration/password_reset_email.txt +0 -0
  97. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/templates/registration/password_reset_subject.txt +0 -0
  98. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/templates/registration/welcome_email.txt +0 -0
  99. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/templates/registration/welcome_subject.txt +0 -0
  100. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/test.py +0 -0
  101. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/urls.py +0 -0
  102. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/views/__init__.py +0 -0
  103. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/views/filters_views.py +0 -0
  104. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/views/locale_views.py +0 -0
  105. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/views/media_rest_view_mixin.py +0 -0
  106. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/views/ordered_rest_view_mixin.py +0 -0
  107. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/views/parameters/__init__.py +0 -0
  108. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/views/parameters/date_format.py +0 -0
  109. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/views/parameters/groups.py +0 -0
  110. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/views/parameters/list_count.py +0 -0
  111. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/views/parameters/list_items.py +0 -0
  112. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/views/parameters/list_mode.py +0 -0
  113. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/views/parameters/list_order.py +0 -0
  114. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/views/parameters/list_search.py +0 -0
  115. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/views/parameters/media_redirect.py +0 -0
  116. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/views/parameters/meta_fields.py +0 -0
  117. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/views/parameters/meta_filters.py +0 -0
  118. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/views/parameters/meta_orders.py +0 -0
  119. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/views/parameters/subset.py +0 -0
  120. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/views/parameters/subset_limit.py +0 -0
  121. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/views/parameters/subset_offset.py +0 -0
  122. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/views/parameters/subset_page.py +0 -0
  123. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/views/parameters/subset_page_size.py +0 -0
  124. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/views/parameters/subset_page_subset.py +0 -0
  125. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/views/rest_views.py +0 -0
  126. {django_pfx-1.7.2.dev8/tests → django_pfx-1.7.2.dev12/pfx/settings}/__init__.py +0 -0
  127. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/settings/dev.py +0 -0
  128. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pyproject.toml +0 -0
  129. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/requirements.txt +0 -0
  130. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/serve-doc +0 -0
  131. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/setup.cfg +0 -0
  132. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/setup.py +0 -0
  133. {django_pfx-1.7.2.dev8/tests/migrations → django_pfx-1.7.2.dev12/tests}/__init__.py +0 -0
  134. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/apps.py +0 -0
  135. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/locale/fr/LC_MESSAGES/django.po +0 -0
  136. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/migrations/0001_initial.py +0 -0
  137. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/migrations/0002_alter_book_cover.py +0 -0
  138. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/migrations/0003_book_local_file.py +0 -0
  139. {django_pfx-1.7.2.dev8/tests/settings → django_pfx-1.7.2.dev12/tests/migrations}/__init__.py +0 -0
  140. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/models.py +0 -0
  141. {django_pfx-1.7.2.dev8/tests_base_user → django_pfx-1.7.2.dev12/tests/settings}/__init__.py +0 -0
  142. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/settings/ci.py +0 -0
  143. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/settings/common.py +0 -0
  144. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/settings/dev.py +0 -0
  145. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/settings/dev_custom_example.py +0 -0
  146. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/settings/dev_default.py +0 -0
  147. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/__init__.py +0 -0
  148. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/basic_api_errors.py +0 -0
  149. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/basic_api_test.py +0 -0
  150. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_api_doc.py +0 -0
  151. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_api_doc_search.py +0 -0
  152. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_body_mixin.py +0 -0
  153. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_cache.py +0 -0
  154. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_client.py +0 -0
  155. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_fields_choices.py +0 -0
  156. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_fields_date.py +0 -0
  157. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_fields_decimal.py +0 -0
  158. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_fields_minutes_duration.py +0 -0
  159. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_fields_one2many.py +0 -0
  160. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_fields_rich_text.py +0 -0
  161. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_filters.py +0 -0
  162. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_locale_api.py +0 -0
  163. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_ordered_rest_view_mixin.py +0 -0
  164. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_perm_tests.py +0 -0
  165. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_permissions.py +0 -0
  166. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_perms_api.py +0 -0
  167. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_post_migrate_groups_update.py +0 -0
  168. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_profiling_middleware.py +0 -0
  169. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_settings.py +0 -0
  170. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_shortcuts.py +0 -0
  171. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_timezone_middleware.py +0 -0
  172. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_tools.py +0 -0
  173. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_user_queryset.py +0 -0
  174. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_view_decorators.py +0 -0
  175. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_view_fields.py +0 -0
  176. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/urls.py +0 -0
  177. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/views.py +0 -0
  178. {django_pfx-1.7.2.dev8/tests_base_user/migrations → django_pfx-1.7.2.dev12/tests_base_user}/__init__.py +0 -0
  179. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests_base_user/migrations/0001_initial.py +0 -0
  180. {django_pfx-1.7.2.dev8/tests_base_user/settings → django_pfx-1.7.2.dev12/tests_base_user/migrations}/__init__.py +0 -0
  181. {django_pfx-1.7.2.dev8/tests_custom_user → django_pfx-1.7.2.dev12/tests_base_user/settings}/__init__.py +0 -0
  182. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests_base_user/settings/ci.py +0 -0
  183. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests_base_user/settings/common.py +0 -0
  184. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests_base_user/settings/dev.py +0 -0
  185. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests_base_user/settings/dev_custom_example.py +0 -0
  186. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests_base_user/settings/dev_default.py +0 -0
  187. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests_base_user/tests/__init__.py +0 -0
  188. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests_base_user/tests/test_api.py +0 -0
  189. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests_base_user/tests/test_auth_api.py +0 -0
  190. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests_base_user/urls.py +0 -0
  191. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests_base_user/views.py +0 -0
  192. {django_pfx-1.7.2.dev8/tests_custom_user/migrations → django_pfx-1.7.2.dev12/tests_custom_user}/__init__.py +0 -0
  193. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests_custom_user/migrations/0001_initial.py +0 -0
  194. {django_pfx-1.7.2.dev8/tests_custom_user/settings → django_pfx-1.7.2.dev12/tests_custom_user/migrations}/__init__.py +0 -0
  195. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests_custom_user/models.py +0 -0
  196. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests_custom_user/settings/ci.py +0 -0
  197. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests_custom_user/settings/common.py +0 -0
  198. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests_custom_user/settings/dev.py +0 -0
  199. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests_custom_user/settings/dev_custom_example.py +0 -0
  200. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests_custom_user/settings/dev_default.py +0 -0
  201. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests_custom_user/tests/__init__.py +0 -0
  202. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests_custom_user/tests/test_api.py +0 -0
  203. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests_custom_user/tests/test_auth_api.py +0 -0
  204. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests_custom_user/urls.py +0 -0
  205. {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/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.2.dev8
3
+ Version: 1.7.2.dev12
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.2.dev8
3
+ Version: 1.7.2.dev12
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
@@ -75,14 +75,18 @@ pfx/pfxcore/models/__init__.py
75
75
  pfx/pfxcore/models/abstract_pfx_base_user.py
76
76
  pfx/pfxcore/models/cache_mixins.py
77
77
  pfx/pfxcore/models/login_ban.py
78
+ pfx/pfxcore/models/mfa_user_mixin.py
78
79
  pfx/pfxcore/models/not_null_fields.py
79
80
  pfx/pfxcore/models/ordered_model_mixin.py
80
- pfx/pfxcore/models/otp_user_mixin.py
81
81
  pfx/pfxcore/models/pfx_models.py
82
82
  pfx/pfxcore/models/pfx_user.py
83
83
  pfx/pfxcore/models/user_filtered_queryset_mixin.py
84
84
  pfx/pfxcore/serializers/__init__.py
85
85
  pfx/pfxcore/serializers/json.py
86
+ pfx/pfxcore/sms/__init__.py
87
+ pfx/pfxcore/sms/backends/__init__.py
88
+ pfx/pfxcore/sms/backends/base.py
89
+ pfx/pfxcore/sms/backends/console.py
86
90
  pfx/pfxcore/storage/__init__.py
87
91
  pfx/pfxcore/storage/exceptions.py
88
92
  pfx/pfxcore/storage/local_storage.py
@@ -130,6 +134,7 @@ tests/locale/fr/LC_MESSAGES/django.po
130
134
  tests/migrations/0001_initial.py
131
135
  tests/migrations/0002_alter_book_cover.py
132
136
  tests/migrations/0003_book_local_file.py
137
+ tests/migrations/0004_mfausermixin_fields.py
133
138
  tests/migrations/__init__.py
134
139
  tests/settings/__init__.py
135
140
  tests/settings/ci.py
@@ -89,8 +89,11 @@ the `Retry-After` header.
89
89
  ### Multifactor Authentication
90
90
  Multifactor authentication can be enabled in django-pfx Authentication API.
91
91
 
92
- PFX currently provides MFA with One Time Password (OTP), compatible with FreeOTP,
93
- Google Authenticator and other OTP app.
92
+ PFX supports three MFA backends:
93
+
94
+ * **`authenticator`** — TOTP, compatible with FreeOTP, Google Authenticator and other OTP apps.
95
+ * **`email`** — a HOTP code sent by email.
96
+ * **`sms`** — a HOTP code sent by SMS.
94
97
 
95
98
  To enable this feature, install django-pfx with otp.
96
99
 
@@ -98,35 +101,46 @@ To enable this feature, install django-pfx with otp.
98
101
  pip install django-pfx[otp]
99
102
  ```
100
103
 
101
- Then the user class must use the {class}`pfx.pfxcore.models.OtpUserMixin`.
104
+ Then the user class must use the {class}`pfx.pfxcore.models.MFAUserMixin`.
102
105
 
103
106
  ```python
104
- from pfx.pfxcore.models import PFXUser, OtpUserMixin
107
+ from pfx.pfxcore.models import PFXUser, MFAUserMixin
105
108
 
106
- class MyUser(OtpUserMixin, PFXUser):
109
+ class MyUser(MFAUserMixin, PFXUser):
107
110
  pass
108
111
  ```
109
112
 
110
113
  or
111
114
 
112
115
  ```python
113
- from pfx.pfxcore.models import AbstractPFXBaseUser, OtpUserMixin
116
+ from pfx.pfxcore.models import AbstractPFXBaseUser, MFAUserMixin
114
117
 
115
- class MyUser(OtpUserMixin, AbstractPFXBaseUser):
118
+ class MyUser(MFAUserMixin, AbstractPFXBaseUser):
116
119
  pass
117
120
  ```
118
121
 
122
+ :::{note}
123
+ `OtpUserMixin` is a deprecated alias for `MFAUserMixin` and will be removed in a future version.
124
+ :::
125
+
119
126
  #### Settings
120
127
 
128
+ * `PFX_MFA_BACKENDS`: Ordered list of enabled MFA backends (optional, default `["authenticator", "email"]`).
129
+ The first backend for which the user is enrolled is used at login.
130
+ * `PFX_MFA_FORCE`: If `True`, MFA is required for all users — a `mfa_setup_required` flag is returned
131
+ at login when the user has not yet dismissed the setup screen (optional, default `False`).
132
+ * `PFX_SMS_BACKEND`: Dotted path to the SMS backend class (optional, default
133
+ `'pfx.pfxcore.sms.backends.console.ConsoleSMSBackend'`).
134
+ Implement {class}`pfx.pfxcore.sms.backends.base.BaseSMSBackend` to integrate a real SMS provider.
121
135
  * `PFX_TOKEN_OTP_VALIDITY`: Validity for OTP tokens (corresponds to the maximum time to enter
122
136
  an OTP code after logging in with a password) (optional, default `{'minutes': 15}`)
123
- * `PFX_HOTP_CODE_VALIDITY`: Validity of HOTP codes in minutes (used to send code by email) (optional, default `15`).
137
+ * `PFX_HOTP_CODE_VALIDITY`: Validity of HOTP codes in minutes (used to send code by email or SMS) (optional, default `15`).
124
138
  * `PFX_OTP_VALID_WINDOW`: TOTP valid window (optional, default `1`).
125
139
  According to [RFC 6238 section 5.2](https://www.ietf.org/rfc/rfc6238.html#section-5.2).
126
140
  * `PFX_OTP_IMAGE`: An image https URL used by FreeOTP. See [FreeOTP URI](https://github.com/npmccallum/freeotp-android/blob/master/URI.md).
127
141
  * `PFX_OTP_COLOR`: A brand color (in RRGGBB format) for used by FreeOTP. See [FreeOTP URI](https://github.com/npmccallum/freeotp-android/blob/master/URI.md).
128
142
 
129
- The user can then enable or disable the OTP auth using the [services documented below](#enable-mfa-otp).
143
+ The user can then enable or disable the authenticator OTP using the [services documented below](#enable-mfa-otp).
130
144
 
131
145
  ## Services
132
146
 
@@ -163,15 +177,16 @@ In cookie mode, the JWT token is saved in an HTTP-only cookie.
163
177
  * `HTTP 401` if the credentials are incorrect
164
178
  * `HTTP 200` with the following body
165
179
 
166
- | Field | Description |
167
- |-------|----------------------------------------|
168
- | token | the jwt token. (only if mode is 'jwt') |
169
- | user | the user object |
180
+ | Field | Description |
181
+ |--------------------|------------------------------------------------------------------------|
182
+ | token | the jwt token (only if mode is `jwt`) |
183
+ | user | the user object |
184
+ | mfa_setup_required | `true` if MFA is forced and the user has not yet dismissed setup screen |
170
185
 
171
186
 
172
- ### Login + TOTP
173
- If the user has enabled the TOTP login, the process is the same as above for the first step,
174
- except that the login service returns a temporary JWT token valid only for the otp services.
187
+ ### Login + MFA
188
+ If the user has an active MFA backend, the login service returns a temporary JWT token and
189
+ triggers a challenge for out-of-band backends (email / SMS).
175
190
 
176
191
  ```{mermaid}
177
192
 
@@ -179,19 +194,23 @@ except that the login service returns a temporary JWT token valid only for the o
179
194
  participant App
180
195
  participant API
181
196
  App->>API: POST /auth/login
182
- alt Login success
197
+ alt Login success + MFA required
183
198
  API->>App: 200 OK
184
- note left of API: temporary jwt token
199
+ note left of API: need_otp=true + temporary jwt token<br/>+ mfa_backend + available_backends
200
+ opt Switch backend or resend code
201
+ App->>API: POST /mfa/challenge
202
+ note right of App: temporary jwt token + mfa_backend
203
+ end
185
204
  App->>API: POST /auth/otp/login
186
- note right of App: temporary jwt token + OTP token in body.
205
+ note right of App: temporary jwt token + OTP code in body
187
206
  alt OTP success
188
207
  API->>App: 200 OK
189
208
  note left of API: JWT token in cookie or in body <br/> + user in body
190
209
  else OTP failed
191
- API->>App: 401 Unautorized
210
+ API->>App: 401 Unauthorized
192
211
  end
193
212
  else Login failed
194
- API->>App: 401 Unautorized
213
+ API->>App: 401 Unauthorized
195
214
  end
196
215
 
197
216
  ```
@@ -209,21 +228,47 @@ except that the login service returns a temporary JWT token valid only for the o
209
228
  **Responses :**
210
229
 
211
230
  * `HTTP 401` if the credentials are incorrect
212
- * `HTTP 200` with the following body
231
+ * `HTTP 200` with the following body when MFA is required (`need_otp=true`)
232
+
233
+ | Field | Description |
234
+ |--------------------|-----------------------------------------------------------------------|
235
+ | need_otp | `true` |
236
+ | token | a temporary jwt token (valid only for OTP/MFA endpoints) |
237
+ | mfa_backend | object with `id` (e.g. `"authenticator"`, `"email"`, `"sms"`) and `is_oob` |
238
+ | available_backends | list of backend objects (`id`, `is_oob`) available for this user |
239
+
240
+ `is_oob` (*out-of-band*) is `true` when the code is delivered via a separate channel (email or SMS),
241
+ as opposed to `authenticator` where the code is generated locally in the user's app.
242
+
243
+ For OOB backends (`is_oob: true`), a challenge code is sent automatically at this step.
213
244
 
214
- | Field | Description |
215
- |-------|-----------------------|
216
- | token | a temporary jwt token |
245
+
246
+ **Request :** `POST` `/mfa/challenge`
247
+
248
+ Request a new challenge code or switch to a different OOB backend (email or SMS).
249
+
250
+ **Request body:**
251
+
252
+ | Field | Description |
253
+ |-------------|---------------------------------------------------|
254
+ | token | the temporary jwt token |
255
+ | mfa_backend | the backend to use (`"email"` or `"sms"`) |
256
+
257
+ **Responses :**
258
+
259
+ * `HTTP 401` if the token is missing, invalid or expired
260
+ * `HTTP 403` if the requested backend is not available for this user
261
+ * `HTTP 200` with a confirmation message
217
262
 
218
263
 
219
264
  **Request :** `POST` `/auth/otp/login?mode=<mode>`
220
265
 
221
266
  **Request body:**
222
267
 
223
- | Field | Description |
224
- |-------------|-------------------------------------|
225
- | token | the temporary jwt token |
226
- | otp | the one time password (TOTP) |
268
+ | Field | Description |
269
+ |-------|------------------------------------------------------|
270
+ | token | the temporary jwt token |
271
+ | otp | the one-time password (TOTP code or emailed/SMS code)|
227
272
 
228
273
  **Responses :**
229
274
 
@@ -233,7 +278,7 @@ except that the login service returns a temporary JWT token valid only for the o
233
278
 
234
279
  | Field | Description |
235
280
  |-------|----------------------------------------|
236
- | token | the jwt token. (only if mode is 'jwt') |
281
+ | token | the jwt token. (only if mode is `jwt`) |
237
282
  | user | the user object |
238
283
 
239
284
  ### Enable MFA OTP
@@ -37,3 +37,8 @@ STORAGE_LOCAL_X_ACCEL_REDIRECT = False
37
37
  PFX_TEST_MODE = False
38
38
 
39
39
  PFX_AUTH_GROUPS_CREATE_ONLY = False
40
+
41
+ PFX_MFA_BACKENDS = ["authenticator", "email"]
42
+ PFX_MFA_FORCE = False
43
+
44
+ PFX_SMS_BACKEND = 'pfx.pfxcore.sms.backends.console.ConsoleSMSBackend'
@@ -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-04-20 13:42+0200\n"
10
+ "POT-Creation-Date: 2026-04-20 16:17+0200\n"
11
11
  "PO-Revision-Date: 2021-06-22 23:31+0200\n"
12
12
  "Last-Translator: \n"
13
13
  "Language-Team: \n"
@@ -87,23 +87,45 @@ msgstr "Login banni"
87
87
  msgid "Login bans"
88
88
  msgstr "Login bannis"
89
89
 
90
- #: models/otp_user_mixin.py:16
90
+ #: models/mfa_user_mixin.py:19
91
91
  msgid "OTP secret token"
92
92
  msgstr "Jeton secret OTP"
93
93
 
94
- #: models/otp_user_mixin.py:20
94
+ #: models/mfa_user_mixin.py:23
95
95
  msgid "Temporary OTP secret token"
96
96
  msgstr "Jeton secret OTP temporaire"
97
97
 
98
- #: models/otp_user_mixin.py:22
98
+ #: models/mfa_user_mixin.py:25
99
99
  msgid "HOTP count"
100
100
  msgstr "Compte HOTP"
101
101
 
102
- #: models/otp_user_mixin.py:24
102
+ #: models/mfa_user_mixin.py:27
103
103
  msgid "HOTP expiry"
104
104
  msgstr "Expiration HOTP"
105
105
 
106
- #: models/otp_user_mixin.py:29 views/authentication_views.py:504
106
+ #: models/mfa_user_mixin.py:30
107
+ msgid "SMS phone number"
108
+ msgstr "Numéro de téléphone (SMS)"
109
+
110
+ #: models/mfa_user_mixin.py:33
111
+ msgid "Temporary SMS phone number"
112
+ msgstr "Numéro de téléphone temporaire (SMS)"
113
+
114
+ #: models/mfa_user_mixin.py:35
115
+ #, fuzzy
116
+ #| msgid "OTP enabled"
117
+ msgid "SMS MFA enabled"
118
+ msgstr "OTP activé"
119
+
120
+ #: models/mfa_user_mixin.py:37
121
+ msgid "Email MFA enabled"
122
+ msgstr "MFA par email activé"
123
+
124
+ #: models/mfa_user_mixin.py:40
125
+ msgid "MFA setup dismissed"
126
+ msgstr "Configuration MFA annulée"
127
+
128
+ #: models/mfa_user_mixin.py:47 views/authentication_views.py:712
107
129
  msgid "OTP enabled"
108
130
  msgstr "OTP activé"
109
131
 
@@ -209,7 +231,12 @@ msgstr "Bienvenue sur %(site_name)s."
209
231
  msgid "Welcome on %(site_name)s"
210
232
  msgstr "Bienvenue sur %(site_name)s"
211
233
 
212
- #: views/authentication_views.py:83
234
+ #: views/authentication_views.py:98
235
+ #, python-brace-format
236
+ msgid "Your {site_name} verification code: {otp_code}"
237
+ msgstr "Votre code pour {site_name} : {otp_code}"
238
+
239
+ #: views/authentication_views.py:113
213
240
  #, python-brace-format
214
241
  msgid ""
215
242
  "Your connection is temporarily disabled after several unsuccessful attempts, "
@@ -218,43 +245,51 @@ msgstr ""
218
245
  "Votre connexion est temporairement désactivée après plusieurs tentatives "
219
246
  "infructueuses, veuillez réessayer dans {seconds} secondes."
220
247
 
221
- #: views/authentication_views.py:177
248
+ #: views/authentication_views.py:214
222
249
  msgid "Successful login"
223
250
  msgstr "Connexion réussie"
224
251
 
225
- #: views/authentication_views.py:251 views/authentication_views.py:421
252
+ #: views/authentication_views.py:385
253
+ msgid "This field is required."
254
+ msgstr "Ce champ est requis."
255
+
256
+ #: views/authentication_views.py:396
257
+ msgid "Code sent."
258
+ msgstr "Code envoyé."
259
+
260
+ #: views/authentication_views.py:459 views/authentication_views.py:629
226
261
  msgid "password updated successfully"
227
262
  msgstr "le mot de passe a été mis à jour avec succès"
228
263
 
229
- #: views/authentication_views.py:256
264
+ #: views/authentication_views.py:464
230
265
  msgid "Incorrect password"
231
266
  msgstr "Mot de passe incorrect"
232
267
 
233
- #: views/authentication_views.py:259 views/authentication_views.py:430
268
+ #: views/authentication_views.py:467 views/authentication_views.py:638
234
269
  msgid "Empty password is not allowed"
235
270
  msgstr "Un mot de passe vide n’est pas autorisé"
236
271
 
237
- #: views/authentication_views.py:350
272
+ #: views/authentication_views.py:558
238
273
  msgid "User and token are valid"
239
274
  msgstr "L'utilisateur et le token sont valides"
240
275
 
241
- #: views/authentication_views.py:352
276
+ #: views/authentication_views.py:560
242
277
  msgid "User or token is invalid"
243
278
  msgstr "L'utilisateur ou le token est invalide"
244
279
 
245
- #: views/authentication_views.py:464
280
+ #: views/authentication_views.py:672
246
281
  msgid "OTP is already enabled"
247
282
  msgstr "OTP est déjà activé"
248
283
 
249
- #: views/authentication_views.py:505 views/authentication_views.py:541
284
+ #: views/authentication_views.py:713 views/authentication_views.py:749
250
285
  msgid "Invalid code"
251
286
  msgstr "Code invalide"
252
287
 
253
- #: views/authentication_views.py:540
288
+ #: views/authentication_views.py:748
254
289
  msgid "OTP disabled"
255
290
  msgstr "OTP désactivé"
256
291
 
257
- #: views/authentication_views.py:801
292
+ #: views/authentication_views.py:1009
258
293
  msgid ""
259
294
  "If the email address you entered is correct, you will receive an email from "
260
295
  "us with instructions to reset your password."
@@ -263,7 +298,7 @@ msgstr ""
263
298
  "un courrier électronique de notre part contenant des instructions pour "
264
299
  "réinitialiser votre mot de passe."
265
300
 
266
- #: views/authentication_views.py:875
301
+ #: views/authentication_views.py:1083
267
302
  msgid "A new authentication code has been sent by email."
268
303
  msgstr "Un nouveau code d'authentification a été envoyé par e-mail."
269
304
 
@@ -1,13 +1,13 @@
1
1
  from .abstract_pfx_base_user import AbstractPFXBaseUser, AbstractPFXUser
2
2
  from .cache_mixins import CacheableMixin, CacheDependsMixin
3
3
  from .login_ban import LoginBan
4
+ from .mfa_user_mixin import MFAUserMixin, OtpUserMixin
4
5
  from .not_null_fields import (
5
6
  NotNullCharField,
6
7
  NotNullTextField,
7
8
  NotNullURLField,
8
9
  )
9
10
  from .ordered_model_mixin import OrderedModelMixin
10
- from .otp_user_mixin import OtpUserMixin
11
11
  from .pfx_models import (
12
12
  ErrorMessageMixin,
13
13
  JSONReprMixin,
@@ -8,8 +8,11 @@ from pfx.pfxcore.decorator import rest_property
8
8
  from pfx.pfxcore.settings import settings
9
9
 
10
10
 
11
- class OtpUserMixin(models.Model):
12
- """A mixin to enable OTP MFA on a user class."""
11
+ class MFAUserMixin(models.Model):
12
+ """A mixin to enable MFA on a user class.
13
+
14
+ Supports three methods: authenticator app (TOTP), SMS, and email.
15
+ """
13
16
 
14
17
  #: OTP secret token.
15
18
  otp_secret_token = models.CharField(
@@ -22,10 +25,25 @@ class OtpUserMixin(models.Model):
22
25
  hotp_count = models.IntegerField(_("HOTP count"), default=0)
23
26
  #: HOTP expiry.
24
27
  hotp_expiry = models.DateTimeField(_("HOTP expiry"), default=timezone.now)
28
+ #: Validated SMS phone number.
29
+ sms_phone_number = models.CharField(
30
+ _("SMS phone number"), max_length=32, null=True, blank=True)
31
+ #: SMS phone number pending validation.
32
+ sms_phone_number_tmp = models.CharField(
33
+ _("Temporary SMS phone number"), max_length=32, null=True, blank=True)
34
+ #: SMS MFA enabled.
35
+ sms_enabled = models.BooleanField(_("SMS MFA enabled"), default=False)
36
+ #: Email MFA enabled.
37
+ email_enabled = models.BooleanField(_("Email MFA enabled"), default=False)
38
+ #: User has dismissed or completed the MFA setup screen.
39
+ mfa_setup_dismissed = models.BooleanField(
40
+ _("MFA setup dismissed"), default=False)
25
41
 
26
42
  class Meta:
27
43
  abstract = True
28
44
 
45
+ # --- deprecated alias ---
46
+
29
47
  @rest_property(_("OTP enabled"), "BooleanField")
30
48
  def is_otp(self):
31
49
  return bool(self.otp_secret_token)
@@ -130,3 +148,7 @@ class OtpUserMixin(models.Model):
130
148
  self.save(update_fields=[
131
149
  'hotp_count', 'hotp_expiry'])
132
150
  return pyotp.hotp.HOTP(self.otp_secret_token).at(self.hotp_count)
151
+
152
+
153
+ #: Deprecated alias for MFAUserMixin.
154
+ OtpUserMixin = MFAUserMixin
@@ -0,0 +1,9 @@
1
+ from django.utils.module_loading import import_string
2
+
3
+ from pfx.pfxcore.settings import settings
4
+
5
+
6
+ def get_sms_backend():
7
+ """Return an instance of the configured SMS backend."""
8
+ backend_class = import_string(settings.PFX_SMS_BACKEND)
9
+ return backend_class()
@@ -0,0 +1,15 @@
1
+ class BaseSMSBackend:
2
+ """Base SMS backend.
3
+
4
+ Subclass this to implement a custom SMS backend.
5
+ The only required method is ``send_sms``."""
6
+
7
+ def send_sms(self, to, message):
8
+ """Send an SMS message.
9
+
10
+ :param to: The recipient phone number (E.164 format recommended).
11
+ :param message: The text content of the message.
12
+ :raises NotImplementedError: Must be implemented by subclasses.
13
+ """
14
+ raise NotImplementedError(
15
+ "Subclasses of BaseSMSBackend must implement send_sms().")
@@ -0,0 +1,14 @@
1
+ import logging
2
+
3
+ from .base import BaseSMSBackend
4
+
5
+ logger = logging.getLogger(__name__)
6
+
7
+
8
+ class ConsoleSMSBackend(BaseSMSBackend):
9
+ """SMS backend that logs messages to the console.
10
+
11
+ Useful for development and testing — no real SMS is sent."""
12
+
13
+ def send_sms(self, to, message):
14
+ logger.info("SMS to %s: %s", to, message)