django-ninja-aio-crud 2.0.0rc7__tar.gz → 2.1.0__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_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/.github/workflows/coverage.yml +1 -1
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/.github/workflows/docs.yml +49 -18
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/PKG-INFO +1 -1
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/api/views/api_view_set.md +51 -0
- django_ninja_aio_crud-2.1.0/docs/auth.md +225 -0
- django_ninja_aio_crud-2.1.0/docs/mixins.md +147 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/mkdocs.yml +5 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/ninja_aio/__init__.py +1 -1
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/ninja_aio/auth.py +121 -3
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/ninja_aio/helpers/api.py +5 -3
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/ninja_aio/types.py +3 -1
- django_ninja_aio_crud-2.1.0/ninja_aio/views/__init__.py +3 -0
- django_ninja_aio_crud-2.0.0rc7/ninja_aio/views.py → django_ninja_aio_crud-2.1.0/ninja_aio/views/api.py +47 -16
- django_ninja_aio_crud-2.1.0/ninja_aio/views/mixins.py +275 -0
- django_ninja_aio_crud-2.1.0/tests/__init__.py +1 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/tests/test_app/models.py +3 -1
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/tests/test_app/views.py +53 -1
- django_ninja_aio_crud-2.1.0/tests/test_auth.py +124 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/tests/views/test_viewset.py +177 -0
- django_ninja_aio_crud-2.0.0rc7/tests/views/__init__.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/.github/dependabot.yml +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/.github/workflows/publish.yml +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/.gitignore +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/.pre-commit-config.yaml +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/LICENSE +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/README.md +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/CNAME +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/api/authentication.md +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/api/models/model_serializer.md +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/api/models/model_util.md +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/api/pagination.md +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/api/renderers/orjson_renderer.md +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/api/views/api_view.md +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/api/views/decorators.md +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/contributing.md +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/extra.css +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/getting_started/images/index/foo-index-create-swagger.png +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/getting_started/images/index/foo-index-delete-swagger.png +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/getting_started/images/index/foo-index-list-swagger.png +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/getting_started/images/index/foo-index-retrieve-swagger.png +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/getting_started/images/index/foo-index-swagger.png +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/getting_started/images/index/foo-index-update-swagger.png +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/getting_started/installation.md +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/getting_started/quick_start.md +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/images/bar-swagger.png +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/images/favicon.ico +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/images/foo-swagger.png +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/images/logo.png +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/images/model_util/foo-reverse-relations-swagger.png +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/index.md +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/release_notes.md +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/requirements.txt +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/tutorial/authentication.md +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/tutorial/crud.md +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/tutorial/filtering.md +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/tutorial/model.md +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/examples/ex_1/models.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/examples/ex_1/urls.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/examples/ex_1/views.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/examples/ex_2/auth.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/examples/ex_2/models.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/examples/ex_2/urls.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/examples/ex_2/views.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/main.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/ninja_aio/api.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/ninja_aio/decorators.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/ninja_aio/exceptions.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/ninja_aio/helpers/__init__.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/ninja_aio/helpers/query.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/ninja_aio/models.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/ninja_aio/parsers.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/ninja_aio/renders.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/ninja_aio/schemas/__init__.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/ninja_aio/schemas/api.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/ninja_aio/schemas/generics.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/ninja_aio/schemas/helpers.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/pyproject.toml +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/requirements.dev.txt +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/run-local-coverage.sh +0 -0
- {django_ninja_aio_crud-2.0.0rc7/tests → django_ninja_aio_crud-2.1.0/tests/core}/__init__.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/tests/core/test_decorators.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/tests/core/test_exceptions_api.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/tests/core/test_renderer_parser.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7/tests/core → django_ninja_aio_crud-2.1.0/tests/generics}/__init__.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/tests/generics/literals.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/tests/generics/models.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/tests/generics/request.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/tests/generics/views.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7/tests/generics → django_ninja_aio_crud-2.1.0/tests/helpers}/__init__.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/tests/helpers/test_many_to_many_api.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7/tests/helpers → django_ninja_aio_crud-2.1.0/tests/models}/__init__.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/tests/models/test_model_util.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/tests/models/test_models_extra.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7/tests/models → django_ninja_aio_crud-2.1.0/tests/test_app}/__init__.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/tests/test_app/schema.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/tests/test_decorators.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/tests/test_exceptions.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/tests/test_query_util.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/tests/test_settings.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7/tests/test_app → django_ninja_aio_crud-2.1.0/tests/views}/__init__.py +0 -0
- {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/tests/views/test_views.py +0 -0
|
@@ -7,7 +7,7 @@ on:
|
|
|
7
7
|
description: 'Docs version to deploy (e.g. v1.0.0)'
|
|
8
8
|
required: true
|
|
9
9
|
default: 'dev'
|
|
10
|
-
type: choice
|
|
10
|
+
type: choice
|
|
11
11
|
options:
|
|
12
12
|
- dev
|
|
13
13
|
- stable
|
|
@@ -16,12 +16,23 @@ on:
|
|
|
16
16
|
make_latest:
|
|
17
17
|
description: 'Set as "latest" and default?'
|
|
18
18
|
type: boolean
|
|
19
|
-
default:
|
|
19
|
+
default: false
|
|
20
20
|
|
|
21
21
|
delete_version:
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
description: 'Docs version to delete'
|
|
23
|
+
required: false
|
|
24
|
+
default: ''
|
|
25
|
+
type: choice
|
|
26
|
+
options:
|
|
27
|
+
- ''
|
|
28
|
+
- dev
|
|
29
|
+
- stable
|
|
30
|
+
- "1.0"
|
|
31
|
+
- "2.0"
|
|
32
|
+
delete_confirm:
|
|
33
|
+
description: 'Confirm deletion of the selected version'
|
|
34
|
+
type: boolean
|
|
35
|
+
default: false
|
|
25
36
|
|
|
26
37
|
permissions:
|
|
27
38
|
contents: write
|
|
@@ -29,8 +40,6 @@ permissions:
|
|
|
29
40
|
jobs:
|
|
30
41
|
deploy:
|
|
31
42
|
runs-on: ubuntu-latest
|
|
32
|
-
|
|
33
|
-
# Only trusted events (SonarQube safe)
|
|
34
43
|
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
|
|
35
44
|
|
|
36
45
|
steps:
|
|
@@ -52,18 +61,15 @@ jobs:
|
|
|
52
61
|
- name: Compute VERSION and MAKE_LATEST
|
|
53
62
|
id: vars
|
|
54
63
|
env:
|
|
55
|
-
# Assign untrusted input to env var FIRST (SonarQube compliant)
|
|
56
64
|
INPUT_VERSION: ${{ inputs.docs_version }}
|
|
57
65
|
shell: bash
|
|
58
66
|
run: |
|
|
59
|
-
# Now use safe shell variable $INPUT_VERSION
|
|
60
67
|
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ -n "$INPUT_VERSION" ]; then
|
|
61
68
|
VERSION="$INPUT_VERSION"
|
|
62
69
|
else
|
|
63
70
|
VERSION="dev"
|
|
64
71
|
fi
|
|
65
72
|
|
|
66
|
-
# Boolean input is always safe ("true"/"false")
|
|
67
73
|
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
|
68
74
|
MAKE_LATEST="${{ inputs.make_latest }}"
|
|
69
75
|
else
|
|
@@ -82,29 +88,54 @@ jobs:
|
|
|
82
88
|
git fetch --all --tags
|
|
83
89
|
git fetch origin gh-pages --depth=1 || true
|
|
84
90
|
|
|
85
|
-
# Idempotent worktree setup
|
|
86
91
|
if git worktree list | grep -q " gh-pages "; then
|
|
87
92
|
git worktree remove gh-pages --force
|
|
88
93
|
fi
|
|
89
94
|
git worktree add gh-pages gh-pages 2>/dev/null || git worktree add gh-pages origin/gh-pages
|
|
90
95
|
|
|
91
96
|
if [ "$MAKE_LATEST" = "true" ]; then
|
|
92
|
-
echo "
|
|
97
|
+
echo "Deploying $VERSION as latest and default"
|
|
93
98
|
mike deploy --push --update-aliases "$VERSION" latest --ignore-remote-status
|
|
94
99
|
mike set-default "$VERSION"
|
|
95
100
|
else
|
|
96
|
-
echo "
|
|
101
|
+
echo "Deploying $VERSION (non-latest)"
|
|
97
102
|
mike deploy --push "$VERSION" --ignore-remote-status
|
|
98
103
|
fi
|
|
99
104
|
|
|
100
105
|
git worktree remove gh-pages --force
|
|
101
106
|
|
|
102
|
-
- name: Delete version
|
|
103
|
-
if: inputs.delete_version != ''
|
|
107
|
+
- name: Delete version (safe)
|
|
108
|
+
if: github.event_name == 'workflow_dispatch' && inputs.delete_version != '' && inputs.delete_confirm == true
|
|
109
|
+
env:
|
|
110
|
+
DELETE_VERSION: ${{ inputs.delete_version }}
|
|
111
|
+
shell: bash
|
|
104
112
|
run: |
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
echo "
|
|
113
|
+
set -euo pipefail
|
|
114
|
+
|
|
115
|
+
echo "Requested delete: $DELETE_VERSION"
|
|
116
|
+
|
|
117
|
+
# Protect aliases and default
|
|
118
|
+
if [ "$DELETE_VERSION" = "latest" ] || [ "$DELETE_VERSION" = "stable" ]; then
|
|
119
|
+
echo "Refusing to delete protected alias: $DELETE_VERSION"
|
|
120
|
+
exit 1
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
# Load current default; fail if attempting to delete it
|
|
124
|
+
CURRENT_DEFAULT="$(mike default || true)"
|
|
125
|
+
if [ -n "$CURRENT_DEFAULT" ] && [ "$DELETE_VERSION" = "$CURRENT_DEFAULT" ]; then
|
|
126
|
+
echo "Refusing to delete current default docs version: $DELETE_VERSION"
|
|
127
|
+
exit 1
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
# Ensure version exists before deleting
|
|
131
|
+
if ! mike list | awk '{print $1}' | grep -Fxq "$DELETE_VERSION"; then
|
|
132
|
+
echo "Version '$DELETE_VERSION' not found. Nothing to delete."
|
|
133
|
+
exit 0
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
echo "Deleting version: $DELETE_VERSION"
|
|
137
|
+
mike delete "$DELETE_VERSION" --push
|
|
138
|
+
echo "Version '$DELETE_VERSION' deleted successfully"
|
|
108
139
|
|
|
109
140
|
- name: List versions
|
|
110
141
|
run: mike list
|
{django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/api/views/api_view_set.md
RENAMED
|
@@ -402,6 +402,57 @@ class UserViewSet(APIViewSet):
|
|
|
402
402
|
return queryset
|
|
403
403
|
```
|
|
404
404
|
|
|
405
|
+
---
|
|
406
|
+
|
|
407
|
+
## ReadOnlyViewSet
|
|
408
|
+
|
|
409
|
+
ReadOnlyViewSet is a convenience subclass of APIViewSet that enables only list and retrieve endpoints. It is equivalent to setting `disable = ["create", "update", "delete"]`.
|
|
410
|
+
|
|
411
|
+
Generated endpoints:
|
|
412
|
+
|
|
413
|
+
- GET `/{base}/` -> List
|
|
414
|
+
- GET `/{base}/{pk}` -> Retrieve
|
|
415
|
+
|
|
416
|
+
Minimal usage:
|
|
417
|
+
|
|
418
|
+
```python
|
|
419
|
+
class MyModelReadOnlyViewSet(ReadOnlyViewSet):
|
|
420
|
+
model = MyModel
|
|
421
|
+
api = api
|
|
422
|
+
|
|
423
|
+
MyModelReadOnlyViewSet().add_views_to_route()
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
Notes:
|
|
427
|
+
|
|
428
|
+
- Supports all features of APIViewSet relevant to read operations (pagination, list filters, auth per verb, dynamic schema generation when using ModelSerializer).
|
|
429
|
+
- M2M endpoints can still be added via `m2m_relations` if desired.
|
|
430
|
+
|
|
431
|
+
## WriteOnlyViewSet
|
|
432
|
+
|
|
433
|
+
WriteOnlyViewSet is a convenience subclass of APIViewSet that enables only create, update, and delete endpoints. It is equivalent to setting `disable = ["list", "retrieve"]`.
|
|
434
|
+
|
|
435
|
+
Generated endpoints:
|
|
436
|
+
|
|
437
|
+
- POST `/{base}/` -> Create
|
|
438
|
+
- PATCH `/{base}/{pk}/` -> Update
|
|
439
|
+
- DELETE `/{base}/{pk}/` -> Delete
|
|
440
|
+
|
|
441
|
+
Minimal usage:
|
|
442
|
+
|
|
443
|
+
```python
|
|
444
|
+
class MyModelWriteOnlyViewSet(WriteOnlyViewSet):
|
|
445
|
+
model = MyModel
|
|
446
|
+
api = api
|
|
447
|
+
|
|
448
|
+
MyModelWriteOnlyViewSet().add_views_to_route()
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
Notes:
|
|
452
|
+
|
|
453
|
+
- Supports auth per verb and dynamic schema generation for write operations when using ModelSerializer.
|
|
454
|
+
- M2M endpoints can still be added via `m2m_relations` if desired.
|
|
455
|
+
|
|
405
456
|
## See Also
|
|
406
457
|
|
|
407
458
|
- [ModelSerializer](../models/model_serializer.md)
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# JWT Authentication and AsyncJwtBearer
|
|
2
|
+
|
|
3
|
+
This page documents the JWT helpers and the `AsyncJwtBearer` class in `ninja_aio/auth.py`, including configuration, validation, and usage in Django Ninja.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
- `AsyncJwtBearer`: Asynchronous HTTP Bearer auth that verifies JWTs, validates claims via a registry, and delegates user resolution to `auth_handler`.
|
|
8
|
+
- Helpers:
|
|
9
|
+
- `validate_key`: Ensures JWK keys are present and of the correct type.
|
|
10
|
+
- `validate_mandatory_claims`: Ensures `iss` and `aud` are present (from settings if not provided).
|
|
11
|
+
- `encode_jwt`: Signs a JWT with time-based claims (`iat`, `nbf`, `exp`) and mandatory `iss/aud`.
|
|
12
|
+
- `decode_jwt`: Verifies and decodes a JWT with a public key and allowed algorithms.
|
|
13
|
+
|
|
14
|
+
## Configuration without settings
|
|
15
|
+
|
|
16
|
+
Settings are not required. Provide keys and claims explicitly:
|
|
17
|
+
|
|
18
|
+
- Pass `private_key` to `encode_jwt` and `public_key` to `decode_jwt`/`AsyncJwtBearer.jwt_public`.
|
|
19
|
+
- Include `iss` and `aud` directly in the `claims` you encode if you are not using settings.
|
|
20
|
+
|
|
21
|
+
Example key usage without settings:
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
# ...existing code...
|
|
25
|
+
from joserfc import jwk
|
|
26
|
+
from ninja_aio.auth import encode_jwt, decode_jwt
|
|
27
|
+
|
|
28
|
+
private_key = jwk.RSAKey.import_key(open("priv.jwk").read())
|
|
29
|
+
public_key = jwk.RSAKey.import_key(open("pub.jwk").read())
|
|
30
|
+
|
|
31
|
+
token = encode_jwt(
|
|
32
|
+
claims={"sub": "123", "iss": "https://auth.example", "aud": "my-api"},
|
|
33
|
+
duration=3600,
|
|
34
|
+
private_key=private_key,
|
|
35
|
+
algorithm="RS256",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
decoded = decode_jwt(token=token, public_key=public_key, algorithms=["RS256"])
|
|
39
|
+
# ...existing code...
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Mandatory claims
|
|
43
|
+
|
|
44
|
+
The library enforces `iss` and `aud` via `JWT_MANDATORY_CLAIMS`. If you do not use settings, include them in the payload you pass to `encode_jwt`.
|
|
45
|
+
|
|
46
|
+
## Configuration with settings (optional)
|
|
47
|
+
|
|
48
|
+
You can centralize configuration in Django settings and omit explicit keys/claims:
|
|
49
|
+
|
|
50
|
+
- `JWT_PRIVATE_KEY`: jwk.RSAKey or jwk.ECKey for signing
|
|
51
|
+
- `JWT_PUBLIC_KEY`: jwk.RSAKey or jwk.ECKey for verification
|
|
52
|
+
- `JWT_ISSUER`: issuer string
|
|
53
|
+
- `JWT_AUDIENCE`: audience string
|
|
54
|
+
|
|
55
|
+
When present:
|
|
56
|
+
|
|
57
|
+
- `encode_jwt` reads `JWT_PRIVATE_KEY` if `private_key` is not passed, and fills `iss`/`aud` via `validate_mandatory_claims` if missing.
|
|
58
|
+
- `decode_jwt` reads `JWT_PUBLIC_KEY` if `public_key` is not passed.
|
|
59
|
+
- `AsyncJwtBearer` can read the public key from settings by assigning `jwt_public = settings.JWT_PUBLIC_KEY`.
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
# settings.py (example)
|
|
63
|
+
JWT_PRIVATE_KEY = jwk.RSAKey.import_key(open("priv.jwk").read())
|
|
64
|
+
JWT_PUBLIC_KEY = jwk.RSAKey.import_key(open("pub.jwk").read())
|
|
65
|
+
JWT_ISSUER = "https://auth.example"
|
|
66
|
+
JWT_AUDIENCE = "my-api"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Usage without passing keys/claims explicitly:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from ninja_aio.auth import encode_jwt, decode_jwt
|
|
73
|
+
# claims missing iss/aud will be completed from settings
|
|
74
|
+
token = encode_jwt(claims={"sub": "123"}, duration=3600)
|
|
75
|
+
|
|
76
|
+
decoded = decode_jwt(token=token) # uses settings.JWT_PUBLIC_KEY
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
AsyncJwtBearer wired to settings:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from django.conf import settings
|
|
83
|
+
from ninja_aio.auth import AsyncJwtBearer
|
|
84
|
+
|
|
85
|
+
class SettingsBearer(AsyncJwtBearer):
|
|
86
|
+
jwt_public = settings.JWT_PUBLIC_KEY
|
|
87
|
+
claims = {
|
|
88
|
+
"iss": {"value": settings.JWT_ISSUER},
|
|
89
|
+
"aud": {"value": settings.JWT_AUDIENCE},
|
|
90
|
+
# Optionally require time-based claims:
|
|
91
|
+
# "exp": {"essential": True},
|
|
92
|
+
# "nbf": {"essential": True},
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async def auth_handler(self, request):
|
|
96
|
+
sub = self.dcd.claims.get("sub")
|
|
97
|
+
return {"user_id": sub}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## AsyncJwtBearer
|
|
101
|
+
|
|
102
|
+
### Key points
|
|
103
|
+
|
|
104
|
+
- `jwt_public`: Must be a JWK (RSA or EC) used to verify signatures.
|
|
105
|
+
- `claims`: Dict passed to `jwt.JWTClaimsRegistry` defining validations (e.g., `iss`, `aud`, `exp`, `nbf`).
|
|
106
|
+
- `algorithms`: Allowed algorithms (default `["RS256"]`).
|
|
107
|
+
- `dcd`: Set after successful decode; instance of `jwt.Token` containing `header` and `claims`.
|
|
108
|
+
- `get_claims()`: Builds the claim registry from `claims`.
|
|
109
|
+
- `validate_claims(claims)`: Validates decoded claims; raises `jose.errors.JoseError` on failure.
|
|
110
|
+
- `auth_handler(request)`: Async hook to resolve application user given the decoded token (`self.dcd`).
|
|
111
|
+
- `authenticate(request, token)`: Decodes, validates, and delegates to `auth_handler`. Returns user or `False`.
|
|
112
|
+
|
|
113
|
+
### Example
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from joserfc import jwk
|
|
117
|
+
from ninja import NinjaAPI
|
|
118
|
+
from ninja_aio.auth import AsyncJwtBearer
|
|
119
|
+
|
|
120
|
+
class MyBearer(AsyncJwtBearer):
|
|
121
|
+
jwt_public = jwk.RSAKey.import_key(open("pub.jwk").read())
|
|
122
|
+
claims = {
|
|
123
|
+
"iss": {"value": "https://auth.example"},
|
|
124
|
+
"aud": {"value": "my-api"},
|
|
125
|
+
# You can add time-based checks if needed:
|
|
126
|
+
# "exp": {"essential": True},
|
|
127
|
+
# "nbf": {"essential": True},
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async def auth_handler(self, request):
|
|
131
|
+
sub = self.dcd.claims.get("sub")
|
|
132
|
+
return {"user_id": sub}
|
|
133
|
+
|
|
134
|
+
api = NinjaAPI()
|
|
135
|
+
|
|
136
|
+
@api.get("/secure", auth=MyBearer())
|
|
137
|
+
def secure_endpoint(request):
|
|
138
|
+
return {"ok": True}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Claims registry helper
|
|
142
|
+
|
|
143
|
+
You can construct and reuse a registry from your class-level `claims`:
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
registry = MyBearer.get_claims()
|
|
147
|
+
# registry.validate(token_claims) # raises JoseError on failure
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## encode_jwt
|
|
151
|
+
|
|
152
|
+
Signs a JWT with safe defaults:
|
|
153
|
+
|
|
154
|
+
- Adds `iat`, `nbf`, and `exp` using timezone-aware `timezone.now()`.
|
|
155
|
+
- Ensures `iss` and `aud` are present via `validate_mandatory_claims` (include them in `claims` if not using settings).
|
|
156
|
+
- Header includes `alg`, `typ=JWT`, and optional `kid`.
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
from joserfc import jwk
|
|
160
|
+
from ninja_aio.auth import encode_jwt
|
|
161
|
+
|
|
162
|
+
private_key = jwk.RSAKey.import_key(open("priv.jwk").read())
|
|
163
|
+
|
|
164
|
+
claims = {"sub": "123", "scope": "read", "iss": "https://auth.example", "aud": "my-api"}
|
|
165
|
+
token = encode_jwt(
|
|
166
|
+
claims=claims,
|
|
167
|
+
duration=3600,
|
|
168
|
+
private_key=private_key,
|
|
169
|
+
algorithm="RS256",
|
|
170
|
+
)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## decode_jwt
|
|
174
|
+
|
|
175
|
+
Verifies and decodes a JWT with a public key and algorithm allow-list.
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
from joserfc import jwk
|
|
179
|
+
from ninja_aio.auth import decode_jwt
|
|
180
|
+
|
|
181
|
+
public_key = jwk.RSAKey.import_key(open("pub.jwk").read())
|
|
182
|
+
|
|
183
|
+
decoded = decode_jwt(
|
|
184
|
+
token=token,
|
|
185
|
+
public_key=public_key,
|
|
186
|
+
algorithms=["RS256"],
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
claims = decoded.claims
|
|
190
|
+
sub = claims.get("sub")
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## validate_key
|
|
194
|
+
|
|
195
|
+
If you do not use settings, pass keys directly. `validate_key` will raise `ValueError` only when neither an explicit key nor a configured setting is provided.
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
from ninja_aio.auth import validate_key
|
|
199
|
+
from joserfc import jwk
|
|
200
|
+
|
|
201
|
+
pkey = validate_key(jwk.RSAKey.import_key(open("priv.jwk").read()), "JWT_PRIVATE_KEY")
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## validate_mandatory_claims
|
|
205
|
+
|
|
206
|
+
Ensures `iss` and `aud` are present; if settings are not used, include them in your input claims.
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
from ninja_aio.auth import validate_mandatory_claims
|
|
210
|
+
|
|
211
|
+
claims = {"sub": "123", "iss": "https://auth.example", "aud": "my-api"}
|
|
212
|
+
claims = validate_mandatory_claims(claims)
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Error handling
|
|
216
|
+
|
|
217
|
+
- `authenticate` returns `False` on decode (`ValueError`) or claim validation failure (`JoseError`). Map this to 401/403 in your views as needed.
|
|
218
|
+
- `validate_claims` raises `jose.errors.JoseError` for invalid claims.
|
|
219
|
+
- `encode_jwt` and `decode_jwt` raise `ValueError` for missing/invalid keys or configuration.
|
|
220
|
+
|
|
221
|
+
## Security notes
|
|
222
|
+
|
|
223
|
+
- Rotate keys and use `kid` headers to support key rotation.
|
|
224
|
+
- Validate critical claims (`exp`, `nbf`, `iss`, `aud`) via the registry.
|
|
225
|
+
- Do not log raw tokens or sensitive claims.
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# ViewSet Mixins
|
|
2
|
+
|
|
3
|
+
These mixins implement a query_params_handler to apply common filtering patterns to Django QuerySets. Import from `ninja_aio.views.mixins`. Values used for filtering come from validated query params in your viewset’s `query_params`.
|
|
4
|
+
|
|
5
|
+
Note: Each mixin overrides `query_params_handler`. When composing multiple mixins, define your own `query_params_handler` and call `super()` in the desired order.
|
|
6
|
+
|
|
7
|
+
## IcontainsFilterViewSetMixin
|
|
8
|
+
|
|
9
|
+
Applies case-insensitive substring filters (`__icontains`) for string values.
|
|
10
|
+
|
|
11
|
+
- Behavior: For each `str` value in `filters`, applies `field__icontains=value`.
|
|
12
|
+
- Ignores non-string values.
|
|
13
|
+
|
|
14
|
+
Example:
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from ninja_aio.views.mixins import IcontainsFilterViewSetMixin
|
|
18
|
+
from ninja_aio.views.api import APIViewSet
|
|
19
|
+
|
|
20
|
+
class UserViewSet(IcontainsFilterViewSetMixin, APIViewSet):
|
|
21
|
+
model = models.User
|
|
22
|
+
api = api
|
|
23
|
+
query_params = {"name": (str, ""), "email": (str, "")}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## BooleanFilterViewSetMixin
|
|
27
|
+
|
|
28
|
+
Filters boolean fields using exact match.
|
|
29
|
+
|
|
30
|
+
- Behavior: Applies `{key: value}` only for `bool` values.
|
|
31
|
+
|
|
32
|
+
Example:
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from ninja_aio.views.mixins import BooleanFilterViewSetMixin
|
|
36
|
+
|
|
37
|
+
class FeatureViewSet(BooleanFilterViewSetMixin, APIViewSet):
|
|
38
|
+
model = models.FeatureFlag
|
|
39
|
+
api = api
|
|
40
|
+
query_params = {"enabled": (bool, False)}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
<!-- Removed ReverseBooleanFilterViewSetMixin: not implemented in code -->
|
|
44
|
+
|
|
45
|
+
## NumericFilterViewSetMixin
|
|
46
|
+
|
|
47
|
+
Applies exact filters for numeric values.
|
|
48
|
+
|
|
49
|
+
- Behavior: Filters only `int` and `float` values.
|
|
50
|
+
|
|
51
|
+
Example:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from ninja_aio.views.mixins import NumericFilterViewSetMixin
|
|
55
|
+
|
|
56
|
+
class OrderViewSet(NumericFilterViewSetMixin, APIViewSet):
|
|
57
|
+
model = models.Order
|
|
58
|
+
api = api
|
|
59
|
+
query_params = {"amount": (float, 0.0), "quantity": (int, 0)}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## DateFilterViewSetMixin
|
|
63
|
+
|
|
64
|
+
Base mixin for date/datetime filtering with custom comparisons.
|
|
65
|
+
|
|
66
|
+
- Attributes:
|
|
67
|
+
- `_compare_attr`: comparison operator suffix (e.g., `__gt`, `__lt`, `__gte`, `__lte`).
|
|
68
|
+
- Behavior: Applies filters for values that implement `isoformat` (date/datetime-like). Prefer using Pydantic `date`/`datetime` types in `query_params`.
|
|
69
|
+
|
|
70
|
+
Example:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from ninja_aio.views.mixins import DateFilterViewSetMixin
|
|
74
|
+
|
|
75
|
+
class EventViewSet(DateFilterViewSetMixin, APIViewSet):
|
|
76
|
+
model = models.Event
|
|
77
|
+
api = api
|
|
78
|
+
# Use date/datetime types so values have `isoformat`.
|
|
79
|
+
query_params = {"created_at": (datetime, None)}
|
|
80
|
+
_compare_attr = "__gt"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## GreaterDateFilterViewSetMixin
|
|
84
|
+
|
|
85
|
+
Sets comparison to strict greater-than (`__gt`).
|
|
86
|
+
|
|
87
|
+
Example:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from ninja_aio.views.mixins import GreaterDateFilterViewSetMixin
|
|
91
|
+
|
|
92
|
+
class EventViewSet(GreaterDateFilterViewSetMixin, APIViewSet):
|
|
93
|
+
model = models.Event
|
|
94
|
+
api = api
|
|
95
|
+
query_params = {"created_at": (datetime, None)}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## LessDateFilterViewSetMixin
|
|
99
|
+
|
|
100
|
+
Sets comparison to strict less-than (`__lt`).
|
|
101
|
+
|
|
102
|
+
Example:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from ninja_aio.views.mixins import LessDateFilterViewSetMixin
|
|
106
|
+
|
|
107
|
+
class EventViewSet(LessDateFilterViewSetMixin, APIViewSet):
|
|
108
|
+
model = models.Event
|
|
109
|
+
api = api
|
|
110
|
+
query_params = {"created_at": (datetime, None)}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## GreaterEqualDateFilterViewSetMixin
|
|
114
|
+
|
|
115
|
+
Sets comparison to greater-than-or-equal (`__gte`).
|
|
116
|
+
|
|
117
|
+
Example:
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from ninja_aio.views.mixins import GreaterEqualDateFilterViewSetMixin
|
|
121
|
+
|
|
122
|
+
class EventViewSet(GreaterEqualDateFilterViewSetMixin, APIViewSet):
|
|
123
|
+
model = models.Event
|
|
124
|
+
api = api
|
|
125
|
+
query_params = {"created_at": (datetime, None)}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## LessEqualDateFilterViewSetMixin
|
|
129
|
+
|
|
130
|
+
Sets comparison to less-than-or-equal (`__lte`).
|
|
131
|
+
|
|
132
|
+
Example:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from ninja_aio.views.mixins import LessEqualDateFilterViewSetMixin
|
|
136
|
+
|
|
137
|
+
class EventViewSet(LessEqualDateFilterViewSetMixin, APIViewSet):
|
|
138
|
+
model = models.Event
|
|
139
|
+
api = api
|
|
140
|
+
query_params = {"created_at": (datetime, None)}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Tips
|
|
144
|
+
|
|
145
|
+
- Align `query_params` types with expected filter values; prefer Pydantic `date`/`datetime` for date filters so values implement `isoformat`.
|
|
146
|
+
- Validate field names and lookups to avoid runtime errors.
|
|
147
|
+
- For multiple mixins, implement your own `async def query_params_handler(...)` and chain with `await super().query_params_handler(...)` to combine behaviors.
|
|
@@ -19,11 +19,15 @@ nav:
|
|
|
19
19
|
- Views:
|
|
20
20
|
- APIView: api/views/api_view.md
|
|
21
21
|
- APIViewSet: api/views/api_view_set.md
|
|
22
|
+
- Decorators: api/views/decorators.md
|
|
23
|
+
- Mixins: docs/mixins.md
|
|
22
24
|
- Models:
|
|
23
25
|
- ModelSerializer: api/models/model_serializer.md
|
|
24
26
|
- ModelUtil: api/models/model_util.md
|
|
25
27
|
- Authentication: api/authentication.md
|
|
26
28
|
- Pagination: api/pagination.md
|
|
29
|
+
- Authentication:
|
|
30
|
+
- JWT & AsyncJwtBearer: auth.md
|
|
27
31
|
- Contributing: contributing.md
|
|
28
32
|
- Release Notes: release_notes.md
|
|
29
33
|
|
|
@@ -94,6 +98,7 @@ extra:
|
|
|
94
98
|
provider: mike
|
|
95
99
|
repo: caspel26/django-ninja-aio-crud
|
|
96
100
|
stable: true
|
|
101
|
+
default: latest
|
|
97
102
|
|
|
98
103
|
plugins:
|
|
99
104
|
- macros:
|