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:
@ -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
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
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
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user