mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-24 01:18:01 +02: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