django-pfx 1.7.2.dev12__tar.gz → 1.7.2.dev16__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.dev12 → django_pfx-1.7.2.dev16}/PKG-INFO +1 -1
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/django_pfx.egg-info/PKG-INFO +1 -1
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/django_pfx.egg-info/SOURCES.txt +2 -0
- django_pfx-1.7.2.dev16/pfx/pfxcore/locale/fr/LC_MESSAGES/django.mo +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/locale/fr/LC_MESSAGES/django.po +38 -24
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/models/mfa_user_mixin.py +43 -9
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/views/authentication_views.py +13 -14
- django_pfx-1.7.2.dev16/tests/migrations/0005_mfausermixin_fields_fix.py +17 -0
- django_pfx-1.7.2.dev16/tests/migrations/0006_rename_otp_enabled_user_mfa_authenticator_enabled_and_more.py +29 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/__init__.py +1 -1
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_auth_api.py +24 -22
- django_pfx-1.7.2.dev12/pfx/pfxcore/locale/fr/LC_MESSAGES/django.mo +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/.gitignore +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/.gitlab-ci.yml +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/.pre-commit-config.yaml +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/LICENSE +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/MANIFEST.in +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/README.md +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/django_pfx.egg-info/dependency_links.txt +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/django_pfx.egg-info/requires.txt +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/django_pfx.egg-info/top_level.txt +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/doc/Makefile +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/doc/conf.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/doc/index.rst +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/doc/source/api.views.rst +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/doc/source/authentication.md +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/doc/source/decorator.md +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/doc/source/generate_openapi.md +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/doc/source/getting_started.md +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/doc/source/internationalisation.md +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/doc/source/model.md +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/doc/source/pfx_views.md +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/doc/source/profiling.md +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/doc/source/settings.md +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/doc/source/testing.md +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/img/pfx.png +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/img/pfx.svg +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/make_messages +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/manage.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/__init__.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/__init__.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/apidoc/__init__.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/apidoc/parameters.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/apidoc/schema.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/apidoc/tags.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/apps.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/decorator/__init__.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/decorator/rest.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/default_settings.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/exceptions.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/fields/__init__.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/fields/decimal_field.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/fields/media_field.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/fields/minutes_duration_field.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/fields/rich_text_field.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/http/__init__.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/http/json_response.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/management/__init__.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/management/commands/__init__.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/management/commands/makeapidoc.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/management/commands/profile.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/middleware/__init__.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/middleware/authentication.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/middleware/locale.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/middleware/profiling.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/migrations/0001_initial.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/migrations/0002_pfxpermissionsuser.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/migrations/0003_delete_pfxpermissionsuser.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/migrations/0004_alter_loginban_failed_counter_and_more.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/migrations/__init__.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/migrations/operations/__init__.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/migrations/operations/permissions.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/models/__init__.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/models/abstract_pfx_base_user.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/models/cache_mixins.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/models/login_ban.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/models/not_null_fields.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/models/ordered_model_mixin.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/models/pfx_models.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/models/pfx_user.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/models/user_filtered_queryset_mixin.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/serializers/__init__.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/serializers/json.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/settings.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/shortcuts.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/sms/__init__.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/sms/backends/__init__.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/sms/backends/base.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/sms/backends/console.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/storage/__init__.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/storage/exceptions.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/storage/local_storage.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/storage/s3_storage.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/templates/registration/otp_code_email.txt +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/templates/registration/otp_code_subject.txt +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/templates/registration/password_reset_email.txt +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/templates/registration/password_reset_subject.txt +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/templates/registration/welcome_email.txt +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/templates/registration/welcome_subject.txt +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/test.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/urls.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/views/__init__.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/views/fields.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/views/filters_views.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/views/locale_views.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/views/media_rest_view_mixin.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/views/ordered_rest_view_mixin.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/views/parameters/__init__.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/views/parameters/date_format.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/views/parameters/groups.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/views/parameters/list_count.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/views/parameters/list_items.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/views/parameters/list_mode.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/views/parameters/list_order.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/views/parameters/list_search.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/views/parameters/media_redirect.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/views/parameters/meta_fields.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/views/parameters/meta_filters.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/views/parameters/meta_orders.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/views/parameters/subset.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/views/parameters/subset_limit.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/views/parameters/subset_offset.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/views/parameters/subset_page.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/views/parameters/subset_page_size.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/views/parameters/subset_page_subset.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/views/rest_views.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/settings/__init__.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/settings/dev.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pyproject.toml +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/requirements.txt +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/serve-doc +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/setup.cfg +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/setup.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/__init__.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/apps.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/locale/fr/LC_MESSAGES/django.po +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/migrations/0001_initial.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/migrations/0002_alter_book_cover.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/migrations/0003_book_local_file.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/migrations/0004_mfausermixin_fields.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/migrations/__init__.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/models.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/settings/__init__.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/settings/ci.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/settings/common.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/settings/dev.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/settings/dev_custom_example.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/settings/dev_default.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/basic_api_errors.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/basic_api_test.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_api_doc.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_api_doc_search.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_body_mixin.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_cache.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_client.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_fields_choices.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_fields_date.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_fields_decimal.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_fields_minutes_duration.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_fields_one2many.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_fields_rich_text.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_filters.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_locale_api.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_ordered_rest_view_mixin.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_perm_tests.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_permissions.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_perms_api.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_post_migrate_groups_update.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_profiling_middleware.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_settings.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_shortcuts.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_timezone_middleware.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_tools.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_user_queryset.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_view_decorators.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_view_fields.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/urls.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/views.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_base_user/__init__.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_base_user/migrations/0001_initial.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_base_user/migrations/__init__.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_base_user/settings/__init__.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_base_user/settings/ci.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_base_user/settings/common.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_base_user/settings/dev.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_base_user/settings/dev_custom_example.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_base_user/settings/dev_default.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_base_user/tests/__init__.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_base_user/tests/test_api.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_base_user/tests/test_auth_api.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_base_user/urls.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_base_user/views.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_custom_user/__init__.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_custom_user/migrations/0001_initial.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_custom_user/migrations/__init__.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_custom_user/models.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_custom_user/settings/__init__.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_custom_user/settings/ci.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_custom_user/settings/common.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_custom_user/settings/dev.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_custom_user/settings/dev_custom_example.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_custom_user/settings/dev_default.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_custom_user/tests/__init__.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_custom_user/tests/test_api.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_custom_user/tests/test_auth_api.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_custom_user/urls.py +0 -0
- {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/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.dev16
|
|
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.dev16
|
|
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
|
|
@@ -135,6 +135,8 @@ tests/migrations/0001_initial.py
|
|
|
135
135
|
tests/migrations/0002_alter_book_cover.py
|
|
136
136
|
tests/migrations/0003_book_local_file.py
|
|
137
137
|
tests/migrations/0004_mfausermixin_fields.py
|
|
138
|
+
tests/migrations/0005_mfausermixin_fields_fix.py
|
|
139
|
+
tests/migrations/0006_rename_otp_enabled_user_mfa_authenticator_enabled_and_more.py
|
|
138
140
|
tests/migrations/__init__.py
|
|
139
141
|
tests/settings/__init__.py
|
|
140
142
|
tests/settings/ci.py
|
|
Binary file
|
{django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/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-
|
|
10
|
+
"POT-Creation-Date: 2026-04-23 14:57+0200\n"
|
|
11
11
|
"PO-Revision-Date: 2021-06-22 23:31+0200\n"
|
|
12
12
|
"Last-Translator: \n"
|
|
13
13
|
"Language-Team: \n"
|
|
@@ -61,11 +61,11 @@ msgstr ""
|
|
|
61
61
|
|
|
62
62
|
#: models/abstract_pfx_base_user.py:52
|
|
63
63
|
msgid "user"
|
|
64
|
-
msgstr ""
|
|
64
|
+
msgstr "utilisateur"
|
|
65
65
|
|
|
66
66
|
#: models/abstract_pfx_base_user.py:53
|
|
67
67
|
msgid "users"
|
|
68
|
-
msgstr ""
|
|
68
|
+
msgstr "utilisateurs"
|
|
69
69
|
|
|
70
70
|
#: models/login_ban.py:54
|
|
71
71
|
msgid "Username"
|
|
@@ -112,22 +112,32 @@ msgid "Temporary SMS phone number"
|
|
|
112
112
|
msgstr "Numéro de téléphone temporaire (SMS)"
|
|
113
113
|
|
|
114
114
|
#: models/mfa_user_mixin.py:35
|
|
115
|
-
#, fuzzy
|
|
116
|
-
#| msgid "OTP enabled"
|
|
117
115
|
msgid "SMS MFA enabled"
|
|
118
|
-
msgstr "
|
|
116
|
+
msgstr "MFA SMS activé"
|
|
119
117
|
|
|
120
|
-
#: models/mfa_user_mixin.py:
|
|
118
|
+
#: models/mfa_user_mixin.py:38
|
|
121
119
|
msgid "Email MFA enabled"
|
|
122
120
|
msgstr "MFA par email activé"
|
|
123
121
|
|
|
124
|
-
#: models/mfa_user_mixin.py:
|
|
122
|
+
#: models/mfa_user_mixin.py:41
|
|
125
123
|
msgid "MFA setup dismissed"
|
|
126
124
|
msgstr "Configuration MFA annulée"
|
|
127
125
|
|
|
128
|
-
#: models/mfa_user_mixin.py:
|
|
129
|
-
msgid "
|
|
130
|
-
msgstr "
|
|
126
|
+
#: models/mfa_user_mixin.py:44
|
|
127
|
+
msgid "Authenticator MFA enabled"
|
|
128
|
+
msgstr "Application MFA activée"
|
|
129
|
+
|
|
130
|
+
#: models/mfa_user_mixin.py:61
|
|
131
|
+
msgid "MFA enabled"
|
|
132
|
+
msgstr "MFA activé"
|
|
133
|
+
|
|
134
|
+
#: models/mfa_user_mixin.py:68
|
|
135
|
+
msgid "MFA setup required"
|
|
136
|
+
msgstr "Configuration MFA requise"
|
|
137
|
+
|
|
138
|
+
#: models/mfa_user_mixin.py:74
|
|
139
|
+
msgid "Is MFA forced"
|
|
140
|
+
msgstr "MFA est forcé"
|
|
131
141
|
|
|
132
142
|
#: models/pfx_models.py:14
|
|
133
143
|
#, python-format
|
|
@@ -245,51 +255,55 @@ msgstr ""
|
|
|
245
255
|
"Votre connexion est temporairement désactivée après plusieurs tentatives "
|
|
246
256
|
"infructueuses, veuillez réessayer dans {seconds} secondes."
|
|
247
257
|
|
|
248
|
-
#: views/authentication_views.py:
|
|
258
|
+
#: views/authentication_views.py:209
|
|
249
259
|
msgid "Successful login"
|
|
250
260
|
msgstr "Connexion réussie"
|
|
251
261
|
|
|
252
|
-
#: views/authentication_views.py:
|
|
262
|
+
#: views/authentication_views.py:386
|
|
253
263
|
msgid "This field is required."
|
|
254
264
|
msgstr "Ce champ est requis."
|
|
255
265
|
|
|
256
|
-
#: views/authentication_views.py:
|
|
266
|
+
#: views/authentication_views.py:397
|
|
257
267
|
msgid "Code sent."
|
|
258
268
|
msgstr "Code envoyé."
|
|
259
269
|
|
|
260
|
-
#: views/authentication_views.py:
|
|
270
|
+
#: views/authentication_views.py:460 views/authentication_views.py:628
|
|
261
271
|
msgid "password updated successfully"
|
|
262
272
|
msgstr "le mot de passe a été mis à jour avec succès"
|
|
263
273
|
|
|
264
|
-
#: views/authentication_views.py:
|
|
274
|
+
#: views/authentication_views.py:465
|
|
265
275
|
msgid "Incorrect password"
|
|
266
276
|
msgstr "Mot de passe incorrect"
|
|
267
277
|
|
|
268
|
-
#: views/authentication_views.py:
|
|
278
|
+
#: views/authentication_views.py:468 views/authentication_views.py:637
|
|
269
279
|
msgid "Empty password is not allowed"
|
|
270
280
|
msgstr "Un mot de passe vide n’est pas autorisé"
|
|
271
281
|
|
|
272
|
-
#: views/authentication_views.py:
|
|
282
|
+
#: views/authentication_views.py:557
|
|
273
283
|
msgid "User and token are valid"
|
|
274
284
|
msgstr "L'utilisateur et le token sont valides"
|
|
275
285
|
|
|
276
|
-
#: views/authentication_views.py:
|
|
286
|
+
#: views/authentication_views.py:559
|
|
277
287
|
msgid "User or token is invalid"
|
|
278
288
|
msgstr "L'utilisateur ou le token est invalide"
|
|
279
289
|
|
|
280
|
-
#: views/authentication_views.py:
|
|
290
|
+
#: views/authentication_views.py:671
|
|
281
291
|
msgid "OTP is already enabled"
|
|
282
292
|
msgstr "OTP est déjà activé"
|
|
283
293
|
|
|
284
|
-
#: views/authentication_views.py:
|
|
294
|
+
#: views/authentication_views.py:711
|
|
295
|
+
msgid "OTP enabled"
|
|
296
|
+
msgstr "OTP activé"
|
|
297
|
+
|
|
298
|
+
#: views/authentication_views.py:712 views/authentication_views.py:748
|
|
285
299
|
msgid "Invalid code"
|
|
286
300
|
msgstr "Code invalide"
|
|
287
301
|
|
|
288
|
-
#: views/authentication_views.py:
|
|
302
|
+
#: views/authentication_views.py:747
|
|
289
303
|
msgid "OTP disabled"
|
|
290
304
|
msgstr "OTP désactivé"
|
|
291
305
|
|
|
292
|
-
#: views/authentication_views.py:
|
|
306
|
+
#: views/authentication_views.py:1008
|
|
293
307
|
msgid ""
|
|
294
308
|
"If the email address you entered is correct, you will receive an email from "
|
|
295
309
|
"us with instructions to reset your password."
|
|
@@ -298,7 +312,7 @@ msgstr ""
|
|
|
298
312
|
"un courrier électronique de notre part contenant des instructions pour "
|
|
299
313
|
"réinitialiser votre mot de passe."
|
|
300
314
|
|
|
301
|
-
#: views/authentication_views.py:
|
|
315
|
+
#: views/authentication_views.py:1082
|
|
302
316
|
msgid "A new authentication code has been sent by email."
|
|
303
317
|
msgstr "Un nouveau code d'authentification a été envoyé par e-mail."
|
|
304
318
|
|
|
@@ -32,21 +32,51 @@ class MFAUserMixin(models.Model):
|
|
|
32
32
|
sms_phone_number_tmp = models.CharField(
|
|
33
33
|
_("Temporary SMS phone number"), max_length=32, null=True, blank=True)
|
|
34
34
|
#: SMS MFA enabled.
|
|
35
|
-
|
|
35
|
+
mfa_sms_enabled = models.BooleanField(_("SMS MFA enabled"), default=False)
|
|
36
36
|
#: Email MFA enabled.
|
|
37
|
-
|
|
37
|
+
mfa_email_enabled = models.BooleanField(
|
|
38
|
+
_("Email MFA enabled"), default=False)
|
|
38
39
|
#: User has dismissed or completed the MFA setup screen.
|
|
39
40
|
mfa_setup_dismissed = models.BooleanField(
|
|
40
41
|
_("MFA setup dismissed"), default=False)
|
|
42
|
+
#: Authenticator app MFA explicitly enabled by the user.
|
|
43
|
+
mfa_authenticator_enabled = models.BooleanField(
|
|
44
|
+
_("Authenticator MFA enabled"), default=False)
|
|
41
45
|
|
|
42
46
|
class Meta:
|
|
43
47
|
abstract = True
|
|
44
48
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
def auth_json_repr(self, **kw):
|
|
50
|
+
res = super().auth_json_repr(
|
|
51
|
+
is_mfa=self.is_mfa,
|
|
52
|
+
mfa_authenticator_enabled=self.mfa_authenticator_enabled,
|
|
53
|
+
mfa_sms_enabled=self.mfa_sms_enabled,
|
|
54
|
+
mfa_email_enabled=self.mfa_email_enabled,
|
|
55
|
+
mfa_setup_dismissed=self.mfa_setup_dismissed,
|
|
56
|
+
mfa_setup_required=self.mfa_setup_required,
|
|
57
|
+
mfa_forced=self.mfa_forced,
|
|
58
|
+
**kw)
|
|
59
|
+
return res
|
|
60
|
+
|
|
61
|
+
@rest_property(_("MFA enabled"), "BooleanField")
|
|
62
|
+
def is_mfa(self):
|
|
63
|
+
return (
|
|
64
|
+
self.mfa_authenticator_enabled
|
|
65
|
+
or self.mfa_sms_enabled
|
|
66
|
+
or self.mfa_email_enabled)
|
|
67
|
+
|
|
68
|
+
@rest_property(_("MFA setup required"), "BooleanField")
|
|
69
|
+
def mfa_setup_required(self):
|
|
70
|
+
return (
|
|
71
|
+
self.mfa_forced and
|
|
72
|
+
not self.mfa_setup_dismissed)
|
|
73
|
+
|
|
74
|
+
@rest_property(_("Is MFA forced"), "BooleanField")
|
|
75
|
+
def mfa_forced(self):
|
|
76
|
+
"""Return True if MFA is forced for this user.
|
|
77
|
+
|
|
78
|
+
Can be overridden to implement per-organisation logic."""
|
|
79
|
+
return settings.PFX_MFA_FORCE
|
|
50
80
|
|
|
51
81
|
def need_otp_response_extra(self):
|
|
52
82
|
"""Add extra attribute to response when need_otp=True."""
|
|
@@ -74,8 +104,10 @@ class MFAUserMixin(models.Model):
|
|
|
74
104
|
if self.is_otp_valid(otp_code, tmp=True):
|
|
75
105
|
self.otp_secret_token = self.otp_secret_token_tmp
|
|
76
106
|
self.otp_secret_token_tmp = None
|
|
107
|
+
self.mfa_authenticator_enabled = True
|
|
77
108
|
self.save(update_fields=[
|
|
78
|
-
'otp_secret_token', 'otp_secret_token_tmp'
|
|
109
|
+
'otp_secret_token', 'otp_secret_token_tmp',
|
|
110
|
+
'mfa_authenticator_enabled'])
|
|
79
111
|
return True
|
|
80
112
|
return False
|
|
81
113
|
|
|
@@ -85,7 +117,9 @@ class MFAUserMixin(models.Model):
|
|
|
85
117
|
Remove the OTP secret token.
|
|
86
118
|
"""
|
|
87
119
|
self.otp_secret_token = None
|
|
88
|
-
self.
|
|
120
|
+
self.mfa_authenticator_enabled = False
|
|
121
|
+
self.save(update_fields=[
|
|
122
|
+
'otp_secret_token', 'mfa_authenticator_enabled'])
|
|
89
123
|
|
|
90
124
|
def get_otp_setup_uri(self, tmp=False, with_color=True):
|
|
91
125
|
"""Return the setup URL for OTP activation.
|
|
@@ -190,10 +190,6 @@ class AuthenticationView(
|
|
|
190
190
|
user.last_login = tz.now()
|
|
191
191
|
user.save(update_fields=['last_login'])
|
|
192
192
|
token = self._prepare_token(user, mode, remember_me)
|
|
193
|
-
mfa_setup_required = (
|
|
194
|
-
isinstance(user, MFAUserMixin) and
|
|
195
|
-
self.is_mfa_forced(user) and
|
|
196
|
-
not user.mfa_setup_dismissed)
|
|
197
193
|
if mode == 'cookie':
|
|
198
194
|
if remember_me:
|
|
199
195
|
expires = datetime.now(
|
|
@@ -203,7 +199,6 @@ class AuthenticationView(
|
|
|
203
199
|
|
|
204
200
|
res = JsonResponse(dict(
|
|
205
201
|
need_otp=False,
|
|
206
|
-
mfa_setup_required=mfa_setup_required,
|
|
207
202
|
user=self.get_user_information(user)))
|
|
208
203
|
res.set_cookie(
|
|
209
204
|
'token', token, secure=settings.PFX_COOKIE_SECURE,
|
|
@@ -213,7 +208,6 @@ class AuthenticationView(
|
|
|
213
208
|
return JsonResponse(dict(
|
|
214
209
|
message=message or _("Successful login"),
|
|
215
210
|
need_otp=False,
|
|
216
|
-
mfa_setup_required=mfa_setup_required,
|
|
217
211
|
token=token,
|
|
218
212
|
user=self.get_user_information(user)))
|
|
219
213
|
|
|
@@ -230,6 +224,12 @@ class AuthenticationView(
|
|
|
230
224
|
self.send_mfa_challenge(user, backend_id)
|
|
231
225
|
token = self._prepare_token(user, mode, remember_me, otp_login=True)
|
|
232
226
|
available = self.get_mfa_backends(user)
|
|
227
|
+
# Ensure the active OOB backend appears in available_backends so the
|
|
228
|
+
# frontend can offer a resend button (e.g. forced-email fallback where
|
|
229
|
+
# email_enabled is False but email is still the active channel).
|
|
230
|
+
available_for_response = list(available)
|
|
231
|
+
if is_oob and backend_id not in available_for_response:
|
|
232
|
+
available_for_response.append(backend_id)
|
|
233
233
|
extra = user.need_otp_response_extra() if isinstance(
|
|
234
234
|
user, MFAUserMixin) else {}
|
|
235
235
|
return JsonResponse(dict(
|
|
@@ -239,7 +239,7 @@ class AuthenticationView(
|
|
|
239
239
|
mfa_backend={'id': backend_id, 'is_oob': is_oob},
|
|
240
240
|
available_backends=[
|
|
241
241
|
{'id': b, 'is_oob': b in {'email', 'sms'}}
|
|
242
|
-
for b in
|
|
242
|
+
for b in available_for_response
|
|
243
243
|
]))
|
|
244
244
|
|
|
245
245
|
# --- MFA helpers ---
|
|
@@ -253,11 +253,12 @@ class AuthenticationView(
|
|
|
253
253
|
return []
|
|
254
254
|
result = []
|
|
255
255
|
for backend_id in settings.PFX_MFA_BACKENDS:
|
|
256
|
-
if backend_id == 'authenticator'
|
|
256
|
+
if (backend_id == 'authenticator'
|
|
257
|
+
and user.mfa_authenticator_enabled):
|
|
257
258
|
result.append(backend_id)
|
|
258
|
-
elif backend_id == 'sms' and user.
|
|
259
|
+
elif backend_id == 'sms' and user.mfa_sms_enabled:
|
|
259
260
|
result.append(backend_id)
|
|
260
|
-
elif backend_id == 'email' and user.
|
|
261
|
+
elif backend_id == 'email' and user.mfa_email_enabled:
|
|
261
262
|
result.append(backend_id)
|
|
262
263
|
return result
|
|
263
264
|
|
|
@@ -265,7 +266,7 @@ class AuthenticationView(
|
|
|
265
266
|
"""Return True if MFA is forced for this user.
|
|
266
267
|
|
|
267
268
|
Can be overridden to implement per-organisation logic."""
|
|
268
|
-
return
|
|
269
|
+
return isinstance(user, MFAUserMixin) and user.mfa_forced
|
|
269
270
|
|
|
270
271
|
def get_active_mfa_backend(self, user):
|
|
271
272
|
"""Return the highest-priority active MFA backend ID for this user.
|
|
@@ -503,8 +504,6 @@ class AuthenticationView(
|
|
|
503
504
|
|
|
504
505
|
:param user: The user"""
|
|
505
506
|
info = user.auth_json_repr()
|
|
506
|
-
if isinstance(user, MFAUserMixin):
|
|
507
|
-
info.update(is_otp=user.is_otp)
|
|
508
507
|
return info
|
|
509
508
|
|
|
510
509
|
@classmethod
|
|
@@ -667,7 +666,7 @@ class AuthenticationView(
|
|
|
667
666
|
if not isinstance(self.request.user, MFAUserMixin):
|
|
668
667
|
logger.error("User must inherit MFAUserMixin to use MFA")
|
|
669
668
|
raise NotFoundError()
|
|
670
|
-
if self.request.user.
|
|
669
|
+
if self.request.user.mfa_authenticator_enabled:
|
|
671
670
|
return JsonResponse(
|
|
672
671
|
dict(message=_("OTP is already enabled")), status=400)
|
|
673
672
|
self.request.user.enable_otp()
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from django.db import migrations, models
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Migration(migrations.Migration):
|
|
5
|
+
|
|
6
|
+
dependencies = [
|
|
7
|
+
('tests', '0004_mfausermixin_fields'),
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
operations = [
|
|
11
|
+
migrations.AddField(
|
|
12
|
+
model_name='user',
|
|
13
|
+
name='otp_enabled',
|
|
14
|
+
field=models.BooleanField(
|
|
15
|
+
default=False, verbose_name='Authenticator MFA enabled'),
|
|
16
|
+
),
|
|
17
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Generated by Django 4.2.30 on 2026-04-23 07:33
|
|
2
|
+
# flake8: noqa
|
|
3
|
+
|
|
4
|
+
from django.db import migrations
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Migration(migrations.Migration):
|
|
8
|
+
|
|
9
|
+
dependencies = [
|
|
10
|
+
('tests', '0005_mfausermixin_fields_fix'),
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
operations = [
|
|
14
|
+
migrations.RenameField(
|
|
15
|
+
model_name='user',
|
|
16
|
+
old_name='otp_enabled',
|
|
17
|
+
new_name='mfa_authenticator_enabled',
|
|
18
|
+
),
|
|
19
|
+
migrations.RenameField(
|
|
20
|
+
model_name='user',
|
|
21
|
+
old_name='email_enabled',
|
|
22
|
+
new_name='mfa_email_enabled',
|
|
23
|
+
),
|
|
24
|
+
migrations.RenameField(
|
|
25
|
+
model_name='user',
|
|
26
|
+
old_name='sms_enabled',
|
|
27
|
+
new_name='mfa_sms_enabled',
|
|
28
|
+
),
|
|
29
|
+
]
|
|
@@ -2,7 +2,7 @@ from .basic_api_errors import BasicAPIErrorTest
|
|
|
2
2
|
from .basic_api_test import BasicAPITest
|
|
3
3
|
from .test_api_doc import ApiDocTest
|
|
4
4
|
from .test_api_doc_search import TestAPIDocSearch
|
|
5
|
-
from .test_auth_api import AuthAPITest
|
|
5
|
+
from .test_auth_api import AuthAPITest, MFALoginTest
|
|
6
6
|
from .test_body_mixin import TestBodyMixin
|
|
7
7
|
from .test_cache import TestCache
|
|
8
8
|
from .test_client import TestApiClient
|
|
@@ -924,6 +924,7 @@ class MFALoginTest(TestAssertMixin, TransactionTestCase):
|
|
|
924
924
|
|
|
925
925
|
def enable_otp(self, user):
|
|
926
926
|
user.otp_secret_token = pyotp.random_base32()
|
|
927
|
+
user.mfa_authenticator_enabled = True
|
|
927
928
|
user.save()
|
|
928
929
|
user.refresh_from_db()
|
|
929
930
|
return pyotp.totp.TOTP(user.otp_secret_token)
|
|
@@ -948,10 +949,10 @@ class MFALoginTest(TestAssertMixin, TransactionTestCase):
|
|
|
948
949
|
@override_settings(
|
|
949
950
|
PFX_OTP_IMAGE="https://example.org/fake.png",
|
|
950
951
|
PFX_OTP_COLOR="FF0000")
|
|
951
|
-
def
|
|
952
|
+
def test_mfa_authenticator_enabled(self):
|
|
952
953
|
self.client.login(username='jrr.tolkien', password='RIGHT PASSWORD')
|
|
953
954
|
|
|
954
|
-
self.assertEqual(self.user1.
|
|
955
|
+
self.assertEqual(self.user1.is_mfa, False)
|
|
955
956
|
response = self.client.get('/api/auth/otp/setup-uri')
|
|
956
957
|
self.assertRC(response, 200)
|
|
957
958
|
self.assertJIn(response, 'setup_uri', "otpauth://totp/")
|
|
@@ -978,9 +979,9 @@ class MFALoginTest(TestAssertMixin, TransactionTestCase):
|
|
|
978
979
|
self.user1.refresh_from_db()
|
|
979
980
|
self.assertEqual(len(self.user1.otp_secret_token), 32)
|
|
980
981
|
self.assertIsNone(self.user1.otp_secret_token_tmp)
|
|
981
|
-
self.assertEqual(self.user1.
|
|
982
|
+
self.assertEqual(self.user1.is_mfa, True)
|
|
982
983
|
|
|
983
|
-
def
|
|
984
|
+
def test_mfa_authenticator_enabled_bad_code(self):
|
|
984
985
|
self.client.login(username='jrr.tolkien', password='RIGHT PASSWORD')
|
|
985
986
|
|
|
986
987
|
response = self.client.get('/api/auth/otp/setup-uri')
|
|
@@ -1009,7 +1010,7 @@ class MFALoginTest(TestAssertMixin, TransactionTestCase):
|
|
|
1009
1010
|
def test_otp_disable(self):
|
|
1010
1011
|
totp = self.enable_otp(self.user1)
|
|
1011
1012
|
|
|
1012
|
-
self.assertEqual(self.user1.
|
|
1013
|
+
self.assertEqual(self.user1.is_mfa, True)
|
|
1013
1014
|
self.otp_login(self.user1, 'RIGHT PASSWORD')
|
|
1014
1015
|
response = self.client.put('/api/auth/otp/disable', dict(
|
|
1015
1016
|
otp_code=totp.now()))
|
|
@@ -1018,7 +1019,7 @@ class MFALoginTest(TestAssertMixin, TransactionTestCase):
|
|
|
1018
1019
|
self.user1.refresh_from_db()
|
|
1019
1020
|
self.assertIsNone(self.user1.otp_secret_token)
|
|
1020
1021
|
self.assertIsNone(self.user1.otp_secret_token_tmp)
|
|
1021
|
-
self.assertEqual(self.user1.
|
|
1022
|
+
self.assertEqual(self.user1.is_mfa, False)
|
|
1022
1023
|
|
|
1023
1024
|
@override_settings(
|
|
1024
1025
|
PFX_HOTP_CODE_VALIDITY=15,
|
|
@@ -1090,8 +1091,8 @@ class MFALoginTest(TestAssertMixin, TransactionTestCase):
|
|
|
1090
1091
|
token=token,
|
|
1091
1092
|
otp_code=totp.now()))
|
|
1092
1093
|
self.assertRC(response, 200)
|
|
1093
|
-
self.assertJE(response, 'user.
|
|
1094
|
-
self.assertJE(response, 'mfa_setup_required', False)
|
|
1094
|
+
self.assertJE(response, 'user.is_mfa', True)
|
|
1095
|
+
self.assertJE(response, 'user.mfa_setup_required', False)
|
|
1095
1096
|
headers = jwt.get_unverified_header(token)
|
|
1096
1097
|
self.assertEqual(headers['pfx_user_pk'], self.user1.pk)
|
|
1097
1098
|
decoded = jwt.decode(
|
|
@@ -1103,6 +1104,7 @@ class MFALoginTest(TestAssertMixin, TransactionTestCase):
|
|
|
1103
1104
|
self.assertEqual(
|
|
1104
1105
|
datetime.fromtimestamp(decoded['exp']),
|
|
1105
1106
|
datetime(2023, 5, 1, 8, 40))
|
|
1107
|
+
response.print()
|
|
1106
1108
|
|
|
1107
1109
|
@override_settings(PFX_TOKEN_LONG_VALIDITY={'days': 1})
|
|
1108
1110
|
def test_otp_login_remember_me(self):
|
|
@@ -1444,7 +1446,7 @@ class MFALoginTest(TestAssertMixin, TransactionTestCase):
|
|
|
1444
1446
|
"""Login with email+authenticator:
|
|
1445
1447
|
email (highest priority) is active backend."""
|
|
1446
1448
|
self.enable_otp(self.user1)
|
|
1447
|
-
self.user1.
|
|
1449
|
+
self.user1.mfa_email_enabled = True
|
|
1448
1450
|
self.user1.save()
|
|
1449
1451
|
with patch(
|
|
1450
1452
|
'pfx.pfxcore.views.authentication_views'
|
|
@@ -1458,19 +1460,19 @@ class MFALoginTest(TestAssertMixin, TransactionTestCase):
|
|
|
1458
1460
|
self.assertJE(response, 'available_backends.@0.id', 'email')
|
|
1459
1461
|
self.assertJE(response, 'available_backends.@1.id', 'authenticator')
|
|
1460
1462
|
|
|
1461
|
-
# --- mfa_setup_required ---
|
|
1463
|
+
# --- user.mfa_setup_required ---
|
|
1462
1464
|
|
|
1463
1465
|
def test_login_success_mfa_setup_required_false_by_default(self):
|
|
1464
1466
|
"""Direct login, no MFA force: mfa_setup_required=False."""
|
|
1465
1467
|
response = self.do_login()
|
|
1466
1468
|
self.assertRC(response, 200)
|
|
1467
1469
|
self.assertJE(response, 'need_otp', False)
|
|
1468
|
-
self.assertJE(response, 'mfa_setup_required', False)
|
|
1470
|
+
self.assertJE(response, 'user.mfa_setup_required', False)
|
|
1469
1471
|
|
|
1470
1472
|
@override_settings(PFX_MFA_FORCE=True)
|
|
1471
1473
|
def test_login_success_mfa_setup_required_true(self):
|
|
1472
1474
|
"""After OTP login, MFA forced, setup not dismissed:
|
|
1473
|
-
mfa_setup_required=True."""
|
|
1475
|
+
user.mfa_setup_required=True."""
|
|
1474
1476
|
totp = self.enable_otp(self.user1)
|
|
1475
1477
|
response = self.do_login()
|
|
1476
1478
|
self.assertRC(response, 200)
|
|
@@ -1478,12 +1480,12 @@ class MFALoginTest(TestAssertMixin, TransactionTestCase):
|
|
|
1478
1480
|
response = self.client.post('/api/auth/otp/login', dict(
|
|
1479
1481
|
token=otp_token, otp_code=totp.now()))
|
|
1480
1482
|
self.assertRC(response, 200)
|
|
1481
|
-
self.assertJE(response, 'mfa_setup_required', True)
|
|
1483
|
+
self.assertJE(response, 'user.mfa_setup_required', True)
|
|
1482
1484
|
|
|
1483
1485
|
@override_settings(PFX_MFA_FORCE=True)
|
|
1484
1486
|
def test_login_success_mfa_setup_required_false_when_dismissed(self):
|
|
1485
1487
|
"""After OTP login, MFA forced, setup dismissed:
|
|
1486
|
-
mfa_setup_required=False."""
|
|
1488
|
+
user.mfa_setup_required=False."""
|
|
1487
1489
|
totp = self.enable_otp(self.user1)
|
|
1488
1490
|
self.user1.mfa_setup_dismissed = True
|
|
1489
1491
|
self.user1.save()
|
|
@@ -1493,14 +1495,14 @@ class MFALoginTest(TestAssertMixin, TransactionTestCase):
|
|
|
1493
1495
|
response = self.client.post('/api/auth/otp/login', dict(
|
|
1494
1496
|
token=otp_token, otp_code=totp.now()))
|
|
1495
1497
|
self.assertRC(response, 200)
|
|
1496
|
-
self.assertJE(response, 'mfa_setup_required', False)
|
|
1498
|
+
self.assertJE(response, 'user.mfa_setup_required', False)
|
|
1497
1499
|
|
|
1498
1500
|
# --- email backend ---
|
|
1499
1501
|
|
|
1500
1502
|
@override_settings(PFX_MFA_BACKENDS=['email'])
|
|
1501
1503
|
def test_login_email_backend_sends_challenge(self):
|
|
1502
1504
|
"""Login with email backend: sends email and returns is_oob=True."""
|
|
1503
|
-
self.user1.
|
|
1505
|
+
self.user1.mfa_email_enabled = True
|
|
1504
1506
|
self.user1.otp_secret_token = pyotp.random_base32()
|
|
1505
1507
|
self.user1.save()
|
|
1506
1508
|
response = self.do_login()
|
|
@@ -1515,7 +1517,7 @@ class MFALoginTest(TestAssertMixin, TransactionTestCase):
|
|
|
1515
1517
|
def test_login_email_backend_generates_token_if_missing(self):
|
|
1516
1518
|
"""Login with email backend auto-generates
|
|
1517
1519
|
otp_secret_token if absent."""
|
|
1518
|
-
self.user1.
|
|
1520
|
+
self.user1.mfa_email_enabled = True
|
|
1519
1521
|
self.user1.save()
|
|
1520
1522
|
self.assertIsNone(self.user1.otp_secret_token)
|
|
1521
1523
|
response = self.do_login()
|
|
@@ -1531,7 +1533,7 @@ class MFALoginTest(TestAssertMixin, TransactionTestCase):
|
|
|
1531
1533
|
def test_login_sms_backend_calls_send_sms(self):
|
|
1532
1534
|
"""Login with SMS backend: calls send_mfa_sms_challenge,
|
|
1533
1535
|
returns is_oob=True."""
|
|
1534
|
-
self.user1.
|
|
1536
|
+
self.user1.mfa_sms_enabled = True
|
|
1535
1537
|
self.user1.otp_secret_token = pyotp.random_base32()
|
|
1536
1538
|
self.user1.save()
|
|
1537
1539
|
with patch(
|
|
@@ -1550,7 +1552,7 @@ class MFALoginTest(TestAssertMixin, TransactionTestCase):
|
|
|
1550
1552
|
def test_login_forced_mfa_fallback_to_email(self):
|
|
1551
1553
|
"""MFA forced with no backend configured:
|
|
1552
1554
|
falls back to email challenge."""
|
|
1553
|
-
# No otp_secret_token,
|
|
1555
|
+
# No otp_secret_token, mfa_sms_enabled=False, mfa_email_enabled=False
|
|
1554
1556
|
response = self.do_login()
|
|
1555
1557
|
self.assertRC(response, 200)
|
|
1556
1558
|
self.assertJE(response, 'need_otp', True)
|
|
@@ -1572,7 +1574,7 @@ class MFALoginTest(TestAssertMixin, TransactionTestCase):
|
|
|
1572
1574
|
@override_settings(PFX_MFA_BACKENDS=['authenticator', 'email'])
|
|
1573
1575
|
def test_mfa_challenge_email_success(self):
|
|
1574
1576
|
"""POST /auth/mfa/challenge with email sends a new code."""
|
|
1575
|
-
self.user1.
|
|
1577
|
+
self.user1.mfa_email_enabled = True
|
|
1576
1578
|
self.user1.otp_secret_token = pyotp.random_base32()
|
|
1577
1579
|
self.user1.save()
|
|
1578
1580
|
otp_token = self._get_otp_token()
|
|
@@ -1589,7 +1591,7 @@ class MFALoginTest(TestAssertMixin, TransactionTestCase):
|
|
|
1589
1591
|
"""POST /auth/mfa/challenge: email allowed even without
|
|
1590
1592
|
email_enabled when MFA forced."""
|
|
1591
1593
|
otp_token = self._get_otp_token()
|
|
1592
|
-
# user1 has
|
|
1594
|
+
# user1 has mfa_email_enabled=False,
|
|
1593
1595
|
# but PFX_MFA_FORCE=True => forced_email is True
|
|
1594
1596
|
response = self.client.post('/api/auth/mfa/challenge', {
|
|
1595
1597
|
'token': otp_token,
|
|
@@ -1640,7 +1642,7 @@ class MFALoginTest(TestAssertMixin, TransactionTestCase):
|
|
|
1640
1642
|
@override_settings(PFX_MFA_BACKENDS=['sms'])
|
|
1641
1643
|
def test_mfa_challenge_sms_success(self):
|
|
1642
1644
|
"""POST /auth/mfa/challenge with sms calls send_mfa_sms_challenge."""
|
|
1643
|
-
self.user1.
|
|
1645
|
+
self.user1.mfa_sms_enabled = True
|
|
1644
1646
|
self.user1.otp_secret_token = pyotp.random_base32()
|
|
1645
1647
|
self.user1.save()
|
|
1646
1648
|
sms_path = (
|
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|