2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-16 12:05:53 +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

@ -23,23 +23,18 @@ import { hideNotification, showNotification } from '@mantine/notifications';
import {
IconAlertCircle,
IconAt,
IconCircleX,
IconExclamationCircle,
IconX
} from '@tabler/icons-react';
import { useQuery } from '@tanstack/react-query';
import { useCallback, useMemo, useState } from 'react';
import { useMemo, useState } from 'react';
import { api } from '../../../../App';
import { StylishText } from '../../../../components/items/StylishText';
import { ApiEndpoints } from '../../../../enums/ApiEndpoints';
import { ProviderLogin, authApi } from '../../../../functions/auth';
import { showApiErrorMessage } from '../../../../functions/notifications';
import { useTable } from '../../../../hooks/UseTable';
import { apiUrl, useServerApiState } from '../../../../states/ApiState';
import type { AuthConfig, Provider } from '../../../../states/states';
import { BooleanColumn } from '../../../../tables/ColumnRenderers';
import { InvenTreeTable } from '../../../../tables/InvenTreeTable';
import type { RowAction } from '../../../../tables/RowActions';
import { ApiTokenTable } from '../../../../tables/settings/ApiTokenTable';
import { QrRegistrationForm } from './QrRegistrationForm';
import { useReauth } from './useConfirm';
@ -91,7 +86,7 @@ export function SecurityContent() {
<StylishText size='lg'>{t`Access Tokens`}</StylishText>
</Accordion.Control>
<Accordion.Panel>
<TokenSection />
<ApiTokenTable only_myself />
</Accordion.Panel>
</Accordion.Item>
</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
}}
/>
);
}

View File

@ -3,44 +3,47 @@ import { Accordion } from '@mantine/core';
import { StylishText } from '../../../../components/items/StylishText';
import { GlobalSettingList } from '../../../../components/settings/SettingList';
import { ApiTokenTable } from '../../../../tables/settings/ApiTokenTable';
import { GroupTable } from '../../../../tables/settings/GroupTable';
import { UserTable } from '../../../../tables/settings/UserTable';
export default function UserManagementPanel() {
return (
<>
<Accordion multiple defaultValue={['users']}>
<Accordion.Item value='users' key='users'>
<Accordion.Control>
<StylishText size='lg'>{t`Users`}</StylishText>
</Accordion.Control>
<Accordion.Panel>
<UserTable />
</Accordion.Panel>
</Accordion.Item>
<Accordion.Item value='groups' key='groups'>
<Accordion.Control>
<StylishText size='lg'>{t`Groups`}</StylishText>
</Accordion.Control>
<Accordion.Panel>
<GroupTable />
</Accordion.Panel>
</Accordion.Item>
<Accordion.Item value='settings' key='settings'>
<Accordion.Control>
<StylishText size='lg'>{t`Settings`}</StylishText>
</Accordion.Control>
<Accordion.Panel>
<GlobalSettingList
keys={[
'LOGIN_ENABLE_REG',
'SIGNUP_GROUP',
'LOGIN_ENABLE_SSO_REG'
]}
/>
</Accordion.Panel>
</Accordion.Item>
</Accordion>
</>
<Accordion multiple defaultValue={['users']}>
<Accordion.Item value='users' key='users'>
<Accordion.Control>
<StylishText size='lg'>{t`Users`}</StylishText>
</Accordion.Control>
<Accordion.Panel>
<UserTable />
</Accordion.Panel>
</Accordion.Item>
<Accordion.Item value='groups' key='groups'>
<Accordion.Control>
<StylishText size='lg'>{t`Groups`}</StylishText>
</Accordion.Control>
<Accordion.Panel>
<GroupTable />
</Accordion.Panel>
</Accordion.Item>
<Accordion.Item value='settings' key='settings'>
<Accordion.Control>
<StylishText size='lg'>{t`Settings`}</StylishText>
</Accordion.Control>
<Accordion.Panel>
<GlobalSettingList
keys={['LOGIN_ENABLE_REG', 'SIGNUP_GROUP', 'LOGIN_ENABLE_SSO_REG']}
/>
</Accordion.Panel>
</Accordion.Item>
<Accordion.Item value='tokens' key='tokens'>
<Accordion.Control>
<StylishText size='lg'>{t`Tokens`}</StylishText>
</Accordion.Control>
<Accordion.Panel>
<ApiTokenTable only_myself={false} />
</Accordion.Panel>
</Accordion.Item>
</Accordion>
);
}

View 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
}}
/>
</>
);
}