django-pfx 1.4.dev28__tar.gz → 1.4.dev36__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.4.dev28 → django-pfx-1.4.dev36}/.gitlab-ci.yml +20 -7
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/PKG-INFO +1 -1
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/django_pfx.egg-info/PKG-INFO +1 -1
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/django_pfx.egg-info/SOURCES.txt +7 -2
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/django_pfx.egg-info/top_level.txt +1 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/doc/source/api.views.rst +18 -1
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/doc/source/authentication.md +42 -14
- django-pfx-1.4.dev36/make_messages +3 -0
- django-pfx-1.4.dev36/manage.py +37 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/locale/fr/LC_MESSAGES/django.po +28 -27
- django-pfx-1.4.dev36/pfx/pfxcore/migrations/0001_initial.py +59 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/models/__init__.py +1 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/models/abstract_pfx_base_user.py +4 -1
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/models/otp_user_mixin.py +39 -0
- django-pfx-1.4.dev36/pfx/pfxcore/models/pfx_user.py +11 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/views/authentication_views.py +2 -2
- django-pfx-1.4.dev36/pfx/settings/dev.py +8 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/setup.cfg +1 -0
- django-pfx-1.4.dev36/tests/__init__.py +0 -0
- django-pfx-1.4.dev36/tests/settings/__init__.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/tests/tests/test_auth_api.py +2 -2
- django-pfx-1.4.dev28/django-admin-test +0 -10
- django-pfx-1.4.dev28/runtest.py +0 -17
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/.gitignore +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/.pre-commit-config.yaml +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/LICENSE +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/MANIFEST.in +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/README.md +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/django_pfx.egg-info/dependency_links.txt +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/django_pfx.egg-info/requires.txt +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/doc/Makefile +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/doc/conf.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/doc/index.rst +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/doc/source/decorator.md +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/doc/source/generate_openapi.md +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/doc/source/getting_started.md +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/doc/source/internationalisation.md +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/doc/source/model.md +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/doc/source/pfx_views.md +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/doc/source/profiling.md +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/doc/source/settings.md +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/doc/source/testing.md +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/img/pfx.png +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/img/pfx.svg +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/__init__.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/__init__.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/apidoc/__init__.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/apidoc/parameters.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/apidoc/schema.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/apidoc/tags.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/apps.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/decorator/__init__.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/decorator/rest.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/default_settings.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/exceptions.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/fields.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/http/__init__.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/http/json_response.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/locale/fr/LC_MESSAGES/django.mo +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/management/__init__.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/management/commands/__init__.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/management/commands/makeapidoc.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/management/commands/profile.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/middleware/__init__.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/middleware/authentication.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/middleware/locale.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/middleware/profiling.py +0 -0
- {django-pfx-1.4.dev28/pfx/pfxcore/serializers → django-pfx-1.4.dev36/pfx/pfxcore/migrations}/__init__.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/models/cache_mixins.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/models/login_ban.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/models/not_null_fields.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/models/pfx_models.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/models/user_filtered_queryset_mixin.py +0 -0
- {django-pfx-1.4.dev28/tests → django-pfx-1.4.dev36/pfx/pfxcore/serializers}/__init__.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/serializers/json.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/settings.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/shortcuts.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/storage/__init__.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/storage/s3_storage.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/templates/registration/otp_code_email.txt +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/templates/registration/otp_code_subject.txt +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/templates/registration/password_reset_email.txt +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/templates/registration/password_reset_subject.txt +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/templates/registration/welcome_email.txt +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/templates/registration/welcome_subject.txt +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/test.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/urls.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/views/__init__.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/views/fields.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/views/filters_views.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/views/locale_views.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/views/parameters/__init__.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/views/parameters/date_format.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/views/parameters/groups.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/views/parameters/list_count.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/views/parameters/list_items.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/views/parameters/list_mode.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/views/parameters/list_order.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/views/parameters/list_search.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/views/parameters/media_redirect.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/views/parameters/meta_fields.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/views/parameters/meta_filters.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/views/parameters/meta_orders.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/views/parameters/subset.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/views/parameters/subset_limit.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/views/parameters/subset_offset.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/views/parameters/subset_page.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/views/parameters/subset_page_size.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/views/parameters/subset_page_subset.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pfx/pfxcore/views/rest_views.py +0 -0
- {django-pfx-1.4.dev28/tests → django-pfx-1.4.dev36/pfx}/settings/__init__.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/pyproject.toml +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/requirements.txt +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/serve-doc +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/setup.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/tests/locale/fr/LC_MESSAGES/django.po +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/tests/models.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/tests/settings/ci.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/tests/settings/common.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/tests/settings/dev.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/tests/settings/dev_custom_example.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/tests/settings/dev_default.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/tests/tests/__init__.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/tests/tests/basic_api_errors.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/tests/tests/basic_api_test.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/tests/tests/test_api_doc.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/tests/tests/test_body_mixin.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/tests/tests/test_cache.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/tests/tests/test_client.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/tests/tests/test_fields.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/tests/tests/test_filters.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/tests/tests/test_locale_api.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/tests/tests/test_perm_tests.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/tests/tests/test_perms_api.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/tests/tests/test_profiling_middleware.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/tests/tests/test_settings.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/tests/tests/test_shortcuts.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/tests/tests/test_timezone_middleware.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/tests/tests/test_tools.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/tests/tests/test_user_queryset.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/tests/tests/test_view_decorators.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/tests/tests/test_view_fields.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/tests/urls.py +0 -0
- {django-pfx-1.4.dev28 → django-pfx-1.4.dev36}/tests/views.py +0 -0
|
@@ -23,20 +23,29 @@ stages:
|
|
|
23
23
|
- build
|
|
24
24
|
- package
|
|
25
25
|
|
|
26
|
+
quality check:
|
|
27
|
+
variables:
|
|
28
|
+
DJANGO_VERSION: 'django==4.2.*' # LTS
|
|
29
|
+
script:
|
|
30
|
+
- apt-get update
|
|
31
|
+
- apt-get install -y gettext
|
|
32
|
+
- flake8 .
|
|
33
|
+
- isort . -c
|
|
34
|
+
- ./make_messages
|
|
35
|
+
- git diff --exit-code # exit if messages changed
|
|
36
|
+
- python manage.py makemigrations --check --dry-run --setting='pfx.settings.dev'
|
|
37
|
+
|
|
26
38
|
test:
|
|
27
39
|
variables:
|
|
28
40
|
POSTGRES_DB: ci
|
|
29
41
|
POSTGRES_USER: postgres
|
|
30
42
|
POSTGRES_PASSWORD: postgres
|
|
31
|
-
|
|
43
|
+
DJANGO_SETTINGS_MODULE: 'tests.settings.ci'
|
|
32
44
|
script:
|
|
33
45
|
- apt-get update
|
|
34
46
|
- apt-get install -y gettext
|
|
35
|
-
-
|
|
36
|
-
-
|
|
37
|
-
- django-admin compilemessages --ignore venv
|
|
38
|
-
- export DJANGO_SETTINGS_MODULE=$TEST_SETTINGS_MODULE
|
|
39
|
-
- coverage run --source='.' runtest.py
|
|
47
|
+
- python manage.py compilemessages -i venv
|
|
48
|
+
- coverage run --source='.' manage.py test
|
|
40
49
|
- coverage report
|
|
41
50
|
- coverage xml
|
|
42
51
|
- coverage html
|
|
@@ -86,6 +95,10 @@ pages:
|
|
|
86
95
|
- master
|
|
87
96
|
|
|
88
97
|
package:
|
|
98
|
+
stage: package
|
|
99
|
+
needs:
|
|
100
|
+
- job: quality check
|
|
101
|
+
- job: test
|
|
89
102
|
rules:
|
|
90
103
|
- if: $CI_COMMIT_TAG =~ /^\d+\.\d+$/
|
|
91
104
|
when: on_success
|
|
@@ -97,6 +110,6 @@ package:
|
|
|
97
110
|
- git fetch --prune --unshallow
|
|
98
111
|
- pip install --upgrade twine
|
|
99
112
|
- pip install -r requirements.txt
|
|
100
|
-
-
|
|
113
|
+
- ./manage.py compilemessages -i venv -i tests
|
|
101
114
|
- python3 -m build
|
|
102
115
|
- TWINE_PASSWORD=${PYPI_DEPLOY_TOKEN} TWINE_USERNAME=__token__ python3 -m twine upload dist/*
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
LICENSE
|
|
5
5
|
MANIFEST.in
|
|
6
6
|
README.md
|
|
7
|
-
|
|
7
|
+
make_messages
|
|
8
|
+
manage.py
|
|
8
9
|
pyproject.toml
|
|
9
10
|
requirements.txt
|
|
10
|
-
runtest.py
|
|
11
11
|
serve-doc
|
|
12
12
|
setup.cfg
|
|
13
13
|
setup.py
|
|
@@ -60,6 +60,8 @@ pfx/pfxcore/middleware/__init__.py
|
|
|
60
60
|
pfx/pfxcore/middleware/authentication.py
|
|
61
61
|
pfx/pfxcore/middleware/locale.py
|
|
62
62
|
pfx/pfxcore/middleware/profiling.py
|
|
63
|
+
pfx/pfxcore/migrations/0001_initial.py
|
|
64
|
+
pfx/pfxcore/migrations/__init__.py
|
|
63
65
|
pfx/pfxcore/models/__init__.py
|
|
64
66
|
pfx/pfxcore/models/abstract_pfx_base_user.py
|
|
65
67
|
pfx/pfxcore/models/cache_mixins.py
|
|
@@ -67,6 +69,7 @@ pfx/pfxcore/models/login_ban.py
|
|
|
67
69
|
pfx/pfxcore/models/not_null_fields.py
|
|
68
70
|
pfx/pfxcore/models/otp_user_mixin.py
|
|
69
71
|
pfx/pfxcore/models/pfx_models.py
|
|
72
|
+
pfx/pfxcore/models/pfx_user.py
|
|
70
73
|
pfx/pfxcore/models/user_filtered_queryset_mixin.py
|
|
71
74
|
pfx/pfxcore/serializers/__init__.py
|
|
72
75
|
pfx/pfxcore/serializers/json.py
|
|
@@ -102,6 +105,8 @@ pfx/pfxcore/views/parameters/subset_offset.py
|
|
|
102
105
|
pfx/pfxcore/views/parameters/subset_page.py
|
|
103
106
|
pfx/pfxcore/views/parameters/subset_page_size.py
|
|
104
107
|
pfx/pfxcore/views/parameters/subset_page_subset.py
|
|
108
|
+
pfx/settings/__init__.py
|
|
109
|
+
pfx/settings/dev.py
|
|
105
110
|
tests/__init__.py
|
|
106
111
|
tests/models.py
|
|
107
112
|
tests/urls.py
|
|
@@ -32,6 +32,18 @@ API Reference
|
|
|
32
32
|
:undoc-members:
|
|
33
33
|
:show-inheritance:
|
|
34
34
|
|
|
35
|
+
.. autoclass:: pfx.pfxcore.models.AbstractPFXBaseUser
|
|
36
|
+
:members:
|
|
37
|
+
:show-inheritance:
|
|
38
|
+
|
|
39
|
+
.. autoclass:: pfx.pfxcore.models.OtpUserMixin
|
|
40
|
+
:members:
|
|
41
|
+
:show-inheritance:
|
|
42
|
+
|
|
43
|
+
.. autoclass:: pfx.pfxcore.models.PFXUser
|
|
44
|
+
:members:
|
|
45
|
+
:show-inheritance:
|
|
46
|
+
|
|
35
47
|
``pfx.pfxcore.views``
|
|
36
48
|
*********************
|
|
37
49
|
|
|
@@ -43,12 +55,17 @@ Base services
|
|
|
43
55
|
:undoc-members:
|
|
44
56
|
:show-inheritance:
|
|
45
57
|
|
|
58
|
+
.. autoclass:: pfx.pfxcore.views.SignupView
|
|
59
|
+
:members:
|
|
60
|
+
:undoc-members:
|
|
61
|
+
:show-inheritance:
|
|
62
|
+
|
|
46
63
|
.. autoclass:: pfx.pfxcore.views.ForgottenPasswordView
|
|
47
64
|
:members:
|
|
48
65
|
:undoc-members:
|
|
49
66
|
:show-inheritance:
|
|
50
67
|
|
|
51
|
-
.. autoclass:: pfx.pfxcore.views.
|
|
68
|
+
.. autoclass:: pfx.pfxcore.views.OtpEmailView
|
|
52
69
|
:members:
|
|
53
70
|
:undoc-members:
|
|
54
71
|
:show-inheritance:
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
# Authentication
|
|
2
2
|
|
|
3
3
|
Django PFX offers services and middlewares for managing user authentication in your API.
|
|
4
|
-
These services replicate some of the functionalities provided by the `django.contrib.auth`
|
|
4
|
+
These services replicate some of the functionalities provided by the {mod}`django.contrib.auth`
|
|
5
5
|
package but in the form of RESTful services.
|
|
6
6
|
They utilize the same user model and authentication backend features,
|
|
7
7
|
including password validation and hashing.
|
|
8
8
|
|
|
9
9
|
## User Model
|
|
10
10
|
|
|
11
|
-
You have the option to use the standard `
|
|
11
|
+
You have the option to use the standard Django User with {class}`pfx.pfxcore.models.PFXUser`
|
|
12
|
+
(which is a {class}`django.contrib.auth.models.User` with PFX required mixins),
|
|
12
13
|
but you may prefer to use your own model. To do this, create your own user class.
|
|
13
14
|
|
|
14
15
|
```python
|
|
15
|
-
from
|
|
16
|
+
from pfx.pfxcore.models import AbstractPFXBaseUser
|
|
16
17
|
|
|
17
|
-
class MyUser(
|
|
18
|
+
class MyUser(AbstractPFXBaseUser):
|
|
18
19
|
pass
|
|
19
20
|
```
|
|
20
21
|
|
|
@@ -28,8 +29,8 @@ AUTH_USER_MODEL = "myapp.MyUser"
|
|
|
28
29
|
|
|
29
30
|
There are two authentication modes available: cookie and bearer token. You can activate either or both by enabling the following middlewares:
|
|
30
31
|
|
|
31
|
-
* `
|
|
32
|
-
* `
|
|
32
|
+
* {class}`pfx.pfxcore.middleware.AuthenticationMiddleware` (bearer token)
|
|
33
|
+
* {class}`pfx.pfxcore.middleware.CookieAuthenticationMiddleware` (cookie)
|
|
33
34
|
|
|
34
35
|
### Token Validity
|
|
35
36
|
|
|
@@ -40,7 +41,7 @@ You can customize token validity by configuring these parameters:
|
|
|
40
41
|
|
|
41
42
|
### Cookie Settings
|
|
42
43
|
|
|
43
|
-
To use the `CookieAuthenticationMiddleware`, you need to configure the following settings:
|
|
44
|
+
To use the {class}`pfx.pfxcore.middleware.CookieAuthenticationMiddleware`, you need to configure the following settings:
|
|
44
45
|
|
|
45
46
|
* `PFX_COOKIE_DOMAIN`: The cookie domain
|
|
46
47
|
* `PFX_COOKIE_SECURE`: `Secure` attribute of the cookie (`True`/`False`)
|
|
@@ -48,28 +49,55 @@ To use the `CookieAuthenticationMiddleware`, you need to configure the following
|
|
|
48
49
|
|
|
49
50
|
See the [MDN Website](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) for more details.
|
|
50
51
|
|
|
52
|
+
### Temporary bans
|
|
53
|
+
|
|
54
|
+
Users will be temporarily banned after several unsuccessful login attempts.
|
|
55
|
+
|
|
56
|
+
In the event of a temporary ban, login services will respond with the HTTP code `429` and
|
|
57
|
+
the `Retry-After` header.
|
|
58
|
+
|
|
59
|
+
#### Settings
|
|
60
|
+
|
|
61
|
+
* `PFX_LOGIN_BAN_FAILED_NUMBER`: The number of failed login attempts before banning. To deactivate the ban completely, set `0` (optional, default `5`).
|
|
62
|
+
* `PFX_LOGIN_BAN_SECONDS_START`: The number of seconds for the first ban (optional, default `60`).
|
|
63
|
+
* `PFX_LOGIN_BAN_SECONDS_STEP`: The number of seconds to be added to the previous ban for consecutive bans (optional, default `60`).
|
|
64
|
+
|
|
51
65
|
### Multifactor Authentication
|
|
52
66
|
Multifactor authentication can be enabled in django-pfx Authentication API.
|
|
53
67
|
|
|
54
68
|
PFX currently provides MFA with One Time Password (OTP), compatible with FreeOTP,
|
|
55
69
|
Google Authenticator and other OTP app.
|
|
56
70
|
|
|
57
|
-
To enable this feature, install django-pfx with otp
|
|
71
|
+
To enable this feature, install django-pfx with otp.
|
|
58
72
|
|
|
59
73
|
```bash
|
|
60
74
|
pip install django-pfx[otp]
|
|
61
75
|
```
|
|
62
76
|
|
|
63
|
-
Then the user class must use the
|
|
77
|
+
Then the user class must use the {class}`pfx.pfxcore.models.OtpUserMixin`.
|
|
64
78
|
|
|
65
79
|
```python
|
|
66
|
-
from
|
|
67
|
-
from pfx.pfxcore.models import OtpUserMixin
|
|
80
|
+
from pfx.pfxcore.models import PFXUser, OtpUserMixin
|
|
68
81
|
|
|
69
|
-
class MyUser(OtpUserMixin,
|
|
82
|
+
class MyUser(OtpUserMixin, PFXUser):
|
|
70
83
|
pass
|
|
71
84
|
```
|
|
72
85
|
|
|
86
|
+
or
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from pfx.pfxcore.models import AbstractPFXBaseUser, OtpUserMixin
|
|
90
|
+
|
|
91
|
+
class MyUser(OtpUserMixin, AbstractPFXBaseUser):
|
|
92
|
+
pass
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
#### Settings
|
|
96
|
+
|
|
97
|
+
* `PFX_TOKEN_OTP_VALIDITY`: Validity for OTP tokens (corresponds to the maximum time to enter
|
|
98
|
+
an OTP code after logging in with a password) (optional, default `{'minutes': 15}`)
|
|
99
|
+
* `PFX_HOTP_CODE_VALIDITY`: Validity of HOTP codes in minutes (used to send code by email) (optional, default `15`).
|
|
100
|
+
|
|
73
101
|
The user can then enable or disable the OTP auth using the [services documented below](#enable-mfa-otp).
|
|
74
102
|
|
|
75
103
|
## Services
|
|
@@ -263,7 +291,7 @@ like so: `https://example.com/reset-password?token={token}&uidb64={uidb64}`.
|
|
|
263
291
|
Your reset page should then call the "set password" service with these two parameters.
|
|
264
292
|
|
|
265
293
|
You can override this class if you need to customize the email templates.
|
|
266
|
-
Refer to
|
|
294
|
+
Refer to {class}`pfx.pfxcore.views.ForgottenPasswordView` for more details.
|
|
267
295
|
|
|
268
296
|
**Request :** `POST` `/auth/forgotten-password`
|
|
269
297
|
|
|
@@ -288,7 +316,7 @@ like so: `https://example.com/reset-password?token={token}&uidb64={uidb64}`.
|
|
|
288
316
|
Your reset page should then call the "set password" service with these two parameters.
|
|
289
317
|
|
|
290
318
|
You can override this class if you need to customize the user or email templates.
|
|
291
|
-
Refer to
|
|
319
|
+
Refer to {class}`pfx.pfxcore.views.SignupView` for more details.
|
|
292
320
|
|
|
293
321
|
**Request :** `POST` `/auth/signup`
|
|
294
322
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""Django's command-line utility for administrative tasks."""
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from django.db.migrations import writer
|
|
8
|
+
|
|
9
|
+
if len(sys.argv) > 1 and sys.argv[1] == 'test':
|
|
10
|
+
logging.disable(logging.WARNING)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
writer.MIGRATION_HEADER_TEMPLATE = """\
|
|
14
|
+
# Generated by Django %(version)s on %(timestamp)s
|
|
15
|
+
# flake8: noqa
|
|
16
|
+
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def main():
|
|
20
|
+
"""Run administrative tasks."""
|
|
21
|
+
cmd = len(sys.argv) > 1 and sys.argv[1]
|
|
22
|
+
os.environ.setdefault(
|
|
23
|
+
"DJANGO_SETTINGS_MODULE",
|
|
24
|
+
cmd == 'test' and "tests.settings.dev" or "pfx.settings.dev")
|
|
25
|
+
try:
|
|
26
|
+
from django.core.management import execute_from_command_line
|
|
27
|
+
except ImportError as exc:
|
|
28
|
+
raise ImportError(
|
|
29
|
+
"Couldn't import Django. Are you sure it's installed and "
|
|
30
|
+
"available on your PYTHONPATH environment variable? Did you "
|
|
31
|
+
"forget to activate a virtual environment?"
|
|
32
|
+
) from exc
|
|
33
|
+
execute_from_command_line(sys.argv)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
if __name__ == '__main__':
|
|
37
|
+
main()
|
|
@@ -7,7 +7,7 @@ msgid ""
|
|
|
7
7
|
msgstr ""
|
|
8
8
|
"Project-Id-Version: \n"
|
|
9
9
|
"Report-Msgid-Bugs-To: \n"
|
|
10
|
-
"POT-Creation-Date: 2024-04-
|
|
10
|
+
"POT-Creation-Date: 2024-04-11 11:40+0200\n"
|
|
11
11
|
"PO-Revision-Date: 2021-06-22 23:31+0200\n"
|
|
12
12
|
"Last-Translator: \n"
|
|
13
13
|
"Language-Team: \n"
|
|
@@ -18,7 +18,7 @@ msgstr ""
|
|
|
18
18
|
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
|
19
19
|
"X-Generator: Poedit 2.3\n"
|
|
20
20
|
|
|
21
|
-
#: decorator/rest.py:
|
|
21
|
+
#: decorator/rest.py:41
|
|
22
22
|
msgid "An internal server error occured."
|
|
23
23
|
msgstr "Une erreur interne du serveur est survenue."
|
|
24
24
|
|
|
@@ -59,39 +59,39 @@ msgstr ""
|
|
|
59
59
|
"Format non valide, il peut s’agir d’un nombre en heures, « 1:05 », « :05 », "
|
|
60
60
|
"« 1h 5m », « 1.5h » ou « 30m »."
|
|
61
61
|
|
|
62
|
-
#: models/login_ban.py:
|
|
62
|
+
#: models/login_ban.py:43
|
|
63
63
|
msgid "Username"
|
|
64
64
|
msgstr "Nom d’utilisateur"
|
|
65
65
|
|
|
66
|
-
#: models/login_ban.py:
|
|
66
|
+
#: models/login_ban.py:44
|
|
67
67
|
msgid "Failed counter"
|
|
68
68
|
msgstr "Compteur d’échec"
|
|
69
69
|
|
|
70
|
-
#: models/login_ban.py:
|
|
70
|
+
#: models/login_ban.py:45
|
|
71
71
|
msgid "Last failed"
|
|
72
72
|
msgstr "Dernier échec"
|
|
73
73
|
|
|
74
|
-
#: models/login_ban.py:
|
|
74
|
+
#: models/login_ban.py:50
|
|
75
75
|
msgid "Login ban"
|
|
76
76
|
msgstr "Login banni"
|
|
77
77
|
|
|
78
|
-
#: models/login_ban.py:
|
|
78
|
+
#: models/login_ban.py:51
|
|
79
79
|
msgid "Login bans"
|
|
80
80
|
msgstr "Login bannis"
|
|
81
81
|
|
|
82
|
-
#: models/otp_user_mixin.py:
|
|
82
|
+
#: models/otp_user_mixin.py:15
|
|
83
83
|
msgid "OTP secret token"
|
|
84
84
|
msgstr "Jeton secret OTP"
|
|
85
85
|
|
|
86
|
-
#: models/otp_user_mixin.py:
|
|
86
|
+
#: models/otp_user_mixin.py:19
|
|
87
87
|
msgid "Temporary OTP secret token"
|
|
88
88
|
msgstr "Jeton secret OTP temporaire"
|
|
89
89
|
|
|
90
|
-
#: models/otp_user_mixin.py:
|
|
90
|
+
#: models/otp_user_mixin.py:21
|
|
91
91
|
msgid "HOTP count"
|
|
92
92
|
msgstr "Compte HOTP"
|
|
93
93
|
|
|
94
|
-
#: models/otp_user_mixin.py:
|
|
94
|
+
#: models/otp_user_mixin.py:23
|
|
95
95
|
msgid "HOTP expiry"
|
|
96
96
|
msgstr "Expiration HOTP"
|
|
97
97
|
|
|
@@ -100,22 +100,22 @@ msgstr "Expiration HOTP"
|
|
|
100
100
|
msgid "%(model_name)s with this %(field_labels)s already exists."
|
|
101
101
|
msgstr "%(model_name)s avec ce/cette %(field_labels)s éxiste déjà."
|
|
102
102
|
|
|
103
|
-
#: shortcuts.py:
|
|
103
|
+
#: shortcuts.py:50
|
|
104
104
|
#, python-brace-format
|
|
105
105
|
msgid "{key} must be an integer number."
|
|
106
106
|
msgstr "{key} doit être un nombre entier."
|
|
107
107
|
|
|
108
|
-
#: shortcuts.py:
|
|
108
|
+
#: shortcuts.py:66
|
|
109
109
|
#, python-brace-format
|
|
110
110
|
msgid "{key} must be a number."
|
|
111
111
|
msgstr "{key} doit être un nombre."
|
|
112
112
|
|
|
113
|
-
#: shortcuts.py:
|
|
113
|
+
#: shortcuts.py:82
|
|
114
114
|
#, python-brace-format
|
|
115
115
|
msgid "{key} must be a date."
|
|
116
116
|
msgstr "{key} doit être une date."
|
|
117
117
|
|
|
118
|
-
#: shortcuts.py:
|
|
118
|
+
#: shortcuts.py:103
|
|
119
119
|
#, python-brace-format
|
|
120
120
|
msgid "{key} must be “true”, “false”, “1”, “0” or empty."
|
|
121
121
|
msgstr "{key} doit être « true », « false », « 1 », « 0 » ou vide"
|
|
@@ -152,6 +152,7 @@ msgid "The %(site_name)s team"
|
|
|
152
152
|
msgstr "L'équipe de %(site_name)s"
|
|
153
153
|
|
|
154
154
|
#: templates/registration/otp_code_subject.txt:2
|
|
155
|
+
#, python-format
|
|
155
156
|
msgid "New authentication code for %(site_name)s"
|
|
156
157
|
msgstr "Nouveau code d’authentication pour %(site_name)s"
|
|
157
158
|
|
|
@@ -191,7 +192,7 @@ msgstr "Bienvenue sur %(site_name)s."
|
|
|
191
192
|
msgid "Welcome on %(site_name)s"
|
|
192
193
|
msgstr "Bienvenue sur %(site_name)s"
|
|
193
194
|
|
|
194
|
-
#: views/authentication_views.py:
|
|
195
|
+
#: views/authentication_views.py:81
|
|
195
196
|
#, python-brace-format
|
|
196
197
|
msgid ""
|
|
197
198
|
"Your connection is temporarily disabled after several unsuccessful attempts, "
|
|
@@ -200,43 +201,43 @@ msgstr ""
|
|
|
200
201
|
"Votre connexion est temporairement désactivée après plusieurs tentatives "
|
|
201
202
|
"infructueuses, veuillez réessayer dans {seconds} secondes."
|
|
202
203
|
|
|
203
|
-
#: views/authentication_views.py:
|
|
204
|
+
#: views/authentication_views.py:243 views/authentication_views.py:404
|
|
204
205
|
msgid "password updated successfully"
|
|
205
206
|
msgstr "le mot de passe a été mis à jour avec succès"
|
|
206
207
|
|
|
207
|
-
#: views/authentication_views.py:
|
|
208
|
+
#: views/authentication_views.py:248
|
|
208
209
|
msgid "Incorrect password"
|
|
209
210
|
msgstr "Mot de passe incorrect"
|
|
210
211
|
|
|
211
|
-
#: views/authentication_views.py:
|
|
212
|
+
#: views/authentication_views.py:251 views/authentication_views.py:413
|
|
212
213
|
msgid "Empty password is not allowed"
|
|
213
214
|
msgstr "Un mot de passe vide n’est pas autorisé"
|
|
214
215
|
|
|
215
|
-
#: views/authentication_views.py:
|
|
216
|
+
#: views/authentication_views.py:338
|
|
216
217
|
msgid "User and token are valid"
|
|
217
218
|
msgstr "L'utilisateur et le token sont valides"
|
|
218
219
|
|
|
219
|
-
#: views/authentication_views.py:
|
|
220
|
+
#: views/authentication_views.py:340
|
|
220
221
|
msgid "User or token is invalid"
|
|
221
222
|
msgstr "L'utilisateur ou le token est invalide"
|
|
222
223
|
|
|
223
|
-
#: views/authentication_views.py:
|
|
224
|
+
#: views/authentication_views.py:446
|
|
224
225
|
msgid "OTP is already activated"
|
|
225
226
|
msgstr "L'OTP est déjà activé"
|
|
226
227
|
|
|
227
|
-
#: views/authentication_views.py:
|
|
228
|
+
#: views/authentication_views.py:484
|
|
228
229
|
msgid "OTP is activated"
|
|
229
230
|
msgstr "L'OTP est activé"
|
|
230
231
|
|
|
231
|
-
#: views/authentication_views.py:
|
|
232
|
+
#: views/authentication_views.py:485 views/authentication_views.py:519
|
|
232
233
|
msgid "Invalid OTP code"
|
|
233
234
|
msgstr "Code OTP invalide"
|
|
234
235
|
|
|
235
|
-
#: views/authentication_views.py:
|
|
236
|
+
#: views/authentication_views.py:518
|
|
236
237
|
msgid "OTP is disabled"
|
|
237
238
|
msgstr "L'OTP est désactivé"
|
|
238
239
|
|
|
239
|
-
#: views/authentication_views.py:
|
|
240
|
+
#: views/authentication_views.py:778
|
|
240
241
|
msgid ""
|
|
241
242
|
"If the email address you entered is correct, you will receive an email from "
|
|
242
243
|
"us with instructions to reset your password."
|
|
@@ -245,7 +246,7 @@ msgstr ""
|
|
|
245
246
|
"un courrier électronique de notre part contenant des instructions pour "
|
|
246
247
|
"réinitialiser votre mot de passe."
|
|
247
248
|
|
|
248
|
-
#: views/authentication_views.py:
|
|
249
|
+
#: views/authentication_views.py:852
|
|
249
250
|
msgid "A new authentication code has been sent by email."
|
|
250
251
|
msgstr "Un nouveau code d'authentification a été envoyé par e-mail."
|
|
251
252
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Generated by Django 4.2.11 on 2024-04-11 04:19
|
|
2
|
+
# flake8: noqa
|
|
3
|
+
|
|
4
|
+
import django.contrib.auth.models
|
|
5
|
+
import django.contrib.auth.validators
|
|
6
|
+
import django.utils.timezone
|
|
7
|
+
from django.db import migrations, models
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Migration(migrations.Migration):
|
|
11
|
+
|
|
12
|
+
initial = True
|
|
13
|
+
|
|
14
|
+
dependencies = [
|
|
15
|
+
('auth', '0012_alter_user_first_name_max_length'),
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
operations = [
|
|
19
|
+
migrations.CreateModel(
|
|
20
|
+
name='LoginBan',
|
|
21
|
+
fields=[
|
|
22
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
23
|
+
('username', models.CharField(max_length=150, unique=True, verbose_name='Username')),
|
|
24
|
+
('failed_counter', models.IntegerField(verbose_name='Failed counter')),
|
|
25
|
+
('last_failed', models.DateTimeField(auto_now=True, verbose_name='Last failed')),
|
|
26
|
+
],
|
|
27
|
+
options={
|
|
28
|
+
'verbose_name': 'Login ban',
|
|
29
|
+
'verbose_name_plural': 'Login bans',
|
|
30
|
+
},
|
|
31
|
+
),
|
|
32
|
+
migrations.CreateModel(
|
|
33
|
+
name='PFXUser',
|
|
34
|
+
fields=[
|
|
35
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
36
|
+
('password', models.CharField(max_length=128, verbose_name='password')),
|
|
37
|
+
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
|
38
|
+
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
|
39
|
+
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
|
|
40
|
+
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
|
|
41
|
+
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
|
42
|
+
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
|
|
43
|
+
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
|
44
|
+
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
|
45
|
+
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
|
46
|
+
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
|
47
|
+
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
|
|
48
|
+
],
|
|
49
|
+
options={
|
|
50
|
+
'verbose_name': 'user',
|
|
51
|
+
'verbose_name_plural': 'users',
|
|
52
|
+
'abstract': False,
|
|
53
|
+
'swappable': 'AUTH_USER_MODEL',
|
|
54
|
+
},
|
|
55
|
+
managers=[
|
|
56
|
+
('objects', django.contrib.auth.models.UserManager()),
|
|
57
|
+
],
|
|
58
|
+
),
|
|
59
|
+
]
|
|
@@ -2,6 +2,8 @@ from django.contrib.auth.models import AbstractBaseUser
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
class AbstractPFXBaseUser(AbstractBaseUser):
|
|
5
|
+
"""The base abstract user for PFX."""
|
|
6
|
+
|
|
5
7
|
class Meta:
|
|
6
8
|
abstract = True
|
|
7
9
|
|
|
@@ -10,6 +12,7 @@ class AbstractPFXBaseUser(AbstractBaseUser):
|
|
|
10
12
|
Return a user secret to sign JWT token.
|
|
11
13
|
|
|
12
14
|
If not empty, the JWT token validity depends on all values
|
|
13
|
-
user to build the return string.
|
|
15
|
+
user to build the return string. So, each time the returned value
|
|
16
|
+
changes, the previously issued tokens will no longer be valid.
|
|
14
17
|
"""
|
|
15
18
|
return self.password
|
|
@@ -8,23 +8,42 @@ from pfx.pfxcore.settings import settings
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class OtpUserMixin(models.Model):
|
|
11
|
+
"""A mixin to enable OTP MFA on a user class."""
|
|
12
|
+
|
|
13
|
+
#: OTP secret token.
|
|
11
14
|
otp_secret_token = models.CharField(
|
|
12
15
|
_("OTP secret token"), max_length=32, null=True,
|
|
13
16
|
blank=True, unique=True)
|
|
17
|
+
#: Temporary OTP secret token (needs confirmation).
|
|
14
18
|
otp_secret_token_tmp = models.CharField(
|
|
15
19
|
_("Temporary OTP secret token"), max_length=32, null=True, blank=True)
|
|
20
|
+
#: HOTP count.
|
|
16
21
|
hotp_count = models.IntegerField(_("HOTP count"), default=0)
|
|
22
|
+
#: HOTP expiry.
|
|
17
23
|
hotp_expiry = models.DateTimeField(_("HOTP expiry"), default=timezone.now)
|
|
18
24
|
|
|
19
25
|
class Meta:
|
|
20
26
|
abstract = True
|
|
21
27
|
|
|
22
28
|
def enable_otp(self):
|
|
29
|
+
"""Activate OTP for this user.
|
|
30
|
+
|
|
31
|
+
Generates a new temporary OTP secret token. To complete activation,
|
|
32
|
+
call `confirm_otp` with a valid code.
|
|
33
|
+
"""
|
|
23
34
|
import pyotp
|
|
24
35
|
self.otp_secret_token_tmp = pyotp.random_base32()
|
|
25
36
|
self.save(update_fields=['otp_secret_token_tmp'])
|
|
26
37
|
|
|
27
38
|
def confirm_otp(self, otp_code):
|
|
39
|
+
"""Confirm OTP activation for this user.
|
|
40
|
+
|
|
41
|
+
Set the OTP secret token from the temporary one if the provided
|
|
42
|
+
code is valid.
|
|
43
|
+
|
|
44
|
+
:param otp_code: A valid OTP code for the temporary OTP secret key.
|
|
45
|
+
:returns: `True` if success, `False` otherwise.
|
|
46
|
+
"""
|
|
28
47
|
if self.is_otp_valid(otp_code, tmp=True):
|
|
29
48
|
self.otp_secret_token = self.otp_secret_token_tmp
|
|
30
49
|
self.otp_secret_token_tmp = None
|
|
@@ -34,10 +53,16 @@ class OtpUserMixin(models.Model):
|
|
|
34
53
|
return False
|
|
35
54
|
|
|
36
55
|
def disable_otp(self):
|
|
56
|
+
"""Disable OTP for this user.
|
|
57
|
+
|
|
58
|
+
Remove the OTP secret token.
|
|
59
|
+
"""
|
|
37
60
|
self.otp_secret_token = None
|
|
38
61
|
self.save(update_fields=['otp_secret_token'])
|
|
39
62
|
|
|
40
63
|
def get_otp_setup_uri(self, tmp=False):
|
|
64
|
+
"""Return the setup URL for OTP activation.
|
|
65
|
+
"""
|
|
41
66
|
import pyotp
|
|
42
67
|
return pyotp.totp.TOTP(
|
|
43
68
|
tmp and self.otp_secret_token_tmp or
|
|
@@ -45,6 +70,13 @@ class OtpUserMixin(models.Model):
|
|
|
45
70
|
name=self.email, issuer_name=settings.PFX_SITE_NAME)
|
|
46
71
|
|
|
47
72
|
def is_otp_valid(self, otp_code, tmp=False):
|
|
73
|
+
"""Verify an OTP code.
|
|
74
|
+
|
|
75
|
+
:param otp_code: A valid OTP code for the OTP secret key.
|
|
76
|
+
:param tmp: If `True`, verify the code with the temporary
|
|
77
|
+
OTP secret key.
|
|
78
|
+
:returns: `True` if the code is valid, `False` otherwise.
|
|
79
|
+
"""
|
|
48
80
|
import pyotp
|
|
49
81
|
totp = pyotp.parse_uri(self.get_otp_setup_uri(tmp=tmp))
|
|
50
82
|
valid = totp.verify(otp_code)
|
|
@@ -56,10 +88,17 @@ class OtpUserMixin(models.Model):
|
|
|
56
88
|
return valid
|
|
57
89
|
|
|
58
90
|
def get_user_jwt_signature_key(self):
|
|
91
|
+
"""Return a user secret to sign JWT token.
|
|
92
|
+
|
|
93
|
+
If the user inherit :class:`pfx.pfxcore.models.AbstractPFXBaseUser`,
|
|
94
|
+
add the OTP secret token to the user signature."""
|
|
59
95
|
return super().get_user_jwt_signature_key() + (
|
|
60
96
|
self.otp_secret_token or "")
|
|
61
97
|
|
|
62
98
|
def get_hotp_code(self):
|
|
99
|
+
"""Return a new valid HOTP code.
|
|
100
|
+
|
|
101
|
+
Increment the HOTP counter and reset the expiry."""
|
|
63
102
|
import pyotp
|
|
64
103
|
if not self.otp_secret_token:
|
|
65
104
|
raise Exception("OTP disabled")
|