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:
@ -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
|
||||
|
@ -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'),
|
||||
]),
|
||||
),
|
||||
|
@ -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."""
|
||||
|
Reference in New Issue
Block a user