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.
Files changed (101) hide show
  1. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/.github/workflows/coverage.yml +1 -1
  2. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/.github/workflows/docs.yml +49 -18
  3. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/PKG-INFO +1 -1
  4. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/api/views/api_view_set.md +51 -0
  5. django_ninja_aio_crud-2.1.0/docs/auth.md +225 -0
  6. django_ninja_aio_crud-2.1.0/docs/mixins.md +147 -0
  7. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/mkdocs.yml +5 -0
  8. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/ninja_aio/__init__.py +1 -1
  9. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/ninja_aio/auth.py +121 -3
  10. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/ninja_aio/helpers/api.py +5 -3
  11. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/ninja_aio/types.py +3 -1
  12. django_ninja_aio_crud-2.1.0/ninja_aio/views/__init__.py +3 -0
  13. 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
  14. django_ninja_aio_crud-2.1.0/ninja_aio/views/mixins.py +275 -0
  15. django_ninja_aio_crud-2.1.0/tests/__init__.py +1 -0
  16. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/tests/test_app/models.py +3 -1
  17. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/tests/test_app/views.py +53 -1
  18. django_ninja_aio_crud-2.1.0/tests/test_auth.py +124 -0
  19. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/tests/views/test_viewset.py +177 -0
  20. django_ninja_aio_crud-2.0.0rc7/tests/views/__init__.py +0 -0
  21. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/.github/dependabot.yml +0 -0
  22. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/.github/workflows/publish.yml +0 -0
  23. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/.gitignore +0 -0
  24. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/.pre-commit-config.yaml +0 -0
  25. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/LICENSE +0 -0
  26. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/README.md +0 -0
  27. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/CNAME +0 -0
  28. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/api/authentication.md +0 -0
  29. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/api/models/model_serializer.md +0 -0
  30. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/api/models/model_util.md +0 -0
  31. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/api/pagination.md +0 -0
  32. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/api/renderers/orjson_renderer.md +0 -0
  33. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/api/views/api_view.md +0 -0
  34. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/api/views/decorators.md +0 -0
  35. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/contributing.md +0 -0
  36. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/extra.css +0 -0
  37. {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
  38. {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
  39. {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
  40. {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
  41. {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
  42. {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
  43. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/getting_started/installation.md +0 -0
  44. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/getting_started/quick_start.md +0 -0
  45. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/images/bar-swagger.png +0 -0
  46. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/images/favicon.ico +0 -0
  47. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/images/foo-swagger.png +0 -0
  48. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/images/logo.png +0 -0
  49. {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
  50. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/index.md +0 -0
  51. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/release_notes.md +0 -0
  52. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/requirements.txt +0 -0
  53. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/tutorial/authentication.md +0 -0
  54. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/tutorial/crud.md +0 -0
  55. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/tutorial/filtering.md +0 -0
  56. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/docs/tutorial/model.md +0 -0
  57. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/examples/ex_1/models.py +0 -0
  58. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/examples/ex_1/urls.py +0 -0
  59. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/examples/ex_1/views.py +0 -0
  60. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/examples/ex_2/auth.py +0 -0
  61. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/examples/ex_2/models.py +0 -0
  62. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/examples/ex_2/urls.py +0 -0
  63. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/examples/ex_2/views.py +0 -0
  64. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/main.py +0 -0
  65. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/ninja_aio/api.py +0 -0
  66. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/ninja_aio/decorators.py +0 -0
  67. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/ninja_aio/exceptions.py +0 -0
  68. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/ninja_aio/helpers/__init__.py +0 -0
  69. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/ninja_aio/helpers/query.py +0 -0
  70. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/ninja_aio/models.py +0 -0
  71. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/ninja_aio/parsers.py +0 -0
  72. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/ninja_aio/renders.py +0 -0
  73. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/ninja_aio/schemas/__init__.py +0 -0
  74. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/ninja_aio/schemas/api.py +0 -0
  75. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/ninja_aio/schemas/generics.py +0 -0
  76. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/ninja_aio/schemas/helpers.py +0 -0
  77. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/pyproject.toml +0 -0
  78. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/requirements.dev.txt +0 -0
  79. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/run-local-coverage.sh +0 -0
  80. {django_ninja_aio_crud-2.0.0rc7/tests → django_ninja_aio_crud-2.1.0/tests/core}/__init__.py +0 -0
  81. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/tests/core/test_decorators.py +0 -0
  82. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/tests/core/test_exceptions_api.py +0 -0
  83. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/tests/core/test_renderer_parser.py +0 -0
  84. {django_ninja_aio_crud-2.0.0rc7/tests/core → django_ninja_aio_crud-2.1.0/tests/generics}/__init__.py +0 -0
  85. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/tests/generics/literals.py +0 -0
  86. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/tests/generics/models.py +0 -0
  87. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/tests/generics/request.py +0 -0
  88. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/tests/generics/views.py +0 -0
  89. {django_ninja_aio_crud-2.0.0rc7/tests/generics → django_ninja_aio_crud-2.1.0/tests/helpers}/__init__.py +0 -0
  90. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/tests/helpers/test_many_to_many_api.py +0 -0
  91. {django_ninja_aio_crud-2.0.0rc7/tests/helpers → django_ninja_aio_crud-2.1.0/tests/models}/__init__.py +0 -0
  92. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/tests/models/test_model_util.py +0 -0
  93. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/tests/models/test_models_extra.py +0 -0
  94. {django_ninja_aio_crud-2.0.0rc7/tests/models → django_ninja_aio_crud-2.1.0/tests/test_app}/__init__.py +0 -0
  95. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/tests/test_app/schema.py +0 -0
  96. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/tests/test_decorators.py +0 -0
  97. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/tests/test_exceptions.py +0 -0
  98. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/tests/test_query_util.py +0 -0
  99. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/tests/test_settings.py +0 -0
  100. {django_ninja_aio_crud-2.0.0rc7/tests/test_app → django_ninja_aio_crud-2.1.0/tests/views}/__init__.py +0 -0
  101. {django_ninja_aio_crud-2.0.0rc7 → django_ninja_aio_crud-2.1.0}/tests/views/test_views.py +0 -0
@@ -27,6 +27,6 @@ jobs:
27
27
  coverage xml
28
28
 
29
29
  - name: Coverage
30
- uses: codecov/codecov-action@v5.5.1
30
+ uses: codecov/codecov-action@v5.5.2
31
31
  with:
32
32
  token: ${{ secrets.CODECOV_TOKEN }}
@@ -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 # ← SonarQube compliant: predefined safe options
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: true
19
+ default: false
20
20
 
21
21
  delete_version:
22
- description: 'Version to DELETE (leave empty to skip)'
23
- required: false
24
- default: ''
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 "🚀 Deploying $VERSION as latest and default"
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 "📦 Deploying $VERSION (non-latest)"
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
- echo "Deleting version: ${{ inputs.delete_version }}"
106
- mike delete "${{ inputs.delete_version }}" --push
107
- echo "Version ${{ inputs.delete_version }} deleted successfully"
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-ninja-aio-crud
3
- Version: 2.0.0rc7
3
+ Version: 2.1.0
4
4
  Summary: Django Ninja AIO CRUD - Rest Framework
5
5
  Author: Giuseppe Casillo
6
6
  Requires-Python: >=3.10
@@ -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:
@@ -1,6 +1,6 @@
1
1
  """Django Ninja AIO CRUD - Rest Framework"""
2
2
 
3
- __version__ = "2.0.0-rc7"
3
+ __version__ = "2.1.0"
4
4
 
5
5
  from .api import NinjaAIO
6
6