mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: Add project and environment scoping to API keys (#336)
Co-authored-by: Fredrik Strand Oseberg <fredrik.no@gmail.com>
This commit is contained in:
		
							parent
							
								
									34df8617d2
								
							
						
					
					
						commit
						1845eb95e6
					
				| @ -0,0 +1,192 @@ | ||||
| import { TextField, } from '@material-ui/core'; | ||||
| import classNames from 'classnames'; | ||||
| import React, { useState, useEffect } from 'react'; | ||||
| import { styles as commonStyles } from '../../../component/common'; | ||||
| import { IApiTokenCreate } from '../../../hooks/api/actions/useApiTokensApi/useApiTokensApi'; | ||||
| import useEnvironments from '../../../hooks/api/getters/useEnvironments/useEnvironments'; | ||||
| import useProjects from '../../../hooks/api/getters/useProjects/useProjects'; | ||||
| import Dialogue from '../../common/Dialogue'; | ||||
| import MySelect from '../../common/select'; | ||||
| import { useStyles } from './styles'; | ||||
| 
 | ||||
| const ALL = '*'; | ||||
| const TYPE_ADMIN = 'ADMIN'; | ||||
| const TYPE_CLIENT = 'CLIENT'; | ||||
| 
 | ||||
| interface IApiTokenCreateProps { | ||||
|     showDialog: boolean; | ||||
|     closeDialog: () => void; | ||||
|     createToken: (token: IApiTokenCreate) => Promise<void>; | ||||
| } | ||||
| 
 | ||||
| interface IDataError { | ||||
|     username?: string; | ||||
|     general?: string; | ||||
| } | ||||
| 
 | ||||
| const INITIAL_DATA: IApiTokenCreate = { | ||||
|     username: '', | ||||
|     type: TYPE_CLIENT, | ||||
|     project: ALL | ||||
| } | ||||
| 
 | ||||
| const ApiTokenCreate = ({ | ||||
|     showDialog, | ||||
|     closeDialog, | ||||
|     createToken, | ||||
| }: IApiTokenCreateProps) => { | ||||
|     const styles = useStyles(); | ||||
|     const [data, setData] = useState(INITIAL_DATA); | ||||
|     const [error, setError] = useState<IDataError>({}); | ||||
|     const { projects } = useProjects(); | ||||
|     const { environments } = useEnvironments(); | ||||
| 
 | ||||
|     useEffect(() => { | ||||
|         if(environments && data.type === TYPE_CLIENT && !data.environment) { | ||||
|             setData({...data, environment: environments[0].name}) | ||||
|         } | ||||
|     }, [data, environments]); | ||||
| 
 | ||||
|     const clear = () => { | ||||
|         setData({...INITIAL_DATA}); | ||||
|         setError({}); | ||||
|     } | ||||
| 
 | ||||
|     const onCancel = (e: Event) => { | ||||
|         clear(); | ||||
|         closeDialog(); | ||||
|     }; | ||||
| 
 | ||||
|     const isValid = () => { | ||||
|         if(!data.username) { | ||||
|             setError({username: 'Username is required.'}); | ||||
|             return false; | ||||
|         } else { | ||||
|             setError({}) | ||||
|             return true; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|      | ||||
| 
 | ||||
|     const submit = async () => { | ||||
|         if(!isValid()) { | ||||
|             return; | ||||
|         } | ||||
|          | ||||
|         try { | ||||
|             await createToken(data); | ||||
|             clear(); | ||||
|             closeDialog(); | ||||
|         } catch (error) { | ||||
|             setError({general: 'Unable to create new API token'}); | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     const setType = (event: React.ChangeEvent<{value: string }>) => { | ||||
|         const value = event.target.value; | ||||
|         if(value === TYPE_ADMIN) {   | ||||
|             setData({...data, type: value, environment: ALL, project: ALL}) | ||||
| 
 | ||||
|         } else { | ||||
|             setData({...data, type: value, environment: environments[0].name}) | ||||
|         } | ||||
|          | ||||
|     } | ||||
| 
 | ||||
|     const setUsername = (event: React.ChangeEvent<{value: string }>) => { | ||||
|         const value = event.target.value; | ||||
|         setData({...data, username: value}) | ||||
|     } | ||||
| 
 | ||||
|     const setProject = (event:  React.ChangeEvent<{value: string }>) => { | ||||
|         const value = event.target.value; | ||||
|         setData({...data, project: value}) | ||||
|     } | ||||
| 
 | ||||
|     const setEnvironment = (event: React.ChangeEvent<{value: string }>) => { | ||||
|         const value = event.target.value; | ||||
|         setData({...data, environment: value}) | ||||
|     } | ||||
| 
 | ||||
|     const selectableProjects = [{id: '*', name: 'ALL'}, ...projects].map(i => ({ | ||||
|         key: i.id, | ||||
|         label: i.name, | ||||
|         title: i.name, | ||||
|     })); | ||||
|      | ||||
|     const selectableEnvs = data.type === TYPE_ADMIN ? [{key: '*', label: 'ALL'}] : environments.map(i => ({ | ||||
|         key: i.name, | ||||
|         label: i.name, | ||||
|         title: i.name, | ||||
|     })); | ||||
| 
 | ||||
| 
 | ||||
|     const selectableTypes = [ | ||||
|         {key: 'CLIENT', label: 'Client', title: 'Client SDK token'}, | ||||
|         {key: 'ADMIN', label: 'Admin', title: 'Admin API token'} | ||||
|     ] | ||||
| 
 | ||||
|     return ( | ||||
|         <Dialogue | ||||
|             onClick={() => submit()} | ||||
|             open={showDialog} | ||||
|             onClose={onCancel} | ||||
|             primaryButtonText="Create" | ||||
|             secondaryButtonText="Cancel" | ||||
|             title="New API token" | ||||
|         > | ||||
|             <form | ||||
|                     onSubmit={submit} | ||||
|                     className={classNames( | ||||
|                         styles.addApiKeyForm, | ||||
|                         commonStyles.contentSpacing | ||||
|                     )} | ||||
|                 > | ||||
|                     <TextField | ||||
|                         value={data.username} | ||||
|                         name="username" | ||||
|                         onChange={setUsername} | ||||
|                         onBlur={isValid} | ||||
|                         label="Username" | ||||
|                         style={{ width: '200px' }} | ||||
|                         error={error.username !== undefined} | ||||
|                         helperText={error.username} | ||||
|                         variant="outlined" | ||||
|                         size="small" | ||||
|                         required | ||||
|                     /> | ||||
|                     <MySelect | ||||
|                         disabled={false} | ||||
|                         options={selectableTypes} | ||||
|                         value={data.type} | ||||
|                         onChange={setType} | ||||
|                         label="Token Type" | ||||
|                         id='api_key_type' | ||||
|                         name="type" className={undefined} classes={undefined} | ||||
|                     /> | ||||
|                     <MySelect | ||||
|                         disabled={data.type === TYPE_ADMIN} | ||||
|                         options={selectableProjects} | ||||
|                         value={data.project} | ||||
|                         onChange={setProject} | ||||
|                         label="Project" | ||||
|                         id='api_key_project' | ||||
|                         name="project" className={undefined} classes={undefined} | ||||
|                     /> | ||||
|                     <MySelect | ||||
|                         disabled={data.type === TYPE_ADMIN} | ||||
|                         options={selectableEnvs} | ||||
|                         value={data.environment} | ||||
|                         required | ||||
|                         onChange={setEnvironment} | ||||
|                         label="Environment" | ||||
|                         id='api_key_environment' | ||||
|                         name="environment" className={undefined} classes={undefined}                    /> | ||||
|                 </form> | ||||
|         </Dialogue> | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| export default ApiTokenCreate; | ||||
| @ -0,0 +1,27 @@ | ||||
| import { makeStyles } from '@material-ui/core/styles'; | ||||
| 
 | ||||
| export const useStyles = makeStyles(theme => ({ | ||||
|     container: { | ||||
|         display: 'flex', | ||||
|         flexWrap: 'wrap', | ||||
|         [theme.breakpoints.down('xs')]: { | ||||
|             justifyContent: 'center', | ||||
|         }, | ||||
|     }, | ||||
|     apiError: { | ||||
|         maxWidth: '400px', | ||||
|         marginBottom: '1rem', | ||||
|     }, | ||||
|     cardLink: { | ||||
|         color: 'inherit', | ||||
|         textDecoration: 'none', | ||||
|         border: 'none', | ||||
|         padding: '0', | ||||
|         background: 'transparent', | ||||
|         fontFamily: theme.typography.fontFamily, | ||||
|         pointer: 'cursor', | ||||
|     }, | ||||
|     center: { | ||||
|         textAlign: 'center' | ||||
|     } | ||||
| })); | ||||
							
								
								
									
										206
									
								
								frontend/src/component/api-token/ApiTokenList/ApiTokenList.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										206
									
								
								frontend/src/component/api-token/ApiTokenList/ApiTokenList.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,206 @@ | ||||
| import { useContext, useState } from 'react'; | ||||
| import { Link } from 'react-router-dom'; | ||||
| import { Button, IconButton, Table, TableBody, TableCell, TableHead, TableRow } from '@material-ui/core'; | ||||
| import AccessContext from '../../../contexts/AccessContext'; | ||||
| import useToast from '../../../hooks/useToast'; | ||||
| import useLoading from '../../../hooks/useLoading'; | ||||
| import useApiTokens from '../../../hooks/api/getters/useApiTokens/useApiTokens'; | ||||
| import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig'; | ||||
| import useApiTokensApi, { IApiTokenCreate } from '../../../hooks/api/actions/useApiTokensApi/useApiTokensApi'; | ||||
| import ApiError from '../../common/ApiError/ApiError'; | ||||
| import PageContent from '../../common/PageContent'; | ||||
| import HeaderTitle from '../../common/HeaderTitle'; | ||||
| import ConditionallyRender from '../../common/ConditionallyRender'; | ||||
| import { CREATE_API_TOKEN, DELETE_API_TOKEN } from '../../AccessProvider/permissions'; | ||||
| import { useStyles } from './ApiTokenList.styles'; | ||||
| import { formatDateWithLocale } from '../../common/util'; | ||||
| import Secret from './secret'; | ||||
| import { Delete } from '@material-ui/icons'; | ||||
| import ApiTokenCreate from '../ApiTokenCreate/ApiTokenCreate'; | ||||
| import Dialogue from '../../common/Dialogue'; | ||||
| 
 | ||||
| interface IApiToken { | ||||
|     createdAt: Date; | ||||
|     username: string; | ||||
|     secret: string; | ||||
|     type: string; | ||||
|     project: string; | ||||
|     environment: string; | ||||
| } | ||||
| 
 | ||||
| interface IApiTokenList { | ||||
|     location: any; | ||||
| } | ||||
| 
 | ||||
| const ApiTokenList = ({ location }: IApiTokenList) => { | ||||
|     const styles = useStyles(); | ||||
|     const { hasAccess } = useContext(AccessContext); | ||||
|     const { uiConfig } = useUiConfig(); | ||||
|     const [showDelete, setShowDelete] = useState(false); | ||||
|     const [delToken, setDeleteToken] = useState<IApiToken>(); | ||||
|     const { toast, setToastData } = useToast(); | ||||
|     const { tokens, loading, refetch, error } = useApiTokens(); | ||||
|     const { deleteToken, createToken } = useApiTokensApi(); | ||||
|     const ref = useLoading(loading); | ||||
| 
 | ||||
|     const [showDialog, setDialog] = useState(false); | ||||
| 
 | ||||
|     const openDialog = () => { | ||||
|         setDialog(true); | ||||
|     }; | ||||
| 
 | ||||
|     const closeDialog = () => { | ||||
|         setDialog(false); | ||||
|     }; | ||||
|         | ||||
| 
 | ||||
|     const renderError = () => { | ||||
|         return ( | ||||
|             <ApiError | ||||
|                 onClick={refetch} | ||||
|                 // className={styles.apiError}
 | ||||
|                 text="Error fetching api tokens" | ||||
|             /> | ||||
|         ); | ||||
|     }; | ||||
| 
 | ||||
|     const onCreateToken = async (token: IApiTokenCreate) => { | ||||
|         await createToken(token); | ||||
|         refetch(); | ||||
|         setToastData({ | ||||
|             type: 'success', | ||||
|             show: true, | ||||
|             text: 'Successfully created API token.', | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     const onDeleteToken = async () => { | ||||
|         if(delToken) { | ||||
|             await deleteToken(delToken.secret); | ||||
|         } | ||||
|         setDeleteToken(undefined); | ||||
|         setShowDelete(false); | ||||
|         refetch(); | ||||
|         setToastData({ | ||||
|             type: 'success', | ||||
|             show: true, | ||||
|             text: 'Successfully deleted API token.', | ||||
|         }); | ||||
|     }; | ||||
| 
 | ||||
|     const renderProject = (projectId: string) => { | ||||
|         if(!projectId || projectId === '*') { | ||||
|             return projectId; | ||||
|         } else { | ||||
|             return (<Link to={`/projects/${projectId}`}>{projectId}</Link>); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     const renderApiTokens = (tokens: IApiToken[]) => { | ||||
|         return ( | ||||
|             <Table size="small"> | ||||
|                 <TableHead> | ||||
|                     <TableRow> | ||||
|                         <TableCell>Created</TableCell> | ||||
|                         <TableCell>Username</TableCell> | ||||
|                         <TableCell className={styles.center}>Type</TableCell> | ||||
|                         <ConditionallyRender condition={uiConfig.flags.E} show={<> | ||||
|                             <TableCell className={styles.center}>Project</TableCell> | ||||
|                             <TableCell className={styles.center}>Environment</TableCell> | ||||
|                         </>} /> | ||||
|                         <TableCell>Secret</TableCell> | ||||
|                         <TableCell align="right">Action</TableCell> | ||||
|                     </TableRow> | ||||
|                 </TableHead> | ||||
|                 <TableBody> | ||||
|                     {tokens.map(item => { | ||||
|                         return ( | ||||
|                             <TableRow key={item.secret}> | ||||
|                                 <TableCell align="left"> | ||||
|                                     {formatDateWithLocale( | ||||
|                                         item.createdAt, | ||||
|                                         location.locale | ||||
|                                     )} | ||||
|                                 </TableCell> | ||||
|                                 <TableCell align="left"> | ||||
|                                     {item.username} | ||||
|                                 </TableCell> | ||||
|                                 <TableCell className={styles.center}> | ||||
|                                     {item.type} | ||||
|                                 </TableCell> | ||||
|                                 <ConditionallyRender condition={uiConfig.flags.E} show={<> | ||||
|                                     <TableCell className={styles.center}> | ||||
|                                         {renderProject(item.project)} | ||||
|                                     </TableCell> | ||||
|                                     <TableCell className={styles.center}> | ||||
|                                         {item.environment} | ||||
|                                     </TableCell> | ||||
|                                 </>} /> | ||||
| 
 | ||||
|                                 <TableCell> | ||||
|                                     <Secret value={item.secret} /> | ||||
|                                 </TableCell> | ||||
|                                 <ConditionallyRender | ||||
|                                     condition={hasAccess(DELETE_API_TOKEN)} | ||||
|                                     show={<TableCell | ||||
|                                         width="20" | ||||
|                                         style={{ textAlign: 'right' }} | ||||
|                                     > | ||||
|                                         <IconButton | ||||
|                                             onClick={() => { | ||||
|                                                 setDeleteToken(item); | ||||
|                                                 setShowDelete(true); | ||||
|                                             } } | ||||
|                                         > | ||||
|                                             <Delete /> | ||||
|                                         </IconButton> | ||||
|                                     </TableCell>} /> | ||||
|                             </TableRow> | ||||
|                         ); | ||||
|                     })} | ||||
|                 </TableBody> | ||||
|             </Table>) | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|         <div ref={ref}> | ||||
|             <PageContent | ||||
|                 headerContent={<HeaderTitle | ||||
|                     title="API Access" | ||||
|                     actions={<ConditionallyRender | ||||
|                         condition={hasAccess(CREATE_API_TOKEN)} | ||||
|                         show={<Button variant="contained" color="primary" onClick={openDialog}>Create API token</Button>} />} />} | ||||
|                 > | ||||
|                 <ConditionallyRender condition={error} show={renderError()} /> | ||||
|                 <div className={styles.container}> | ||||
|                     <ConditionallyRender | ||||
|                         condition={tokens.length < 1 && !loading} | ||||
|                         show={<div>No API tokens available.</div>} | ||||
|                         elseShow={renderApiTokens(tokens)} | ||||
|                     /> | ||||
|                 </div> | ||||
|                 {toast} | ||||
|                 <ApiTokenCreate showDialog={showDialog} createToken={onCreateToken} closeDialog={closeDialog} /> | ||||
|                 <Dialogue | ||||
|                     open={showDelete} | ||||
|                     onClick={onDeleteToken} | ||||
|                     onClose={() => { | ||||
|                         setShowDelete(false); | ||||
|                         setDeleteToken(undefined); | ||||
|                     }} | ||||
|                     title="Confirm deletion" | ||||
|                 > | ||||
|                     <div> | ||||
|                         Are you sure you want to delete the following API token?<br /> | ||||
|                         <ul> | ||||
|                         <li><strong>username</strong>: <code>{delToken?.username}</code></li> | ||||
|                         <li><strong>type</strong>: <code>{delToken?.type}</code></li> | ||||
|                         </ul> | ||||
|                     </div> | ||||
|                 </Dialogue> | ||||
|             </PageContent> | ||||
|         </div> | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| export default ApiTokenList; | ||||
| @ -12,9 +12,9 @@ function Secret({ value }) { | ||||
|     return ( | ||||
|         <div> | ||||
|             {show ? ( | ||||
|                 <input readOnly value={value} style={{ width: '240px' }} /> | ||||
|                 <input readOnly value={value} style={{ width: '250px' }} /> | ||||
|             ) : ( | ||||
|                 <span>***************************</span> | ||||
|                 <span style={{ width: '250px', display: 'inline-block' }}>************************************</span> | ||||
|             )} | ||||
| 
 | ||||
|             <IconButton | ||||
| @ -16,7 +16,6 @@ const Dialogue = ({ | ||||
|     onClick, | ||||
|     onClose, | ||||
|     title, | ||||
|     style, | ||||
|     primaryButtonText, | ||||
|     disabledPrimaryButton = false, | ||||
|     secondaryButtonText, | ||||
|  | ||||
| @ -9,9 +9,9 @@ import { useStyles } from './styles'; | ||||
| const PageContent = ({ | ||||
|     children, | ||||
|     headerContent, | ||||
|     disablePadding, | ||||
|     disableBorder, | ||||
|     bodyClass, | ||||
|     disablePadding = false, | ||||
|     disableBorder = false, | ||||
|     bodyClass = undefined, | ||||
|     ...rest | ||||
| }) => { | ||||
|     const styles = useStyles(); | ||||
|  | ||||
| @ -0,0 +1,44 @@ | ||||
| import useAPI from '../useApi/useApi'; | ||||
| 
 | ||||
| export interface IApiTokenCreate { | ||||
|     username: string; | ||||
|     type: string; | ||||
|     project: string; | ||||
|     environment?: string; | ||||
| } | ||||
| 
 | ||||
| const useApiTokensApi = () => { | ||||
|     const { makeRequest, createRequest, errors } = useAPI({ | ||||
|         propagateErrors: true, | ||||
|     }); | ||||
| 
 | ||||
|     const deleteToken = async (secret: string) => { | ||||
|         const path = `api/admin/api-tokens/${secret}`; | ||||
|         const req = createRequest(path, { method: 'DELETE' }); | ||||
| 
 | ||||
|         try { | ||||
|             const res = await makeRequest(req.caller, req.id); | ||||
| 
 | ||||
|             return res; | ||||
|         } catch (e) { | ||||
|             throw e; | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     const createToken = async (newToken: IApiTokenCreate) => { | ||||
|         const path = `api/admin/api-tokens`; | ||||
|         const req = createRequest(path, { method: 'POST', body: JSON.stringify(newToken) }); | ||||
| 
 | ||||
|         try { | ||||
|             const res = await makeRequest(req.caller, req.id); | ||||
| 
 | ||||
|             return res; | ||||
|         } catch (e) { | ||||
|             throw e; | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     return { deleteToken, createToken, errors }; | ||||
| }; | ||||
| 
 | ||||
| export default useApiTokensApi; | ||||
							
								
								
									
										35
									
								
								frontend/src/hooks/api/getters/useApiTokens/useApiTokens.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								frontend/src/hooks/api/getters/useApiTokens/useApiTokens.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,35 @@ | ||||
| import useSWR, { mutate } from 'swr'; | ||||
| import { useState, useEffect } from 'react'; | ||||
| import { formatApiPath } from '../../../../utils/format-path'; | ||||
| 
 | ||||
| const useApiTokens = () => { | ||||
|     const fetcher = async () => { | ||||
|         const path = formatApiPath(`api/admin/api-tokens`); | ||||
|         const res = await fetch(path, { | ||||
|             method: 'GET', | ||||
|         }); | ||||
|         return res.json(); | ||||
|     }; | ||||
| 
 | ||||
|     const KEY = `api/admin/api-tokens`; | ||||
| 
 | ||||
|     const { data, error } = useSWR(KEY, fetcher); | ||||
|     const [loading, setLoading] = useState(!error && !data); | ||||
| 
 | ||||
|     const refetch = () => { | ||||
|         mutate(KEY); | ||||
|     }; | ||||
| 
 | ||||
|     useEffect(() => { | ||||
|         setLoading(!error && !data); | ||||
|     }, [data, error]); | ||||
| 
 | ||||
|     return { | ||||
|         tokens: data?.tokens || [], | ||||
|         error, | ||||
|         loading, | ||||
|         refetch, | ||||
|     }; | ||||
| }; | ||||
| 
 | ||||
| export default useApiTokens; | ||||
| @ -13,6 +13,7 @@ export interface IUiConfig { | ||||
| export interface IFlags { | ||||
|     C: boolean; | ||||
|     P: boolean; | ||||
|     E: boolean; | ||||
| } | ||||
| 
 | ||||
| export interface IVersionInfo { | ||||
|  | ||||
| @ -1,114 +0,0 @@ | ||||
| import React, { useState } from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import { | ||||
|     Select, | ||||
|     TextField, | ||||
|     Button, | ||||
|     MenuItem, | ||||
|     FormControl, | ||||
|     InputLabel, | ||||
| } from '@material-ui/core'; | ||||
| import Dialogue from '../../../component/common/Dialogue/Dialogue'; | ||||
| import classnames from 'classnames'; | ||||
| import { styles as commonStyles } from '../../../component/common'; | ||||
| import { useStyles } from './styles'; | ||||
| 
 | ||||
| function CreateApiKey({ addKey, show, setShow }) { | ||||
|     const styles = useStyles(); | ||||
|     const [type, setType] = useState('CLIENT'); | ||||
|     const [username, setUsername] = useState(); | ||||
|     const [error, setError] = useState(); | ||||
| 
 | ||||
|     const toggle = evt => { | ||||
|         evt.preventDefault(); | ||||
|         setShow(!show); | ||||
|     }; | ||||
| 
 | ||||
|     const submit = async e => { | ||||
|         e.preventDefault(); | ||||
|         if (!username) { | ||||
|             setError('You must define a username'); | ||||
|             return; | ||||
|         } | ||||
|         await addKey({ username, type }); | ||||
|         setUsername(''); | ||||
|         setType('CLIENT'); | ||||
|         setShow(false); | ||||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
|         <div style={{ margin: '5px' }}> | ||||
|             <Dialogue | ||||
|                 onClick={e => { | ||||
|                     submit(e); | ||||
|                     setShow(false); | ||||
|                 }} | ||||
|                 open={show} | ||||
|                 primaryButtonText="Create new key" | ||||
|                 onClose={toggle} | ||||
|                 secondaryButtonText="Cancel" | ||||
|                 title="Add new API key" | ||||
|             > | ||||
|                 <form | ||||
|                     onSubmit={submit} | ||||
|                     className={classnames( | ||||
|                         styles.addApiKeyForm, | ||||
|                         commonStyles.contentSpacing | ||||
|                     )} | ||||
|                 > | ||||
|                     <TextField | ||||
|                         value={username || ''} | ||||
|                         name="username" | ||||
|                         onChange={e => setUsername(e.target.value)} | ||||
|                         label="Username" | ||||
|                         style={{ width: '200px' }} | ||||
|                         error={error !== undefined} | ||||
|                         helperText={error} | ||||
|                         variant="outlined" | ||||
|                         size="small" | ||||
|                     /> | ||||
|                     <FormControl | ||||
|                         variant="outlined" | ||||
|                         size="small" | ||||
|                         style={{ minWidth: '120px' }} | ||||
|                     > | ||||
|                         <InputLabel id="apikey_type" /> | ||||
|                         <Select | ||||
|                             labelId="apikey_type" | ||||
|                             id="apikey_select" | ||||
|                             value={type} | ||||
|                             onChange={e => setType(e.target.value)} | ||||
|                         > | ||||
|                             <MenuItem | ||||
|                                 value="CLIENT" | ||||
|                                 key="apikey_client" | ||||
|                                 title="Client" | ||||
|                             > | ||||
|                                 Client | ||||
|                             </MenuItem> | ||||
|                             <MenuItem | ||||
|                                 value="ADMIN" | ||||
|                                 key="apikey_admin" | ||||
|                                 title="Admin" | ||||
|                             > | ||||
|                                 Admin | ||||
|                             </MenuItem> | ||||
|                         </Select> | ||||
|                     </FormControl> | ||||
|                 </form> | ||||
|             </Dialogue> | ||||
|             <Button onClick={toggle} variant="contained" color="primary"> | ||||
|                 Add new API key | ||||
|             </Button> | ||||
|         </div> | ||||
|     ); | ||||
| } | ||||
| 
 | ||||
| CreateApiKey.propTypes = { | ||||
|     addKey: PropTypes.func.isRequired, | ||||
|     setShow: PropTypes.func.isRequired, | ||||
|     show: PropTypes.bool.isRequired, | ||||
|     toggle: PropTypes.func.isRequired, | ||||
| }; | ||||
| 
 | ||||
| export default CreateApiKey; | ||||
| @ -1,12 +0,0 @@ | ||||
| import { connect } from 'react-redux'; | ||||
| 
 | ||||
| import Component from './api-key-list'; | ||||
| import { fetchApiKeys, removeKey, addKey } from './../../../store/e-api-admin/actions'; | ||||
| export default connect( | ||||
|     state => ({ | ||||
|         location: state.settings.toJS().location || {}, | ||||
|         unleashUrl: state.uiConfig.toJS().unleashUrl, | ||||
|         keys: state.apiAdmin.toJS(), | ||||
|     }), | ||||
|     { fetchApiKeys, removeKey, addKey } | ||||
| )(Component); | ||||
| @ -1,169 +0,0 @@ | ||||
| /* eslint-disable no-alert */ | ||||
| import React, { useContext, useEffect, useState } from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import { | ||||
|     Table, | ||||
|     TableHead, | ||||
|     TableBody, | ||||
|     TableRow, | ||||
|     TableCell, | ||||
|     IconButton, | ||||
| } from '@material-ui/core'; | ||||
| import { Delete } from '@material-ui/icons'; | ||||
| import { Alert } from '@material-ui/lab'; | ||||
| import { formatFullDateTimeWithLocale } from '../../../component/common/util'; | ||||
| import CreateApiKey from './api-key-create'; | ||||
| import Secret from './secret'; | ||||
| import ConditionallyRender from '../../../component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import Dialogue from '../../../component/common/Dialogue/Dialogue'; | ||||
| import AccessContext from '../../../contexts/AccessContext'; | ||||
| import { | ||||
|     DELETE_API_TOKEN, | ||||
|     CREATE_API_TOKEN, | ||||
| } from '../../../component/AccessProvider/permissions'; | ||||
| import PageContent from '../../../component/common/PageContent'; | ||||
| import HeaderTitle from '../../../component/common/HeaderTitle'; | ||||
| 
 | ||||
| function ApiKeyList({ | ||||
|     location, | ||||
|     fetchApiKeys, | ||||
|     removeKey, | ||||
|     addKey, | ||||
|     keys, | ||||
|     unleashUrl, | ||||
| }) { | ||||
|     const [show, setShow] = useState(false); | ||||
| 
 | ||||
|     const { hasAccess } = useContext(AccessContext); | ||||
|     const [showDelete, setShowDelete] = useState(false); | ||||
|     const [delKey, setDelKey] = useState(undefined); | ||||
|     const deleteKey = async () => { | ||||
|         await removeKey(delKey); | ||||
|         setDelKey(undefined); | ||||
|         setShowDelete(false); | ||||
|     }; | ||||
| 
 | ||||
|     useEffect(() => { | ||||
|         fetchApiKeys(); | ||||
|         // eslint-disable-next-line react-hooks/exhaustive-deps | ||||
|     }, []); | ||||
| 
 | ||||
|     return ( | ||||
|         <PageContent | ||||
|             headerContent={ | ||||
|                 <HeaderTitle | ||||
|                     title="API Access" | ||||
|                     actions={ | ||||
|                         <ConditionallyRender | ||||
|                             condition={hasAccess(CREATE_API_TOKEN)} | ||||
|                             show={ | ||||
|                                 <CreateApiKey | ||||
|                                     addKey={addKey} | ||||
|                                     setShow={setShow} | ||||
|                                     show={show} | ||||
|                                 /> | ||||
|                             } | ||||
|                         /> | ||||
|                     } | ||||
|                 /> | ||||
|             } | ||||
|         > | ||||
|             <div> | ||||
|                 <Alert severity="info"> | ||||
|                     <p> | ||||
|                         Read the{' '} | ||||
|                         <a | ||||
|                             href="https://docs.getunleash.io/docs" | ||||
|                             target="_blank" | ||||
|                             rel="noreferrer" | ||||
|                         > | ||||
|                             Getting started guide | ||||
|                         </a>{' '} | ||||
|                         to learn how to connect to the Unleash API from your | ||||
|                         application or programmatically. Please note it can take | ||||
|                         up to 1 minute before a new API key is activated. | ||||
|                     </p> | ||||
|                     <br /> | ||||
|                     <strong>API URL: </strong>{' '} | ||||
|                     <pre style={{ display: 'inline' }}>{unleashUrl}/api/</pre> | ||||
|                 </Alert> | ||||
| 
 | ||||
|                 <br /> | ||||
|                 <br /> | ||||
| 
 | ||||
|                 <br /> | ||||
|                 <Table> | ||||
|                     <TableHead> | ||||
|                         <TableRow> | ||||
|                             <TableCell>Created</TableCell> | ||||
|                             <TableCell>Username</TableCell> | ||||
|                             <TableCell>Access Type</TableCell> | ||||
|                             <TableCell>Secret</TableCell> | ||||
|                             <TableCell>Action</TableCell> | ||||
|                         </TableRow> | ||||
|                     </TableHead> | ||||
|                     <TableBody> | ||||
|                         {keys.map(item => ( | ||||
|                             <TableRow key={item.secret}> | ||||
|                                 <TableCell style={{ textAlign: 'left' }}> | ||||
|                                     {formatFullDateTimeWithLocale( | ||||
|                                         item.createdAt, | ||||
|                                         location.locale | ||||
|                                     )} | ||||
|                                 </TableCell> | ||||
|                                 <TableCell style={{ textAlign: 'left' }}> | ||||
|                                     {item.username} | ||||
|                                 </TableCell> | ||||
|                                 <TableCell style={{ textAlign: 'left' }}> | ||||
|                                     {item.type} | ||||
|                                 </TableCell> | ||||
|                                 <TableCell style={{ textAlign: 'left' }}> | ||||
|                                     <Secret value={item.secret} /> | ||||
|                                 </TableCell> | ||||
|                                 <ConditionallyRender | ||||
|                                     condition={hasAccess(DELETE_API_TOKEN)} | ||||
|                                     show={ | ||||
|                                         <TableCell | ||||
|                                             style={{ textAlign: 'right' }} | ||||
|                                         > | ||||
|                                             <IconButton | ||||
|                                                 onClick={() => { | ||||
|                                                     setDelKey(item.secret); | ||||
|                                                     setShowDelete(true); | ||||
|                                                 }} | ||||
|                                             > | ||||
|                                                 <Delete /> | ||||
|                                             </IconButton> | ||||
|                                         </TableCell> | ||||
|                                     } | ||||
|                                 /> | ||||
|                             </TableRow> | ||||
|                         ))} | ||||
|                     </TableBody> | ||||
|                 </Table> | ||||
|                 <Dialogue | ||||
|                     open={showDelete} | ||||
|                     onClick={deleteKey} | ||||
|                     onClose={() => { | ||||
|                         setShowDelete(false); | ||||
|                         setDelKey(undefined); | ||||
|                     }} | ||||
|                     title="Really delete API key?" | ||||
|                 > | ||||
|                     <div>Are you sure you want to delete?</div> | ||||
|                 </Dialogue> | ||||
|             </div> | ||||
|         </PageContent> | ||||
|     ); | ||||
| } | ||||
| 
 | ||||
| ApiKeyList.propTypes = { | ||||
|     location: PropTypes.object, | ||||
|     fetchApiKeys: PropTypes.func.isRequired, | ||||
|     removeKey: PropTypes.func.isRequired, | ||||
|     addKey: PropTypes.func.isRequired, | ||||
|     keys: PropTypes.array.isRequired, | ||||
|     unleashUrl: PropTypes.string, | ||||
| }; | ||||
| 
 | ||||
| export default ApiKeyList; | ||||
| @ -1,11 +1,11 @@ | ||||
| import PropTypes from 'prop-types'; | ||||
| import ApiKeyList from './api-key-list-container'; | ||||
| import ApiTokenList from '../../../component/api-token/ApiTokenList/ApiTokenList'; | ||||
| 
 | ||||
| import AdminMenu from '../admin-menu'; | ||||
| import usePermissions from '../../../hooks/usePermissions'; | ||||
| import ConditionallyRender from '../../../component/common/ConditionallyRender'; | ||||
| 
 | ||||
| const ApiPage = ({ history }) => { | ||||
| const ApiPage = ({ history, location }) => { | ||||
|     const { isAdmin } = usePermissions(); | ||||
| 
 | ||||
|     return ( | ||||
| @ -14,7 +14,7 @@ const ApiPage = ({ history }) => { | ||||
|                 condition={isAdmin()} | ||||
|                 show={<AdminMenu history={history} />} | ||||
|             /> | ||||
|             <ApiKeyList /> | ||||
|             <ApiTokenList location={location} /> | ||||
|         </div> | ||||
|     ); | ||||
| }; | ||||
| @ -22,6 +22,7 @@ const ApiPage = ({ history }) => { | ||||
| ApiPage.propTypes = { | ||||
|     match: PropTypes.object.isRequired, | ||||
|     history: PropTypes.object.isRequired, | ||||
|     location: PropTypes.object.isRequired, | ||||
| }; | ||||
| 
 | ||||
| export default ApiPage; | ||||
|  | ||||
| @ -1,40 +0,0 @@ | ||||
| import api from './api'; | ||||
| import { dispatchError } from '../util'; | ||||
| export const RECIEVE_KEYS = 'RECIEVE_KEYS'; | ||||
| export const ERROR_FETCH_KEYS = 'ERROR_FETCH_KEYS'; | ||||
| export const REMOVE_KEY = 'REMOVE_KEY'; | ||||
| export const REMOVE_KEY_ERROR = 'REMOVE_KEY_ERROR'; | ||||
| export const ADD_KEY = 'ADD_KEY'; | ||||
| export const ADD_KEY_ERROR = 'ADD_KEY_ERROR'; | ||||
| 
 | ||||
| const debug = require('debug')('unleash:e-api-admin-actions'); | ||||
| 
 | ||||
| export function fetchApiKeys() { | ||||
|     debug('Start fetching api-keys'); | ||||
|     return dispatch => | ||||
|         api | ||||
|             .fetchAll() | ||||
|             .then(value => | ||||
|                 dispatch({ | ||||
|                     type: RECIEVE_KEYS, | ||||
|                     tokens: value.tokens, | ||||
|                 }) | ||||
|             ) | ||||
|             .catch(dispatchError(dispatch, ERROR_FETCH_KEYS)); | ||||
| } | ||||
| 
 | ||||
| export function removeKey(secret) { | ||||
|     return dispatch => | ||||
|         api | ||||
|             .remove(secret) | ||||
|             .then(() => dispatch({ type: REMOVE_KEY, secret })) | ||||
|             .catch(dispatchError(dispatch, REMOVE_KEY)); | ||||
| } | ||||
| 
 | ||||
| export function addKey(data) { | ||||
|     return dispatch => | ||||
|         api | ||||
|             .create(data) | ||||
|             .then(newToken => dispatch({ type: ADD_KEY, token: newToken })) | ||||
|             .catch(dispatchError(dispatch, ADD_KEY_ERROR)); | ||||
| } | ||||
| @ -1,35 +0,0 @@ | ||||
| import { formatApiPath } from '../../utils/format-path'; | ||||
| import { throwIfNotSuccess, headers } from '../api-helper'; | ||||
| 
 | ||||
| const URI = formatApiPath('api/admin/api-tokens'); | ||||
| 
 | ||||
| function fetchAll() { | ||||
|     return fetch(URI, { headers, credentials: 'include' }) | ||||
|         .then(throwIfNotSuccess) | ||||
|         .then(response => response.json()); | ||||
| } | ||||
| 
 | ||||
| function create(data) { | ||||
|     return fetch(URI, { | ||||
|         method: 'POST', | ||||
|         headers, | ||||
|         body: JSON.stringify(data), | ||||
|         credentials: 'include', | ||||
|     }) | ||||
|         .then(throwIfNotSuccess) | ||||
|         .then(response => response.json()); | ||||
| } | ||||
| 
 | ||||
| function remove(key) { | ||||
|     return fetch(`${URI}/${key}`, { | ||||
|         method: 'DELETE', | ||||
|         headers, | ||||
|         credentials: 'include', | ||||
|     }).then(throwIfNotSuccess); | ||||
| } | ||||
| 
 | ||||
| export default { | ||||
|     fetchAll, | ||||
|     create, | ||||
|     remove, | ||||
| }; | ||||
| @ -1,17 +0,0 @@ | ||||
| import { List } from 'immutable'; | ||||
| import { RECIEVE_KEYS, ADD_KEY, REMOVE_KEY } from './actions'; | ||||
| 
 | ||||
| const store = (state = new List(), action) => { | ||||
|     switch (action.type) { | ||||
|         case RECIEVE_KEYS: | ||||
|             return new List(action.tokens); | ||||
|         case ADD_KEY: | ||||
|             return state.push(action.token); | ||||
|         case REMOVE_KEY: | ||||
|             return state.filter(v => v.secret !== action.secret); | ||||
|         default: | ||||
|             return state; | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| export default store; | ||||
| @ -16,7 +16,6 @@ import uiConfig from './ui-config'; | ||||
| import context from './context'; | ||||
| import projects from './project'; | ||||
| import addons from './addons'; | ||||
| import apiAdmin from './e-api-admin'; | ||||
| import authAdmin from './e-admin-auth'; | ||||
| import apiCalls from './api-calls'; | ||||
| import invoiceAdmin from './e-admin-invoice'; | ||||
| @ -40,7 +39,6 @@ const unleashStore = combineReducers({ | ||||
|     context, | ||||
|     projects, | ||||
|     addons, | ||||
|     apiAdmin, | ||||
|     authAdmin, | ||||
|     apiCalls, | ||||
|     invoiceAdmin, | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user