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.
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/PKG-INFO +1 -1
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/django_pfx.egg-info/PKG-INFO +1 -1
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/django_pfx.egg-info/SOURCES.txt +6 -1
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/doc/source/authentication.md +75 -30
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/default_settings.py +5 -0
- django_pfx-1.7.2.dev12/pfx/pfxcore/locale/fr/LC_MESSAGES/django.mo +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/locale/fr/LC_MESSAGES/django.po +53 -18
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/models/__init__.py +1 -1
- 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
- django_pfx-1.7.2.dev12/pfx/pfxcore/sms/__init__.py +9 -0
- django_pfx-1.7.2.dev12/pfx/pfxcore/sms/backends/base.py +15 -0
- django_pfx-1.7.2.dev12/pfx/pfxcore/sms/backends/console.py +14 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/views/authentication_views.py +225 -17
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/views/fields.py +3 -0
- django_pfx-1.7.2.dev12/tests/migrations/0004_mfausermixin_fields.py +43 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_auth_api.py +277 -36
- django_pfx-1.7.2.dev12/tests_custom_user/settings/__init__.py +0 -0
- django_pfx-1.7.2.dev8/pfx/pfxcore/locale/fr/LC_MESSAGES/django.mo +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/.gitignore +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/.gitlab-ci.yml +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/.pre-commit-config.yaml +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/LICENSE +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/MANIFEST.in +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/README.md +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/django_pfx.egg-info/dependency_links.txt +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/django_pfx.egg-info/requires.txt +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/django_pfx.egg-info/top_level.txt +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/doc/Makefile +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/doc/conf.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/doc/index.rst +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/doc/source/api.views.rst +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/doc/source/decorator.md +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/doc/source/generate_openapi.md +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/doc/source/getting_started.md +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/doc/source/internationalisation.md +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/doc/source/model.md +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/doc/source/pfx_views.md +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/doc/source/profiling.md +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/doc/source/settings.md +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/doc/source/testing.md +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/img/pfx.png +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/img/pfx.svg +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/make_messages +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/manage.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/__init__.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/__init__.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/apidoc/__init__.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/apidoc/parameters.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/apidoc/schema.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/apidoc/tags.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/apps.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/decorator/__init__.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/decorator/rest.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/exceptions.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/fields/__init__.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/fields/decimal_field.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/fields/media_field.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/fields/minutes_duration_field.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/fields/rich_text_field.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/http/__init__.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/http/json_response.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/management/__init__.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/management/commands/__init__.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/management/commands/makeapidoc.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/management/commands/profile.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/middleware/__init__.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/middleware/authentication.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/middleware/locale.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/middleware/profiling.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/migrations/0001_initial.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/migrations/0002_pfxpermissionsuser.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/migrations/0003_delete_pfxpermissionsuser.py +0 -0
- {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
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/migrations/__init__.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/migrations/operations/__init__.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/migrations/operations/permissions.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/models/abstract_pfx_base_user.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/models/cache_mixins.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/models/login_ban.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/models/not_null_fields.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/models/ordered_model_mixin.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/models/pfx_models.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/models/pfx_user.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/models/user_filtered_queryset_mixin.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/serializers/__init__.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/serializers/json.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/settings.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/shortcuts.py +0 -0
- {django_pfx-1.7.2.dev8/pfx/settings → django_pfx-1.7.2.dev12/pfx/pfxcore/sms/backends}/__init__.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/storage/__init__.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/storage/exceptions.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/storage/local_storage.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/storage/s3_storage.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/templates/registration/otp_code_email.txt +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/templates/registration/otp_code_subject.txt +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/templates/registration/password_reset_email.txt +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/templates/registration/password_reset_subject.txt +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/templates/registration/welcome_email.txt +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/templates/registration/welcome_subject.txt +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/test.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/urls.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/views/__init__.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/views/filters_views.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/views/locale_views.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/views/media_rest_view_mixin.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/views/ordered_rest_view_mixin.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/views/parameters/__init__.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/views/parameters/date_format.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/views/parameters/groups.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/views/parameters/list_count.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/views/parameters/list_items.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/views/parameters/list_mode.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/views/parameters/list_order.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/views/parameters/list_search.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/views/parameters/media_redirect.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/views/parameters/meta_fields.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/views/parameters/meta_filters.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/views/parameters/meta_orders.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/views/parameters/subset.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/views/parameters/subset_limit.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/views/parameters/subset_offset.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/views/parameters/subset_page.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/views/parameters/subset_page_size.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/views/parameters/subset_page_subset.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/views/rest_views.py +0 -0
- {django_pfx-1.7.2.dev8/tests → django_pfx-1.7.2.dev12/pfx/settings}/__init__.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/settings/dev.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pyproject.toml +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/requirements.txt +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/serve-doc +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/setup.cfg +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/setup.py +0 -0
- {django_pfx-1.7.2.dev8/tests/migrations → django_pfx-1.7.2.dev12/tests}/__init__.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/apps.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/locale/fr/LC_MESSAGES/django.po +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/migrations/0001_initial.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/migrations/0002_alter_book_cover.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/migrations/0003_book_local_file.py +0 -0
- {django_pfx-1.7.2.dev8/tests/settings → django_pfx-1.7.2.dev12/tests/migrations}/__init__.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/models.py +0 -0
- {django_pfx-1.7.2.dev8/tests_base_user → django_pfx-1.7.2.dev12/tests/settings}/__init__.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/settings/ci.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/settings/common.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/settings/dev.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/settings/dev_custom_example.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/settings/dev_default.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/__init__.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/basic_api_errors.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/basic_api_test.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_api_doc.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_api_doc_search.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_body_mixin.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_cache.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_client.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_fields_choices.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_fields_date.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_fields_decimal.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_fields_minutes_duration.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_fields_one2many.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_fields_rich_text.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_filters.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_locale_api.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_ordered_rest_view_mixin.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_perm_tests.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_permissions.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_perms_api.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_post_migrate_groups_update.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_profiling_middleware.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_settings.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_shortcuts.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_timezone_middleware.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_tools.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_user_queryset.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_view_decorators.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/tests/test_view_fields.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/urls.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests/views.py +0 -0
- {django_pfx-1.7.2.dev8/tests_base_user/migrations → django_pfx-1.7.2.dev12/tests_base_user}/__init__.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests_base_user/migrations/0001_initial.py +0 -0
- {django_pfx-1.7.2.dev8/tests_base_user/settings → django_pfx-1.7.2.dev12/tests_base_user/migrations}/__init__.py +0 -0
- {django_pfx-1.7.2.dev8/tests_custom_user → django_pfx-1.7.2.dev12/tests_base_user/settings}/__init__.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests_base_user/settings/ci.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests_base_user/settings/common.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests_base_user/settings/dev.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests_base_user/settings/dev_custom_example.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests_base_user/settings/dev_default.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests_base_user/tests/__init__.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests_base_user/tests/test_api.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests_base_user/tests/test_auth_api.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests_base_user/urls.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests_base_user/views.py +0 -0
- {django_pfx-1.7.2.dev8/tests_custom_user/migrations → django_pfx-1.7.2.dev12/tests_custom_user}/__init__.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests_custom_user/migrations/0001_initial.py +0 -0
- {django_pfx-1.7.2.dev8/tests_custom_user/settings → django_pfx-1.7.2.dev12/tests_custom_user/migrations}/__init__.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests_custom_user/models.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests_custom_user/settings/ci.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests_custom_user/settings/common.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests_custom_user/settings/dev.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests_custom_user/settings/dev_custom_example.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests_custom_user/settings/dev_default.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests_custom_user/tests/__init__.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests_custom_user/tests/test_api.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests_custom_user/tests/test_auth_api.py +0 -0
- {django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/tests_custom_user/urls.py +0 -0
- {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.
|
|
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.
|
|
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
|
|
93
|
-
|
|
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.
|
|
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,
|
|
107
|
+
from pfx.pfxcore.models import PFXUser, MFAUserMixin
|
|
105
108
|
|
|
106
|
-
class MyUser(
|
|
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,
|
|
116
|
+
from pfx.pfxcore.models import AbstractPFXBaseUser, MFAUserMixin
|
|
114
117
|
|
|
115
|
-
class MyUser(
|
|
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
|
|
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
|
|
167
|
-
|
|
168
|
-
| token
|
|
169
|
-
| user
|
|
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 +
|
|
173
|
-
If the user has
|
|
174
|
-
|
|
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
|
|
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
|
|
210
|
+
API->>App: 401 Unauthorized
|
|
192
211
|
end
|
|
193
212
|
else Login failed
|
|
194
|
-
API->>App: 401
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
|
224
|
-
|
|
225
|
-
| token
|
|
226
|
-
| otp
|
|
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
|
|
281
|
+
| token | the jwt token. (only if mode is `jwt`) |
|
|
237
282
|
| user | the user object |
|
|
238
283
|
|
|
239
284
|
### Enable MFA OTP
|
|
Binary file
|
{django_pfx-1.7.2.dev8 → django_pfx-1.7.2.dev12}/pfx/pfxcore/locale/fr/LC_MESSAGES/django.po
RENAMED
|
@@ -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
|
|
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/
|
|
90
|
+
#: models/mfa_user_mixin.py:19
|
|
91
91
|
msgid "OTP secret token"
|
|
92
92
|
msgstr "Jeton secret OTP"
|
|
93
93
|
|
|
94
|
-
#: models/
|
|
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/
|
|
98
|
+
#: models/mfa_user_mixin.py:25
|
|
99
99
|
msgid "HOTP count"
|
|
100
100
|
msgstr "Compte HOTP"
|
|
101
101
|
|
|
102
|
-
#: models/
|
|
102
|
+
#: models/mfa_user_mixin.py:27
|
|
103
103
|
msgid "HOTP expiry"
|
|
104
104
|
msgstr "Expiration HOTP"
|
|
105
105
|
|
|
106
|
-
#: models/
|
|
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:
|
|
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:
|
|
248
|
+
#: views/authentication_views.py:214
|
|
222
249
|
msgid "Successful login"
|
|
223
250
|
msgstr "Connexion réussie"
|
|
224
251
|
|
|
225
|
-
#: views/authentication_views.py:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
288
|
+
#: views/authentication_views.py:748
|
|
254
289
|
msgid "OTP disabled"
|
|
255
290
|
msgstr "OTP désactivé"
|
|
256
291
|
|
|
257
|
-
#: views/authentication_views.py:
|
|
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:
|
|
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
|
|
12
|
-
"""A mixin to enable
|
|
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)
|