mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-28 11:36:44 +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:
parent
5a5f16fd47
commit
f8de4e29a1
@ -1,13 +1,17 @@
|
|||||||
"""InvenTree API version information."""
|
"""InvenTree API version information."""
|
||||||
|
|
||||||
# InvenTree API version
|
# 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."""
|
"""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 = """
|
||||||
|
|
||||||
|
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
|
v324 - 2025-03-17 : https://github.com/inventree/InvenTree/pull/9320
|
||||||
- Adds BulkUpdate support for the SalesOrderAllocation model
|
- Adds BulkUpdate support for the SalesOrderAllocation model
|
||||||
- Adds BulkUpdate support for the PartCategory 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
|
from django.views.generic.base import RedirectView
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
|
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
|
||||||
from rest_framework import exceptions, permissions
|
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.permissions import IsAuthenticated
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.views import APIView
|
|
||||||
|
|
||||||
import InvenTree.helpers
|
import InvenTree.helpers
|
||||||
import InvenTree.permissions
|
import InvenTree.permissions
|
||||||
@ -30,6 +30,7 @@ from users.models import ApiToken, Owner, UserProfile
|
|||||||
from users.serializers import (
|
from users.serializers import (
|
||||||
ApiTokenSerializer,
|
ApiTokenSerializer,
|
||||||
ExtendedUserSerializer,
|
ExtendedUserSerializer,
|
||||||
|
GetAuthTokenSerializer,
|
||||||
GroupSerializer,
|
GroupSerializer,
|
||||||
MeUserSerializer,
|
MeUserSerializer,
|
||||||
OwnerSerializer,
|
OwnerSerializer,
|
||||||
@ -214,12 +215,23 @@ class GroupList(GroupMixin, ListCreateAPI):
|
|||||||
ordering_fields = ['name']
|
ordering_fields = ['name']
|
||||||
|
|
||||||
|
|
||||||
class GetAuthToken(APIView):
|
class GetAuthToken(GenericAPIView):
|
||||||
"""Return authentication token for an authenticated user."""
|
"""Return authentication token for an authenticated user."""
|
||||||
|
|
||||||
permission_classes = [permissions.IsAuthenticated]
|
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):
|
def get(self, request, *args, **kwargs):
|
||||||
"""Return an API token if the user is authenticated.
|
"""Return an API token if the user is authenticated.
|
||||||
|
|
||||||
@ -272,16 +284,68 @@ class GetAuthToken(APIView):
|
|||||||
raise exceptions.NotAuthenticated() # pragma: no cover
|
raise exceptions.NotAuthenticated() # pragma: no cover
|
||||||
|
|
||||||
|
|
||||||
class TokenListView(DestroyAPIView, ListAPI):
|
class TokenMixin:
|
||||||
"""List of registered tokens for current users."""
|
"""Mixin for API token endpoints."""
|
||||||
|
|
||||||
permission_classes = (IsAuthenticated,)
|
permission_classes = (IsAuthenticated,)
|
||||||
serializer_class = ApiTokenSerializer
|
serializer_class = ApiTokenSerializer
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
"""Only return data for current user."""
|
"""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)
|
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):
|
def perform_destroy(self, instance):
|
||||||
"""Revoke token."""
|
"""Revoke token."""
|
||||||
instance.revoked = True
|
instance.revoked = True
|
||||||
@ -314,7 +378,7 @@ user_urls = [
|
|||||||
path(
|
path(
|
||||||
'tokens/',
|
'tokens/',
|
||||||
include([
|
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'),
|
path('', TokenListView.as_view(), name='api-token-list'),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
|
@ -127,6 +127,9 @@ class ApiTokenSerializer(InvenTreeModelSerializer):
|
|||||||
"""Serializer for the ApiToken model."""
|
"""Serializer for the ApiToken model."""
|
||||||
|
|
||||||
in_use = serializers.SerializerMethodField(read_only=True)
|
in_use = serializers.SerializerMethodField(read_only=True)
|
||||||
|
user = serializers.PrimaryKeyRelatedField(
|
||||||
|
queryset=User.objects.all(), required=False
|
||||||
|
)
|
||||||
|
|
||||||
def get_in_use(self, token: ApiToken) -> bool:
|
def get_in_use(self, token: ApiToken) -> bool:
|
||||||
"""Return True if the token is currently used to call the endpoint."""
|
"""Return True if the token is currently used to call the endpoint."""
|
||||||
@ -153,6 +156,26 @@ class ApiTokenSerializer(InvenTreeModelSerializer):
|
|||||||
'in_use',
|
'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):
|
class BriefUserProfileSerializer(InvenTreeModelSerializer):
|
||||||
"""Brief serializer for the UserProfile model."""
|
"""Brief serializer for the UserProfile model."""
|
||||||
|
@ -23,23 +23,18 @@ import { hideNotification, showNotification } from '@mantine/notifications';
|
|||||||
import {
|
import {
|
||||||
IconAlertCircle,
|
IconAlertCircle,
|
||||||
IconAt,
|
IconAt,
|
||||||
IconCircleX,
|
|
||||||
IconExclamationCircle,
|
IconExclamationCircle,
|
||||||
IconX
|
IconX
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { api } from '../../../../App';
|
import { api } from '../../../../App';
|
||||||
import { StylishText } from '../../../../components/items/StylishText';
|
import { StylishText } from '../../../../components/items/StylishText';
|
||||||
import { ApiEndpoints } from '../../../../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../../../../enums/ApiEndpoints';
|
||||||
import { ProviderLogin, authApi } from '../../../../functions/auth';
|
import { ProviderLogin, authApi } from '../../../../functions/auth';
|
||||||
import { showApiErrorMessage } from '../../../../functions/notifications';
|
|
||||||
import { useTable } from '../../../../hooks/UseTable';
|
|
||||||
import { apiUrl, useServerApiState } from '../../../../states/ApiState';
|
import { apiUrl, useServerApiState } from '../../../../states/ApiState';
|
||||||
import type { AuthConfig, Provider } from '../../../../states/states';
|
import type { AuthConfig, Provider } from '../../../../states/states';
|
||||||
import { BooleanColumn } from '../../../../tables/ColumnRenderers';
|
import { ApiTokenTable } from '../../../../tables/settings/ApiTokenTable';
|
||||||
import { InvenTreeTable } from '../../../../tables/InvenTreeTable';
|
|
||||||
import type { RowAction } from '../../../../tables/RowActions';
|
|
||||||
import { QrRegistrationForm } from './QrRegistrationForm';
|
import { QrRegistrationForm } from './QrRegistrationForm';
|
||||||
import { useReauth } from './useConfirm';
|
import { useReauth } from './useConfirm';
|
||||||
|
|
||||||
@ -91,7 +86,7 @@ export function SecurityContent() {
|
|||||||
<StylishText size='lg'>{t`Access Tokens`}</StylishText>
|
<StylishText size='lg'>{t`Access Tokens`}</StylishText>
|
||||||
</Accordion.Control>
|
</Accordion.Control>
|
||||||
<Accordion.Panel>
|
<Accordion.Panel>
|
||||||
<TokenSection />
|
<ApiTokenTable only_myself />
|
||||||
</Accordion.Panel>
|
</Accordion.Panel>
|
||||||
</Accordion.Item>
|
</Accordion.Item>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
@ -715,68 +710,3 @@ async function runActionWithFallback(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function TokenSection() {
|
|
||||||
const table = useTable('api-tokens', 'id');
|
|
||||||
|
|
||||||
const tableColumns = useMemo(() => {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
accessor: 'name'
|
|
||||||
},
|
|
||||||
BooleanColumn({
|
|
||||||
accessor: 'active'
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
accessor: 'token'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessor: 'last_seen'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessor: 'expiry'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const rowActions = useCallback((record: any): RowAction[] => {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
title: t`Revoke`,
|
|
||||||
color: 'red',
|
|
||||||
hidden: !record.active || record.in_use,
|
|
||||||
icon: <IconCircleX />,
|
|
||||||
onClick: () => {
|
|
||||||
revokeToken(record.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const revokeToken = async (id: string) => {
|
|
||||||
api
|
|
||||||
.delete(apiUrl(ApiEndpoints.user_tokens, id))
|
|
||||||
.then(() => {
|
|
||||||
table.refreshTable();
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
showApiErrorMessage({
|
|
||||||
error: error,
|
|
||||||
title: t`Error revoking token`
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<InvenTreeTable
|
|
||||||
tableState={table}
|
|
||||||
url={apiUrl(ApiEndpoints.user_tokens)}
|
|
||||||
columns={tableColumns}
|
|
||||||
props={{
|
|
||||||
rowActions: rowActions,
|
|
||||||
enableSearch: false,
|
|
||||||
enableColumnSwitching: false
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
@ -3,44 +3,47 @@ import { Accordion } from '@mantine/core';
|
|||||||
|
|
||||||
import { StylishText } from '../../../../components/items/StylishText';
|
import { StylishText } from '../../../../components/items/StylishText';
|
||||||
import { GlobalSettingList } from '../../../../components/settings/SettingList';
|
import { GlobalSettingList } from '../../../../components/settings/SettingList';
|
||||||
|
import { ApiTokenTable } from '../../../../tables/settings/ApiTokenTable';
|
||||||
import { GroupTable } from '../../../../tables/settings/GroupTable';
|
import { GroupTable } from '../../../../tables/settings/GroupTable';
|
||||||
import { UserTable } from '../../../../tables/settings/UserTable';
|
import { UserTable } from '../../../../tables/settings/UserTable';
|
||||||
|
|
||||||
export default function UserManagementPanel() {
|
export default function UserManagementPanel() {
|
||||||
return (
|
return (
|
||||||
<>
|
<Accordion multiple defaultValue={['users']}>
|
||||||
<Accordion multiple defaultValue={['users']}>
|
<Accordion.Item value='users' key='users'>
|
||||||
<Accordion.Item value='users' key='users'>
|
<Accordion.Control>
|
||||||
<Accordion.Control>
|
<StylishText size='lg'>{t`Users`}</StylishText>
|
||||||
<StylishText size='lg'>{t`Users`}</StylishText>
|
</Accordion.Control>
|
||||||
</Accordion.Control>
|
<Accordion.Panel>
|
||||||
<Accordion.Panel>
|
<UserTable />
|
||||||
<UserTable />
|
</Accordion.Panel>
|
||||||
</Accordion.Panel>
|
</Accordion.Item>
|
||||||
</Accordion.Item>
|
<Accordion.Item value='groups' key='groups'>
|
||||||
<Accordion.Item value='groups' key='groups'>
|
<Accordion.Control>
|
||||||
<Accordion.Control>
|
<StylishText size='lg'>{t`Groups`}</StylishText>
|
||||||
<StylishText size='lg'>{t`Groups`}</StylishText>
|
</Accordion.Control>
|
||||||
</Accordion.Control>
|
<Accordion.Panel>
|
||||||
<Accordion.Panel>
|
<GroupTable />
|
||||||
<GroupTable />
|
</Accordion.Panel>
|
||||||
</Accordion.Panel>
|
</Accordion.Item>
|
||||||
</Accordion.Item>
|
<Accordion.Item value='settings' key='settings'>
|
||||||
<Accordion.Item value='settings' key='settings'>
|
<Accordion.Control>
|
||||||
<Accordion.Control>
|
<StylishText size='lg'>{t`Settings`}</StylishText>
|
||||||
<StylishText size='lg'>{t`Settings`}</StylishText>
|
</Accordion.Control>
|
||||||
</Accordion.Control>
|
<Accordion.Panel>
|
||||||
<Accordion.Panel>
|
<GlobalSettingList
|
||||||
<GlobalSettingList
|
keys={['LOGIN_ENABLE_REG', 'SIGNUP_GROUP', 'LOGIN_ENABLE_SSO_REG']}
|
||||||
keys={[
|
/>
|
||||||
'LOGIN_ENABLE_REG',
|
</Accordion.Panel>
|
||||||
'SIGNUP_GROUP',
|
</Accordion.Item>
|
||||||
'LOGIN_ENABLE_SSO_REG'
|
<Accordion.Item value='tokens' key='tokens'>
|
||||||
]}
|
<Accordion.Control>
|
||||||
/>
|
<StylishText size='lg'>{t`Tokens`}</StylishText>
|
||||||
</Accordion.Panel>
|
</Accordion.Control>
|
||||||
</Accordion.Item>
|
<Accordion.Panel>
|
||||||
</Accordion>
|
<ApiTokenTable only_myself={false} />
|
||||||
</>
|
</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
|
</Accordion>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
201
src/frontend/src/tables/settings/ApiTokenTable.tsx
Normal file
201
src/frontend/src/tables/settings/ApiTokenTable.tsx
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
import { Trans, t } from '@lingui/macro';
|
||||||
|
import { Badge, Code, Flex, Modal, Text } from '@mantine/core';
|
||||||
|
import { useDisclosure } from '@mantine/hooks';
|
||||||
|
import { IconCircleX } from '@tabler/icons-react';
|
||||||
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
|
import { api } from '../../App';
|
||||||
|
import { AddItemButton } from '../../components/buttons/AddItemButton';
|
||||||
|
import { CopyButton } from '../../components/buttons/CopyButton';
|
||||||
|
import { StylishText } from '../../components/items/StylishText';
|
||||||
|
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||||
|
import { showApiErrorMessage } from '../../functions/notifications';
|
||||||
|
import { useCreateApiFormModal } from '../../hooks/UseForm';
|
||||||
|
import { useTable } from '../../hooks/UseTable';
|
||||||
|
import { apiUrl } from '../../states/ApiState';
|
||||||
|
import type { RowAction } from '../../tables/RowActions';
|
||||||
|
import { BooleanColumn } from '../ColumnRenderers';
|
||||||
|
import { type TableFilter, UserFilter } from '../Filter';
|
||||||
|
import { InvenTreeTable } from '../InvenTreeTable';
|
||||||
|
|
||||||
|
export function ApiTokenTable({
|
||||||
|
only_myself = true
|
||||||
|
}: Readonly<{ only_myself: boolean }>) {
|
||||||
|
const [token, setToken] = useState<string>('');
|
||||||
|
const [opened, { open, close }] = useDisclosure(false);
|
||||||
|
|
||||||
|
const generateToken = useCreateApiFormModal({
|
||||||
|
url: ApiEndpoints.user_tokens,
|
||||||
|
title: t`Generate Token`,
|
||||||
|
fields: { name: {} },
|
||||||
|
successMessage: t`Token generated`,
|
||||||
|
onFormSuccess: (data: any) => {
|
||||||
|
setToken(data.token);
|
||||||
|
open();
|
||||||
|
table.refreshTable();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const tableActions = useMemo(() => {
|
||||||
|
if (only_myself)
|
||||||
|
return [
|
||||||
|
<AddItemButton
|
||||||
|
key={'generate'}
|
||||||
|
tooltip={t`Generate Token`}
|
||||||
|
onClick={() => generateToken.open()}
|
||||||
|
/>
|
||||||
|
];
|
||||||
|
return [];
|
||||||
|
}, [only_myself]);
|
||||||
|
|
||||||
|
const table = useTable('api-tokens', 'id');
|
||||||
|
|
||||||
|
const tableColumns = useMemo(() => {
|
||||||
|
const cols = [
|
||||||
|
{
|
||||||
|
accessor: 'name',
|
||||||
|
title: t`Name`,
|
||||||
|
sortable: true
|
||||||
|
},
|
||||||
|
BooleanColumn({
|
||||||
|
accessor: 'active',
|
||||||
|
title: t`Active`,
|
||||||
|
sortable: false
|
||||||
|
}),
|
||||||
|
BooleanColumn({
|
||||||
|
accessor: 'revoked',
|
||||||
|
title: t`Revoked`
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
accessor: 'token',
|
||||||
|
title: t`Token`,
|
||||||
|
render: (record: any) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{record.token}{' '}
|
||||||
|
{record.in_use ? (
|
||||||
|
<Badge color='green'>
|
||||||
|
<Trans>In Use</Trans>
|
||||||
|
</Badge>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: 'last_seen',
|
||||||
|
title: t`Last Seen`,
|
||||||
|
sortable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: 'expiry',
|
||||||
|
title: t`Expiry`,
|
||||||
|
sortable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: 'created',
|
||||||
|
title: t`Created`,
|
||||||
|
sortable: true
|
||||||
|
}
|
||||||
|
];
|
||||||
|
if (!only_myself) {
|
||||||
|
cols.push({ accessor: 'user', title: t`User`, sortable: true });
|
||||||
|
}
|
||||||
|
return cols;
|
||||||
|
}, [only_myself]);
|
||||||
|
|
||||||
|
const tableFilters: TableFilter[] = useMemo(() => {
|
||||||
|
const filters: TableFilter[] = [
|
||||||
|
{
|
||||||
|
name: 'revoked',
|
||||||
|
label: t`Revoked`,
|
||||||
|
description: t`Show revoked tokens`
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!only_myself) {
|
||||||
|
filters.push(
|
||||||
|
UserFilter({
|
||||||
|
name: 'user',
|
||||||
|
label: t`User`,
|
||||||
|
description: t`Filter by user`
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return filters;
|
||||||
|
}, [only_myself]);
|
||||||
|
|
||||||
|
const rowActions = useCallback((record: any): RowAction[] => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
title: t`Revoke`,
|
||||||
|
color: 'red',
|
||||||
|
hidden: !record.active || record.in_use,
|
||||||
|
icon: <IconCircleX />,
|
||||||
|
onClick: () => {
|
||||||
|
revokeToken(record.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const revokeToken = async (id: string) => {
|
||||||
|
let targetUrl = apiUrl(ApiEndpoints.user_tokens, id);
|
||||||
|
if (!only_myself) {
|
||||||
|
targetUrl += '?all_users=true';
|
||||||
|
}
|
||||||
|
api
|
||||||
|
.delete(targetUrl)
|
||||||
|
.then(() => {
|
||||||
|
table.refreshTable();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
showApiErrorMessage({
|
||||||
|
error: error,
|
||||||
|
title: t`Error revoking token`
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const urlParams = useMemo(() => {
|
||||||
|
if (only_myself) return {};
|
||||||
|
return { all_users: true };
|
||||||
|
}, [only_myself]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{only_myself && (
|
||||||
|
<>
|
||||||
|
{generateToken.modal}
|
||||||
|
<Modal
|
||||||
|
opened={opened}
|
||||||
|
onClose={close}
|
||||||
|
title={<StylishText size='xl'>{t`Token`}</StylishText>}
|
||||||
|
centered
|
||||||
|
>
|
||||||
|
<Text c='dimmed'>
|
||||||
|
<Trans>
|
||||||
|
Tokens are only shown once - make sure to note it down.
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
<Flex>
|
||||||
|
<Code>{token}</Code>
|
||||||
|
<CopyButton value={token} />
|
||||||
|
</Flex>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<InvenTreeTable
|
||||||
|
tableState={table}
|
||||||
|
url={apiUrl(ApiEndpoints.user_tokens)}
|
||||||
|
columns={tableColumns}
|
||||||
|
props={{
|
||||||
|
params: urlParams,
|
||||||
|
rowActions: rowActions,
|
||||||
|
enableSearch: false,
|
||||||
|
enableColumnSwitching: false,
|
||||||
|
tableActions: tableActions,
|
||||||
|
tableFilters: tableFilters
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user