mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
[FR] Add Tokens to PUI (#7693)
* [FR] Add Tokens to PUI Fixes #6500 * fix name / call pattern * cleanup * add "revoke" action * Lock currently used token * Update serializers.py * bump api version
This commit is contained in:
parent
69298c365a
commit
d46ed4af6f
@ -1,12 +1,15 @@
|
|||||||
"""InvenTree API version information."""
|
"""InvenTree API version information."""
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 226
|
INVENTREE_API_VERSION = 227
|
||||||
|
|
||||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||||
|
|
||||||
|
|
||||||
INVENTREE_API_TEXT = """
|
INVENTREE_API_TEXT = """
|
||||||
|
v227 - 2024-07-19 : https://github.com/inventree/InvenTree/pull/7693/
|
||||||
|
- Adds endpoints to list and revoke the tokens issued to the current user
|
||||||
|
|
||||||
v226 - 2024-07-15 : https://github.com/inventree/InvenTree/pull/7648
|
v226 - 2024-07-15 : https://github.com/inventree/InvenTree/pull/7648
|
||||||
- Adds barcode generation API endpoint
|
- Adds barcode generation API endpoint
|
||||||
|
|
||||||
|
@ -18,6 +18,8 @@ from drf_spectacular.utils import OpenApiResponse, extend_schema, extend_schema_
|
|||||||
from rest_framework import exceptions, permissions
|
from rest_framework import exceptions, permissions
|
||||||
from rest_framework.authentication import BasicAuthentication
|
from rest_framework.authentication import BasicAuthentication
|
||||||
from rest_framework.decorators import authentication_classes
|
from rest_framework.decorators import authentication_classes
|
||||||
|
from rest_framework.generics import DestroyAPIView
|
||||||
|
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
@ -34,7 +36,12 @@ from InvenTree.mixins import (
|
|||||||
from InvenTree.serializers import ExendedUserSerializer, UserCreateSerializer
|
from InvenTree.serializers import ExendedUserSerializer, UserCreateSerializer
|
||||||
from InvenTree.settings import FRONTEND_URL_BASE
|
from InvenTree.settings import FRONTEND_URL_BASE
|
||||||
from users.models import ApiToken, Owner
|
from users.models import ApiToken, Owner
|
||||||
from users.serializers import GroupSerializer, OwnerSerializer, RoleSerializer
|
from users.serializers import (
|
||||||
|
ApiTokenSerializer,
|
||||||
|
GroupSerializer,
|
||||||
|
OwnerSerializer,
|
||||||
|
RoleSerializer,
|
||||||
|
)
|
||||||
|
|
||||||
logger = logging.getLogger('inventree')
|
logger = logging.getLogger('inventree')
|
||||||
|
|
||||||
@ -342,6 +349,22 @@ class GetAuthToken(APIView):
|
|||||||
raise exceptions.NotAuthenticated()
|
raise exceptions.NotAuthenticated()
|
||||||
|
|
||||||
|
|
||||||
|
class TokenListView(DestroyAPIView, ListAPI):
|
||||||
|
"""List of registered tokens for current users."""
|
||||||
|
|
||||||
|
permission_classes = (IsAuthenticated,)
|
||||||
|
serializer_class = ApiTokenSerializer
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
"""Only return data for current user."""
|
||||||
|
return ApiToken.objects.filter(user=self.request.user)
|
||||||
|
|
||||||
|
def perform_destroy(self, instance):
|
||||||
|
"""Revoke token."""
|
||||||
|
instance.revoked = True
|
||||||
|
instance.save()
|
||||||
|
|
||||||
|
|
||||||
class LoginRedirect(RedirectView):
|
class LoginRedirect(RedirectView):
|
||||||
"""Redirect to the correct starting page after backend login."""
|
"""Redirect to the correct starting page after backend login."""
|
||||||
|
|
||||||
@ -356,6 +379,13 @@ class LoginRedirect(RedirectView):
|
|||||||
user_urls = [
|
user_urls = [
|
||||||
path('roles/', RoleDetails.as_view(), name='api-user-roles'),
|
path('roles/', RoleDetails.as_view(), name='api-user-roles'),
|
||||||
path('token/', GetAuthToken.as_view(), name='api-token'),
|
path('token/', GetAuthToken.as_view(), name='api-token'),
|
||||||
|
path(
|
||||||
|
'tokens/',
|
||||||
|
include([
|
||||||
|
path('<int:pk>/', TokenListView.as_view(), name='api-token-detail'),
|
||||||
|
path('', TokenListView.as_view(), name='api-token-list'),
|
||||||
|
]),
|
||||||
|
),
|
||||||
path('me/', MeUserDetail.as_view(), name='api-user-me'),
|
path('me/', MeUserDetail.as_view(), name='api-user-me'),
|
||||||
path(
|
path(
|
||||||
'owner/',
|
'owner/',
|
||||||
|
@ -8,7 +8,7 @@ from rest_framework import serializers
|
|||||||
|
|
||||||
from InvenTree.serializers import InvenTreeModelSerializer
|
from InvenTree.serializers import InvenTreeModelSerializer
|
||||||
|
|
||||||
from .models import Owner, RuleSet, check_user_role
|
from .models import ApiToken, Owner, RuleSet, check_user_role
|
||||||
|
|
||||||
|
|
||||||
class OwnerSerializer(InvenTreeModelSerializer):
|
class OwnerSerializer(InvenTreeModelSerializer):
|
||||||
@ -116,5 +116,35 @@ def generate_permission_dict(permissions):
|
|||||||
perms[model] = []
|
perms[model] = []
|
||||||
|
|
||||||
perms[model].append(perm)
|
perms[model].append(perm)
|
||||||
|
|
||||||
return perms
|
return perms
|
||||||
|
|
||||||
|
|
||||||
|
class ApiTokenSerializer(InvenTreeModelSerializer):
|
||||||
|
"""Serializer for the ApiToken model."""
|
||||||
|
|
||||||
|
in_use = serializers.SerializerMethodField(read_only=True)
|
||||||
|
|
||||||
|
def get_in_use(self, token: ApiToken) -> bool:
|
||||||
|
"""Return True if the token is currently used to call the endpoint."""
|
||||||
|
from InvenTree.middleware import get_token_from_request
|
||||||
|
|
||||||
|
request = self.context.get('request')
|
||||||
|
rq_token = get_token_from_request(request)
|
||||||
|
return token.key == rq_token
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Meta options for ApiTokenSerializer."""
|
||||||
|
|
||||||
|
model = ApiToken
|
||||||
|
fields = [
|
||||||
|
'created',
|
||||||
|
'expiry',
|
||||||
|
'id',
|
||||||
|
'last_seen',
|
||||||
|
'name',
|
||||||
|
'token',
|
||||||
|
'active',
|
||||||
|
'revoked',
|
||||||
|
'user',
|
||||||
|
'in_use',
|
||||||
|
]
|
||||||
|
@ -14,6 +14,7 @@ export enum ApiEndpoints {
|
|||||||
user_me = 'user/me/',
|
user_me = 'user/me/',
|
||||||
user_roles = 'user/roles/',
|
user_roles = 'user/roles/',
|
||||||
user_token = 'user/token/',
|
user_token = 'user/token/',
|
||||||
|
user_tokens = 'user/tokens/',
|
||||||
user_simple_login = 'email/generate/',
|
user_simple_login = 'email/generate/',
|
||||||
user_reset = 'auth/password/reset/',
|
user_reset = 'auth/password/reset/',
|
||||||
user_reset_set = 'auth/password/reset/confirm/',
|
user_reset_set = 'auth/password/reset/confirm/',
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
Loader,
|
Loader,
|
||||||
Radio,
|
Radio,
|
||||||
Stack,
|
Stack,
|
||||||
|
Table,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
Title,
|
||||||
@ -15,9 +16,10 @@ import {
|
|||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { IconAlertCircle, IconAt } from '@tabler/icons-react';
|
import { IconAlertCircle, IconAt } from '@tabler/icons-react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { api, queryClient } from '../../../../App';
|
import { api, queryClient } from '../../../../App';
|
||||||
|
import { YesNoButton } from '../../../../components/buttons/YesNoButton';
|
||||||
import { PlaceholderPill } from '../../../../components/items/Placeholder';
|
import { PlaceholderPill } from '../../../../components/items/Placeholder';
|
||||||
import { ApiEndpoints } from '../../../../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../../../../enums/ApiEndpoints';
|
||||||
import { apiUrl } from '../../../../states/ApiState';
|
import { apiUrl } from '../../../../states/ApiState';
|
||||||
@ -85,6 +87,11 @@ export function SecurityContent() {
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<Title order={5}>
|
||||||
|
<Trans>Token</Trans>
|
||||||
|
</Title>
|
||||||
|
<TokenContent />
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -329,3 +336,85 @@ function MfaContent() {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function TokenContent() {
|
||||||
|
const { isLoading, data, refetch } = useQuery({
|
||||||
|
queryKey: ['token-list'],
|
||||||
|
queryFn: () =>
|
||||||
|
api.get(apiUrl(ApiEndpoints.user_tokens)).then((res) => res.data)
|
||||||
|
});
|
||||||
|
|
||||||
|
function revokeToken(id: string) {
|
||||||
|
api
|
||||||
|
.delete(apiUrl(ApiEndpoints.user_tokens, id))
|
||||||
|
.then(() => {
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch((res) => console.log(res.data));
|
||||||
|
}
|
||||||
|
const rows = useMemo(() => {
|
||||||
|
if (isLoading || data === undefined) return null;
|
||||||
|
return data.map((token: any) => (
|
||||||
|
<Table.Tr key={token.id}>
|
||||||
|
<Table.Td>
|
||||||
|
<YesNoButton value={token.active} />
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>{token.expiry}</Table.Td>
|
||||||
|
<Table.Td>{token.last_seen}</Table.Td>
|
||||||
|
<Table.Td>{token.token}</Table.Td>
|
||||||
|
<Table.Td>{token.name}</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
{token.in_use ? (
|
||||||
|
<Trans>Token is used - no actions</Trans>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
onClick={() => revokeToken(token.id)}
|
||||||
|
color="red"
|
||||||
|
disabled={!token.active}
|
||||||
|
>
|
||||||
|
<Trans>Revoke</Trans>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
));
|
||||||
|
}, [data, isLoading]);
|
||||||
|
|
||||||
|
/* renderer */
|
||||||
|
if (isLoading) return <Loader />;
|
||||||
|
|
||||||
|
if (data.length == 0)
|
||||||
|
return (
|
||||||
|
<Alert icon={<IconAlertCircle size="1rem" />} color="green">
|
||||||
|
<Trans>No tokens configured</Trans>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table stickyHeader striped highlightOnHover withTableBorder>
|
||||||
|
<Table.Thead>
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Th>
|
||||||
|
<Trans>Active</Trans>
|
||||||
|
</Table.Th>
|
||||||
|
<Table.Th>
|
||||||
|
<Trans>Expiry</Trans>
|
||||||
|
</Table.Th>
|
||||||
|
<Table.Th>
|
||||||
|
<Trans>Last Seen</Trans>
|
||||||
|
</Table.Th>
|
||||||
|
<Table.Th>
|
||||||
|
<Trans>Token</Trans>
|
||||||
|
</Table.Th>
|
||||||
|
<Table.Th>
|
||||||
|
<Trans>Name</Trans>
|
||||||
|
</Table.Th>
|
||||||
|
<Table.Th>
|
||||||
|
<Trans>Actions</Trans>
|
||||||
|
</Table.Th>
|
||||||
|
</Table.Tr>
|
||||||
|
</Table.Thead>
|
||||||
|
<Table.Tbody>{rows}</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user