2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-17 04:25:42 +00:00

feat(frontend): Add token managment tools (#9244)

* Add typing

* feat(frontend): Add token managment tools
Closes https://github.com/inventree/InvenTree/issues/9166

* remove debug msg

* split responsibilities for token endpoint

* move ApiTokenTable

* add option for superusers to show all user tokens

* Add tokens to admin users interface

* adjust api text

* adress raised issues

* make stuff sortable / filterable
This commit is contained in:
Matthias Mair
2025-03-17 21:27:19 +01:00
committed by GitHub
parent 5a5f16fd47
commit f8de4e29a1
6 changed files with 340 additions and 115 deletions

View File

@ -1,13 +1,17 @@
"""InvenTree API version information."""
# InvenTree API version
INVENTREE_API_VERSION = 324
INVENTREE_API_VERSION = 325
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
v324 - 2024-03-17 : https://github.com/inventree/InvenTree/pull/9244
- Adds the option for superusers to list all user tokens
- Make list endpoints sortable, filterable and searchable
v324 - 2025-03-17 : https://github.com/inventree/InvenTree/pull/9320
- Adds BulkUpdate support for the SalesOrderAllocation model
- Adds BulkUpdate support for the PartCategory model

View File

@ -9,11 +9,11 @@ from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.generic.base import RedirectView
import structlog
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
from rest_framework import exceptions, permissions
from rest_framework.generics import DestroyAPIView
from rest_framework.generics import DestroyAPIView, GenericAPIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
import InvenTree.helpers
import InvenTree.permissions
@ -30,6 +30,7 @@ from users.models import ApiToken, Owner, UserProfile
from users.serializers import (
ApiTokenSerializer,
ExtendedUserSerializer,
GetAuthTokenSerializer,
GroupSerializer,
MeUserSerializer,
OwnerSerializer,
@ -214,12 +215,23 @@ class GroupList(GroupMixin, ListCreateAPI):
ordering_fields = ['name']
class GetAuthToken(APIView):
class GetAuthToken(GenericAPIView):
"""Return authentication token for an authenticated user."""
permission_classes = [permissions.IsAuthenticated]
serializer_class = None
serializer_class = GetAuthTokenSerializer
@extend_schema(
parameters=[
OpenApiParameter(
name='name',
type=str,
location=OpenApiParameter.QUERY,
description='Name of the token',
)
],
responses={200: OpenApiResponse(response=GetAuthTokenSerializer())},
)
def get(self, request, *args, **kwargs):
"""Return an API token if the user is authenticated.
@ -272,16 +284,68 @@ class GetAuthToken(APIView):
raise exceptions.NotAuthenticated() # pragma: no cover
class TokenListView(DestroyAPIView, ListAPI):
"""List of registered tokens for current users."""
class TokenMixin:
"""Mixin for API token endpoints."""
permission_classes = (IsAuthenticated,)
serializer_class = ApiTokenSerializer
def get_queryset(self):
"""Only return data for current user."""
if self.request.user.is_superuser and self.request.query_params.get(
'all_users', False
):
return ApiToken.objects.all()
return ApiToken.objects.filter(user=self.request.user)
@extend_schema(
parameters=[
OpenApiParameter(
name='all_users',
type=bool,
location=OpenApiParameter.QUERY,
description='Display tokens for all users (superuser only)',
)
]
)
def get(self, request, *args, **kwargs):
"""Details for a user token."""
return super().get(request, *args, **kwargs)
class TokenListView(TokenMixin, ListCreateAPI):
"""List of user tokens for current user."""
filter_backends = SEARCH_ORDER_FILTER
search_fields = ['name', 'key']
ordering_fields = [
'created',
'expiry',
'last_seen',
'user',
'name',
'revoked',
'revoked',
]
filterset_fields = ['revoked', 'user']
def create(self, request, *args, **kwargs):
"""Create token and show key to user."""
resp = super().create(request, *args, **kwargs)
resp.data['token'] = self.serializer_class.Meta.model.objects.get(
id=resp.data['id']
).key
return resp
def get(self, request, *args, **kwargs):
"""List of user tokens for current user."""
return super().get(request, *args, **kwargs)
class TokenDetailView(TokenMixin, DestroyAPIView, RetrieveAPI):
"""Details for a user token."""
def perform_destroy(self, instance):
"""Revoke token."""
instance.revoked = True
@ -314,7 +378,7 @@ user_urls = [
path(
'tokens/',
include([
path('<int:pk>/', TokenListView.as_view(), name='api-token-detail'),
path('<int:pk>/', TokenDetailView.as_view(), name='api-token-detail'),
path('', TokenListView.as_view(), name='api-token-list'),
]),
),

View File

@ -127,6 +127,9 @@ class ApiTokenSerializer(InvenTreeModelSerializer):
"""Serializer for the ApiToken model."""
in_use = serializers.SerializerMethodField(read_only=True)
user = serializers.PrimaryKeyRelatedField(
queryset=User.objects.all(), required=False
)
def get_in_use(self, token: ApiToken) -> bool:
"""Return True if the token is currently used to call the endpoint."""
@ -153,6 +156,26 @@ class ApiTokenSerializer(InvenTreeModelSerializer):
'in_use',
]
def validate(self, data):
"""Validate the data for the serializer."""
if 'user' not in data:
data['user'] = self.context['request'].user
return super().validate(data)
class GetAuthTokenSerializer(serializers.Serializer):
"""Serializer for the GetAuthToken API endpoint."""
class Meta:
"""Meta options for GetAuthTokenSerializer."""
model = ApiToken
fields = ['token', 'name', 'expiry']
token = serializers.CharField(read_only=True)
name = serializers.CharField()
expiry = serializers.DateField(read_only=True)
class BriefUserProfileSerializer(InvenTreeModelSerializer):
"""Brief serializer for the UserProfile model."""