mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: manage SA tokens through UI (#2840)
https://linear.app/unleash/issue/2-542/manage-service-account-tokens-through-the-ui 
This commit is contained in:
		
							parent
							
								
									2b8f1ee0d7
								
							
						
					
					
						commit
						b10d9c435e
					
				| @ -31,6 +31,7 @@ import { | ||||
| } from 'component/user/Profile/PersonalAPITokensTab/CreatePersonalAPIToken/PersonalAPITokenForm/PersonalAPITokenForm'; | ||||
| import { usePersonalAPITokensApi } from 'hooks/api/actions/usePersonalAPITokensApi/usePersonalAPITokensApi'; | ||||
| import { INewPersonalAPIToken } from 'interfaces/personalAPIToken'; | ||||
| import { ServiceAccountTokens } from './ServiceAccountTokens/ServiceAccountTokens'; | ||||
| 
 | ||||
| const StyledForm = styled('form')(() => ({ | ||||
|     display: 'flex', | ||||
| @ -86,9 +87,7 @@ const StyledButtonContainer = styled('div')(({ theme }) => ({ | ||||
|     marginTop: 'auto', | ||||
|     display: 'flex', | ||||
|     justifyContent: 'flex-end', | ||||
|     [theme.breakpoints.down('sm')]: { | ||||
|         marginTop: theme.spacing(4), | ||||
|     }, | ||||
|     paddingTop: theme.spacing(4), | ||||
| })); | ||||
| 
 | ||||
| const StyledCancelButton = styled(Button)(({ theme }) => ({ | ||||
| @ -225,16 +224,18 @@ export const ServiceAccountModal = ({ | ||||
|         !serviceAccounts?.some( | ||||
|             (serviceAccount: IUser) => serviceAccount.username === value | ||||
|         ); | ||||
|     const isPATValid = | ||||
|         tokenGeneration === TokenGeneration.LATER || | ||||
|         (isNotEmpty(patDescription) && patExpiresAt > new Date()); | ||||
|     const isValid = | ||||
|         isNotEmpty(name) && | ||||
|         isNotEmpty(username) && | ||||
|         (editing || isUnique(username)) && | ||||
|         (tokenGeneration === TokenGeneration.LATER || | ||||
|             isNotEmpty(patDescription)); | ||||
|         isPATValid; | ||||
| 
 | ||||
|     const suggestUsername = () => { | ||||
|         if (isNotEmpty(name) && !isNotEmpty(username)) { | ||||
|             const normalizedFromName = `service:${name | ||||
|             const normalizedFromName = `service-${name | ||||
|                 .toLowerCase() | ||||
|                 .replace(/ /g, '-') | ||||
|                 .replace(/[^\w_-]/g, '')}`;
 | ||||
| @ -411,6 +412,16 @@ export const ServiceAccountModal = ({ | ||||
|                                     </StyledInlineContainer> | ||||
|                                 </StyledSecondaryContainer> | ||||
|                             } | ||||
|                             elseShow={ | ||||
|                                 <> | ||||
|                                     <StyledInputDescription> | ||||
|                                         Service account tokens | ||||
|                                     </StyledInputDescription> | ||||
|                                     <ServiceAccountTokens | ||||
|                                         serviceAccount={serviceAccount!} | ||||
|                                     /> | ||||
|                                 </> | ||||
|                             } | ||||
|                         /> | ||||
|                     </div> | ||||
| 
 | ||||
|  | ||||
| @ -0,0 +1,83 @@ | ||||
| import { useEffect, useState } from 'react'; | ||||
| import { Dialogue } from 'component/common/Dialogue/Dialogue'; | ||||
| import { | ||||
|     calculateExpirationDate, | ||||
|     ExpirationOption, | ||||
|     IPersonalAPITokenFormErrors, | ||||
|     PersonalAPITokenForm, | ||||
| } from 'component/user/Profile/PersonalAPITokensTab/CreatePersonalAPIToken/PersonalAPITokenForm/PersonalAPITokenForm'; | ||||
| import { ICreatePersonalApiTokenPayload } from 'hooks/api/actions/usePersonalAPITokensApi/usePersonalAPITokensApi'; | ||||
| import { IUser } from 'interfaces/user'; | ||||
| import { usePersonalAPITokens } from 'hooks/api/getters/usePersonalAPITokens/usePersonalAPITokens'; | ||||
| 
 | ||||
| const DEFAULT_EXPIRATION = ExpirationOption['30DAYS']; | ||||
| 
 | ||||
| interface IServiceAccountCreateTokenDialogProps { | ||||
|     open: boolean; | ||||
|     setOpen: React.Dispatch<React.SetStateAction<boolean>>; | ||||
|     serviceAccount: IUser; | ||||
|     onCreateClick: (newToken: ICreatePersonalApiTokenPayload) => void; | ||||
| } | ||||
| 
 | ||||
| export const ServiceAccountCreateTokenDialog = ({ | ||||
|     open, | ||||
|     setOpen, | ||||
|     serviceAccount, | ||||
|     onCreateClick, | ||||
| }: IServiceAccountCreateTokenDialogProps) => { | ||||
|     const { tokens = [] } = usePersonalAPITokens(serviceAccount.id); | ||||
| 
 | ||||
|     const [patDescription, setPatDescription] = useState(''); | ||||
|     const [patExpiration, setPatExpiration] = | ||||
|         useState<ExpirationOption>(DEFAULT_EXPIRATION); | ||||
|     const [patExpiresAt, setPatExpiresAt] = useState( | ||||
|         calculateExpirationDate(DEFAULT_EXPIRATION) | ||||
|     ); | ||||
|     const [patErrors, setPatErrors] = useState<IPersonalAPITokenFormErrors>({}); | ||||
| 
 | ||||
|     useEffect(() => { | ||||
|         setPatDescription(''); | ||||
|         setPatExpiration(DEFAULT_EXPIRATION); | ||||
|         setPatExpiresAt(calculateExpirationDate(DEFAULT_EXPIRATION)); | ||||
|         setPatErrors({}); | ||||
|     }, [open]); | ||||
| 
 | ||||
|     const isDescriptionUnique = (description: string) => | ||||
|         !tokens?.some(token => token.description === description); | ||||
| 
 | ||||
|     const isPATValid = | ||||
|         patDescription.length && | ||||
|         isDescriptionUnique(patDescription) && | ||||
|         patExpiresAt > new Date(); | ||||
| 
 | ||||
|     return ( | ||||
|         <Dialogue | ||||
|             open={open} | ||||
|             primaryButtonText="Create token" | ||||
|             secondaryButtonText="Cancel" | ||||
|             onClick={() => | ||||
|                 onCreateClick({ | ||||
|                     description: patDescription, | ||||
|                     expiresAt: patExpiresAt, | ||||
|                 }) | ||||
|             } | ||||
|             disabledPrimaryButton={!isPATValid} | ||||
|             onClose={() => { | ||||
|                 setOpen(false); | ||||
|             }} | ||||
|             title="New token" | ||||
|         > | ||||
|             <PersonalAPITokenForm | ||||
|                 description={patDescription} | ||||
|                 setDescription={setPatDescription} | ||||
|                 isDescriptionUnique={isDescriptionUnique} | ||||
|                 expiration={patExpiration} | ||||
|                 setExpiration={setPatExpiration} | ||||
|                 expiresAt={patExpiresAt} | ||||
|                 setExpiresAt={setPatExpiresAt} | ||||
|                 errors={patErrors} | ||||
|                 setErrors={setPatErrors} | ||||
|             /> | ||||
|         </Dialogue> | ||||
|     ); | ||||
| }; | ||||
| @ -0,0 +1,364 @@ | ||||
| import { Delete } from '@mui/icons-material'; | ||||
| import { | ||||
|     Button, | ||||
|     IconButton, | ||||
|     styled, | ||||
|     Tooltip, | ||||
|     Typography, | ||||
|     useMediaQuery, | ||||
|     useTheme, | ||||
| } from '@mui/material'; | ||||
| import { Search } from 'component/common/Search/Search'; | ||||
| import { TablePlaceholder, VirtualizedTable } from 'component/common/Table'; | ||||
| import { ActionCell } from 'component/common/Table/cells/ActionCell/ActionCell'; | ||||
| import { DateCell } from 'component/common/Table/cells/DateCell/DateCell'; | ||||
| import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell'; | ||||
| import { TextCell } from 'component/common/Table/cells/TextCell/TextCell'; | ||||
| import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; | ||||
| import { PAT_LIMIT } from '@server/util/constants'; | ||||
| import { usePersonalAPITokens } from 'hooks/api/getters/usePersonalAPITokens/usePersonalAPITokens'; | ||||
| import { useSearch } from 'hooks/useSearch'; | ||||
| import { | ||||
|     INewPersonalAPIToken, | ||||
|     IPersonalAPIToken, | ||||
| } from 'interfaces/personalAPIToken'; | ||||
| import { useMemo, useState } from 'react'; | ||||
| import { useTable, SortingRule, useSortBy, useFlexLayout } from 'react-table'; | ||||
| import { sortTypes } from 'utils/sortTypes'; | ||||
| import { ServiceAccountCreateTokenDialog } from './ServiceAccountCreateTokenDialog/ServiceAccountCreateTokenDialog'; | ||||
| import { ServiceAccountTokenDialog } from 'component/admin/serviceAccounts/ServiceAccountsTable/ServiceAccountTokenDialog/ServiceAccountTokenDialog'; | ||||
| import { TimeAgoCell } from 'component/common/Table/cells/TimeAgoCell/TimeAgoCell'; | ||||
| import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import { IUser } from 'interfaces/user'; | ||||
| import { Dialogue } from 'component/common/Dialogue/Dialogue'; | ||||
| import { | ||||
|     ICreatePersonalApiTokenPayload, | ||||
|     usePersonalAPITokensApi, | ||||
| } from 'hooks/api/actions/usePersonalAPITokensApi/usePersonalAPITokensApi'; | ||||
| import useToast from 'hooks/useToast'; | ||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | ||||
| 
 | ||||
| const StyledHeader = styled('div')(({ theme }) => ({ | ||||
|     display: 'flex', | ||||
|     justifyContent: 'space-between', | ||||
|     alignItems: 'center', | ||||
|     marginBottom: theme.spacing(2), | ||||
|     gap: theme.spacing(2), | ||||
|     '& > div': { | ||||
|         [theme.breakpoints.down('md')]: { | ||||
|             marginTop: 0, | ||||
|         }, | ||||
|     }, | ||||
| })); | ||||
| 
 | ||||
| const StyledTablePlaceholder = styled('div')(({ theme }) => ({ | ||||
|     display: 'flex', | ||||
|     flexDirection: 'column', | ||||
|     alignItems: 'center', | ||||
|     padding: theme.spacing(3), | ||||
| })); | ||||
| 
 | ||||
| const StyledPlaceholderTitle = styled(Typography)(({ theme }) => ({ | ||||
|     fontSize: theme.fontSizes.bodySize, | ||||
|     marginBottom: theme.spacing(0.5), | ||||
| })); | ||||
| 
 | ||||
| const StyledPlaceholderSubtitle = styled(Typography)(({ theme }) => ({ | ||||
|     fontSize: theme.fontSizes.smallBody, | ||||
|     color: theme.palette.text.secondary, | ||||
|     marginBottom: theme.spacing(1.5), | ||||
| })); | ||||
| 
 | ||||
| export const tokensPlaceholder: IPersonalAPIToken[] = Array(15).fill({ | ||||
|     description: 'Short description of the feature', | ||||
|     type: '-', | ||||
|     createdAt: new Date(2022, 1, 1), | ||||
|     project: 'projectID', | ||||
| }); | ||||
| 
 | ||||
| export type PageQueryType = Partial< | ||||
|     Record<'sort' | 'order' | 'search', string> | ||||
| >; | ||||
| 
 | ||||
| const defaultSort: SortingRule<string> = { id: 'createdAt' }; | ||||
| 
 | ||||
| interface IServiceAccountTokensProps { | ||||
|     serviceAccount: IUser; | ||||
|     readOnly?: boolean; | ||||
| } | ||||
| 
 | ||||
| export const ServiceAccountTokens = ({ | ||||
|     serviceAccount, | ||||
|     readOnly, | ||||
| }: IServiceAccountTokensProps) => { | ||||
|     const theme = useTheme(); | ||||
|     const { setToastData, setToastApiError } = useToast(); | ||||
|     const isSmallScreen = useMediaQuery(theme.breakpoints.down('md')); | ||||
|     const isExtraSmallScreen = useMediaQuery(theme.breakpoints.down('sm')); | ||||
|     const { | ||||
|         tokens = [], | ||||
|         refetchTokens, | ||||
|         loading, | ||||
|     } = usePersonalAPITokens(serviceAccount.id); | ||||
|     const { createUserPersonalAPIToken, deleteUserPersonalAPIToken } = | ||||
|         usePersonalAPITokensApi(); | ||||
| 
 | ||||
|     const [initialState] = useState(() => ({ | ||||
|         sortBy: [defaultSort], | ||||
|     })); | ||||
| 
 | ||||
|     const [searchValue, setSearchValue] = useState(''); | ||||
|     const [createOpen, setCreateOpen] = useState(false); | ||||
|     const [tokenOpen, setTokenOpen] = useState(false); | ||||
|     const [deleteOpen, setDeleteOpen] = useState(false); | ||||
|     const [newToken, setNewToken] = useState<INewPersonalAPIToken>(); | ||||
|     const [selectedToken, setSelectedToken] = useState<IPersonalAPIToken>(); | ||||
| 
 | ||||
|     const onCreateClick = async (newToken: ICreatePersonalApiTokenPayload) => { | ||||
|         try { | ||||
|             const token = await createUserPersonalAPIToken( | ||||
|                 serviceAccount.id, | ||||
|                 newToken | ||||
|             ); | ||||
|             refetchTokens(); | ||||
|             setCreateOpen(false); | ||||
|             setNewToken(token); | ||||
|             setTokenOpen(true); | ||||
|             setToastData({ | ||||
|                 title: 'Token created successfully', | ||||
|                 type: 'success', | ||||
|             }); | ||||
|         } catch (error: unknown) { | ||||
|             setToastApiError(formatUnknownError(error)); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     const onDeleteClick = async () => { | ||||
|         if (selectedToken) { | ||||
|             try { | ||||
|                 await deleteUserPersonalAPIToken( | ||||
|                     serviceAccount.id, | ||||
|                     selectedToken?.id | ||||
|                 ); | ||||
|                 refetchTokens(); | ||||
|                 setDeleteOpen(false); | ||||
|                 setToastData({ | ||||
|                     title: 'Token deleted successfully', | ||||
|                     type: 'success', | ||||
|                 }); | ||||
|             } catch (error: unknown) { | ||||
|                 setToastApiError(formatUnknownError(error)); | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     const columns = useMemo( | ||||
|         () => [ | ||||
|             { | ||||
|                 Header: 'Description', | ||||
|                 accessor: 'description', | ||||
|                 Cell: HighlightCell, | ||||
|                 minWidth: 100, | ||||
|                 searchable: true, | ||||
|             }, | ||||
|             { | ||||
|                 Header: 'Expires', | ||||
|                 accessor: 'expiresAt', | ||||
|                 Cell: ({ value }: { value: string }) => { | ||||
|                     const date = new Date(value); | ||||
|                     if (date.getFullYear() > new Date().getFullYear() + 100) { | ||||
|                         return <TextCell>Never</TextCell>; | ||||
|                     } | ||||
|                     return <DateCell value={value} />; | ||||
|                 }, | ||||
|                 sortType: 'date', | ||||
|                 maxWidth: 150, | ||||
|             }, | ||||
|             { | ||||
|                 Header: 'Created', | ||||
|                 accessor: 'createdAt', | ||||
|                 Cell: DateCell, | ||||
|                 sortType: 'date', | ||||
|                 maxWidth: 150, | ||||
|             }, | ||||
|             { | ||||
|                 Header: 'Last seen', | ||||
|                 accessor: 'seenAt', | ||||
|                 Cell: TimeAgoCell, | ||||
|                 sortType: 'date', | ||||
|                 maxWidth: 150, | ||||
|             }, | ||||
|             { | ||||
|                 Header: 'Actions', | ||||
|                 id: 'Actions', | ||||
|                 align: 'center', | ||||
|                 Cell: ({ row: { original: rowToken } }: any) => ( | ||||
|                     <ActionCell> | ||||
|                         <Tooltip title="Delete token" arrow describeChild> | ||||
|                             <span> | ||||
|                                 <IconButton | ||||
|                                     onClick={() => { | ||||
|                                         setSelectedToken(rowToken); | ||||
|                                         setDeleteOpen(true); | ||||
|                                     }} | ||||
|                                 > | ||||
|                                     <Delete /> | ||||
|                                 </IconButton> | ||||
|                             </span> | ||||
|                         </Tooltip> | ||||
|                     </ActionCell> | ||||
|                 ), | ||||
|                 maxWidth: 100, | ||||
|                 disableSortBy: true, | ||||
|             }, | ||||
|         ], | ||||
|         [setSelectedToken, setDeleteOpen] | ||||
|     ); | ||||
| 
 | ||||
|     const { | ||||
|         data: searchedData, | ||||
|         getSearchText, | ||||
|         getSearchContext, | ||||
|     } = useSearch(columns, searchValue, tokens); | ||||
| 
 | ||||
|     const data = useMemo( | ||||
|         () => | ||||
|             searchedData?.length === 0 && loading | ||||
|                 ? tokensPlaceholder | ||||
|                 : searchedData, | ||||
|         [searchedData, loading] | ||||
|     ); | ||||
| 
 | ||||
|     const { | ||||
|         headerGroups, | ||||
|         rows, | ||||
|         prepareRow, | ||||
|         state: { sortBy }, | ||||
|         setHiddenColumns, | ||||
|     } = useTable( | ||||
|         { | ||||
|             columns, | ||||
|             data, | ||||
|             initialState, | ||||
|             sortTypes, | ||||
|             autoResetSortBy: false, | ||||
|             disableSortRemove: true, | ||||
|             disableMultiSort: true, | ||||
|         }, | ||||
|         useSortBy, | ||||
|         useFlexLayout | ||||
|     ); | ||||
| 
 | ||||
|     useConditionallyHiddenColumns( | ||||
|         [ | ||||
|             { | ||||
|                 condition: isExtraSmallScreen, | ||||
|                 columns: ['expiresAt'], | ||||
|             }, | ||||
|             { | ||||
|                 condition: isSmallScreen, | ||||
|                 columns: ['createdAt'], | ||||
|             }, | ||||
|             { | ||||
|                 condition: Boolean(readOnly), | ||||
|                 columns: ['Actions', 'expiresAt', 'createdAt'], | ||||
|             }, | ||||
|         ], | ||||
|         setHiddenColumns, | ||||
|         columns | ||||
|     ); | ||||
| 
 | ||||
|     return ( | ||||
|         <> | ||||
|             <ConditionallyRender | ||||
|                 condition={!readOnly} | ||||
|                 show={ | ||||
|                     <StyledHeader> | ||||
|                         <Search | ||||
|                             initialValue={searchValue} | ||||
|                             onChange={setSearchValue} | ||||
|                             getSearchContext={getSearchContext} | ||||
|                         /> | ||||
|                         <Button | ||||
|                             variant="contained" | ||||
|                             color="primary" | ||||
|                             disabled={tokens.length >= PAT_LIMIT} | ||||
|                             onClick={() => setCreateOpen(true)} | ||||
|                         > | ||||
|                             New token | ||||
|                         </Button> | ||||
|                     </StyledHeader> | ||||
|                 } | ||||
|             /> | ||||
|             <SearchHighlightProvider value={getSearchText(searchValue)}> | ||||
|                 <VirtualizedTable | ||||
|                     rows={rows} | ||||
|                     headerGroups={headerGroups} | ||||
|                     prepareRow={prepareRow} | ||||
|                 /> | ||||
|             </SearchHighlightProvider> | ||||
|             <ConditionallyRender | ||||
|                 condition={rows.length === 0} | ||||
|                 show={ | ||||
|                     <ConditionallyRender | ||||
|                         condition={searchValue?.length > 0} | ||||
|                         show={ | ||||
|                             <TablePlaceholder> | ||||
|                                 No tokens found matching “ | ||||
|                                 {searchValue} | ||||
|                                 ” | ||||
|                             </TablePlaceholder> | ||||
|                         } | ||||
|                         elseShow={ | ||||
|                             <StyledTablePlaceholder> | ||||
|                                 <StyledPlaceholderTitle> | ||||
|                                     You have no tokens for this service account | ||||
|                                     yet. | ||||
|                                 </StyledPlaceholderTitle> | ||||
|                                 <StyledPlaceholderSubtitle> | ||||
|                                     Create a service account token for access to | ||||
|                                     the Unleash API. | ||||
|                                 </StyledPlaceholderSubtitle> | ||||
|                                 <Button | ||||
|                                     variant="outlined" | ||||
|                                     onClick={() => setCreateOpen(true)} | ||||
|                                 > | ||||
|                                     Create new service account token | ||||
|                                 </Button> | ||||
|                             </StyledTablePlaceholder> | ||||
|                         } | ||||
|                     /> | ||||
|                 } | ||||
|             /> | ||||
|             <ServiceAccountCreateTokenDialog | ||||
|                 open={createOpen} | ||||
|                 setOpen={setCreateOpen} | ||||
|                 serviceAccount={serviceAccount} | ||||
|                 onCreateClick={onCreateClick} | ||||
|             /> | ||||
|             <ServiceAccountTokenDialog | ||||
|                 open={tokenOpen} | ||||
|                 setOpen={setTokenOpen} | ||||
|                 token={newToken} | ||||
|             /> | ||||
|             <Dialogue | ||||
|                 open={deleteOpen} | ||||
|                 primaryButtonText="Delete token" | ||||
|                 secondaryButtonText="Cancel" | ||||
|                 onClick={onDeleteClick} | ||||
|                 onClose={() => { | ||||
|                     setDeleteOpen(false); | ||||
|                 }} | ||||
|                 title="Delete token?" | ||||
|             > | ||||
|                 <Typography> | ||||
|                     Any applications or scripts using this token " | ||||
|                     <strong>{selectedToken?.description}</strong>" will no | ||||
|                     longer be able to access the Unleash API. You cannot undo | ||||
|                     this action. | ||||
|                 </Typography> | ||||
|             </Dialogue> | ||||
|         </> | ||||
|     ); | ||||
| }; | ||||
| @ -13,7 +13,6 @@ import { useFlexLayout, useSortBy, useTable } from 'react-table'; | ||||
| import { sortTypes } from 'utils/sortTypes'; | ||||
| import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell'; | ||||
| import { TextCell } from 'component/common/Table/cells/TextCell/TextCell'; | ||||
| import { useNavigate } from 'react-router-dom'; | ||||
| import { DateCell } from 'component/common/Table/cells/DateCell/DateCell'; | ||||
| import theme from 'themes/theme'; | ||||
| import { Search } from 'component/common/Search/Search'; | ||||
| @ -29,7 +28,6 @@ import { INewPersonalAPIToken } from 'interfaces/personalAPIToken'; | ||||
| import { ServiceAccountTokenDialog } from './ServiceAccountTokenDialog/ServiceAccountTokenDialog'; | ||||
| 
 | ||||
| export const ServiceAccountsTable = () => { | ||||
|     const navigate = useNavigate(); | ||||
|     const { setToastData, setToastApiError } = useToast(); | ||||
| 
 | ||||
|     const { serviceAccounts, roles, refetch, loading } = useServiceAccounts(); | ||||
| @ -128,7 +126,7 @@ export const ServiceAccountsTable = () => { | ||||
|                 searchable: true, | ||||
|             }, | ||||
|         ], | ||||
|         [roles, navigate] | ||||
|         [roles] | ||||
|     ); | ||||
| 
 | ||||
|     const [initialState] = useState({ | ||||
|  | ||||
| @ -155,7 +155,7 @@ export const PersonalAPITokenForm = ({ | ||||
|         if (isDescriptionUnique && !isDescriptionUnique(description)) { | ||||
|             setError( | ||||
|                 ErrorField.DESCRIPTION, | ||||
|                 'A personal API token with that description already exists.' | ||||
|                 'A token with that description already exists.' | ||||
|             ); | ||||
|         } | ||||
|         setDescription(description); | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| import { INewPersonalAPIToken } from 'interfaces/personalAPIToken'; | ||||
| import useAPI from '../useApi/useApi'; | ||||
| 
 | ||||
| interface ICreatePersonalApiTokenPayload { | ||||
| export interface ICreatePersonalApiTokenPayload { | ||||
|     description: string; | ||||
|     expiresAt: Date; | ||||
| } | ||||
| @ -53,10 +53,22 @@ export const usePersonalAPITokensApi = () => { | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     const deleteUserPersonalAPIToken = async (userId: number, id: string) => { | ||||
|         const req = createRequest(`api/admin/user-admin/${userId}/pat/${id}`, { | ||||
|             method: 'DELETE', | ||||
|         }); | ||||
|         try { | ||||
|             await makeRequest(req.caller, req.id); | ||||
|         } catch (e) { | ||||
|             throw e; | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     return { | ||||
|         createPersonalAPIToken, | ||||
|         deletePersonalAPIToken, | ||||
|         createUserPersonalAPIToken, | ||||
|         deleteUserPersonalAPIToken, | ||||
|         errors, | ||||
|         loading, | ||||
|     }; | ||||
|  | ||||
| @ -10,9 +10,15 @@ export interface IUsePersonalAPITokensOutput { | ||||
|     error?: Error; | ||||
| } | ||||
| 
 | ||||
| export const usePersonalAPITokens = (): IUsePersonalAPITokensOutput => { | ||||
| export const usePersonalAPITokens = ( | ||||
|     userId?: number | ||||
| ): IUsePersonalAPITokensOutput => { | ||||
|     const { data, error, mutate } = useSWR( | ||||
|         formatApiPath('api/admin/user/tokens'), | ||||
|         formatApiPath( | ||||
|             userId | ||||
|                 ? `api/admin/user-admin/${userId}/pat` | ||||
|                 : 'api/admin/user/tokens' | ||||
|         ), | ||||
|         fetcher | ||||
|     ); | ||||
| 
 | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user