Commit a21554b1 authored by tom goriunov's avatar tom goriunov Committed by GitHub

Merge pull request #73 from blockscout/api-keys-backend

api keys: backend integration
parents e4dd9486 cb9ff16a
export const apiKey = [
{
token: '6fd12fe0-841c-4abf-ac2a-8c1b08dadf8e',
name: 'zapper.fi',
},
{
token: '057085a1-d2eb-4d8d-8b89-1dd9fba32071',
name: 'TenderlyBlaBlaName',
},
{
token: '057085a1-d2eb-4d8d-8b89-1dd9fba32071',
name: 'Application name',
},
];
export type TApiKey = Array<TApiKeyItem>
export type TApiKeyItem = {
token: string;
name: string;
}
import { ChakraProvider } from '@chakra-ui/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import type { AppProps } from 'next/app';
import React, { useState } from 'react';
......@@ -19,6 +20,7 @@ function MyApp({ Component, pageProps }: AppProps) {
<ChakraProvider theme={ theme }>
<Component { ...pageProps }/>
</ChakraProvider>
<ReactQueryDevtools/>
</QueryClientProvider>
);
}
......
import type { NextApiRequest } from 'next';
import handler from 'pages/api/utils/handler';
const getUrl = (req: NextApiRequest) => {
return `/account/v1/user/api_keys/${ req.query.id }`;
};
const apiKeysHandler = handler(getUrl, [ 'DELETE', 'PUT' ]);
export default apiKeysHandler;
import type { ApiKeys } from 'pages/api/types/account';
import handler from 'pages/api/utils/handler';
const apiKeysHandler = handler<ApiKeys>(() => '/account/v1/user/api_keys', [ 'GET', 'POST' ]);
export default apiKeysHandler;
......@@ -10,8 +10,8 @@ export interface AddressTag {
export type AddressTags = Array<AddressTag>
export interface ApiKey {
apiKey: string;
apiKeyName: string;
api_key: string;
name: string;
}
export type ApiKeys = Array<ApiKey>
......
......@@ -22,6 +22,7 @@ export default function handler<TRes>(getUrl: (_req: NextApiRequest) => string,
} else if (allowedMethods.includes('PUT') && _req.method === 'PUT') {
const response = await fetch(getUrl(_req), {
method: 'PUT',
body: _req.body,
});
const data = await response.json() as TRes;
......
......@@ -5,14 +5,16 @@ import {
FormLabel,
Input,
} from '@chakra-ui/react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import React, { useCallback, useEffect } from 'react';
import type { SubmitHandler, ControllerRenderProps } from 'react-hook-form';
import { useForm, Controller } from 'react-hook-form';
import type { TApiKeyItem } from 'data/apiKey';
import type { ApiKey, ApiKeys } from 'pages/api/types/account';
type Props = {
data?: TApiKeyItem;
data?: ApiKey;
onClose: () => void;
}
type Inputs = {
......@@ -23,16 +25,54 @@ type Inputs = {
// idk, maybe there is no limit
const NAME_MAX_LENGTH = 100;
const ApiKeyForm: React.FC<Props> = ({ data }) => {
const ApiKeyForm: React.FC<Props> = ({ data, onClose }) => {
const { control, handleSubmit, formState: { errors }, setValue } = useForm<Inputs>();
const queryClient = useQueryClient();
useEffect(() => {
setValue('token', data?.token || '');
setValue('token', data?.api_key || '');
setValue('name', data?.name || '');
}, [ setValue, data ]);
// eslint-disable-next-line no-console
const onSubmit: SubmitHandler<Inputs> = data => console.log(data);
const updateApiKey = (data: Inputs) => {
const body = JSON.stringify({ name: data.name });
if (!data.token) {
return fetch('/api/account/api-keys', { method: 'POST', body });
}
return fetch(`/api/account/api-keys/${ data.token }`, { method: 'PUT', body });
};
const mutation = useMutation(updateApiKey, {
onSuccess: async(data) => {
const response: ApiKey = await data.json();
queryClient.setQueryData([ 'api-keys' ], (prevData: ApiKeys | undefined) => {
const isExisting = prevData && prevData.some((item) => item.api_key === response.api_key);
if (isExisting) {
return prevData.map((item) => {
if (item.api_key === response.api_key) {
return response;
}
return item;
});
}
return [ ...(prevData || []), response ];
});
onClose();
},
// eslint-disable-next-line no-console
onError: console.error,
});
const onSubmit: SubmitHandler<Inputs> = useCallback((data) => {
mutation.mutate(data);
}, [ mutation ]);
const renderTokenInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'token'>}) => {
return (
......@@ -86,6 +126,7 @@ const ApiKeyForm: React.FC<Props> = ({ data }) => {
variant="primary"
onClick={ handleSubmit(onSubmit) }
disabled={ Object.keys(errors).length > 0 }
isLoading={ mutation.isLoading }
>
{ data ? 'Save' : 'Generate API key' }
</Button>
......
import React, { useCallback } from 'react';
import type { TApiKeyItem } from 'data/apiKey';
import type { ApiKey } from 'pages/api/types/account';
import FormModal from 'ui/shared/FormModal';
import ApiKeyForm from './ApiKeyForm';
......@@ -8,7 +9,7 @@ import ApiKeyForm from './ApiKeyForm';
type Props = {
isOpen: boolean;
onClose: () => void;
data?: TApiKeyItem;
data?: ApiKey;
}
const AddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
......@@ -16,10 +17,10 @@ const AddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const text = 'Add an application name to identify your API key. Click the button below to auto-generate the associated key.';
const renderForm = useCallback(() => {
return <ApiKeyForm data={ data }/>;
}, [ data ]);
return <ApiKeyForm data={ data } onClose={ onClose }/>;
}, [ data, onClose ]);
return (
<FormModal<TApiKeyItem>
<FormModal<ApiKey>
isOpen={ isOpen }
onClose={ onClose }
title={ title }
......
......@@ -8,14 +8,14 @@ import {
} from '@chakra-ui/react';
import React from 'react';
import type { TApiKey, TApiKeyItem } from 'data/apiKey';
import type { ApiKeys, ApiKey } from 'pages/api/types/account';
import ApiKeyTableItem from './ApiKeyTableItem';
interface Props {
data: TApiKey;
onEditClick: (data: TApiKeyItem) => void;
onDeleteClick: (data: TApiKeyItem) => void;
data: ApiKeys;
onEditClick: (item: ApiKey) => void;
onDeleteClick: (item: ApiKey) => void;
limit: number;
}
......@@ -33,7 +33,7 @@ const ApiKeyTable = ({ data, onDeleteClick, onEditClick, limit }: Props) => {
{ data.map((item) => (
<ApiKeyTableItem
item={ item }
key={ item.token }
key={ item.api_key }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
/>
......
......@@ -6,15 +6,16 @@ import {
} from '@chakra-ui/react';
import React, { useCallback } from 'react';
import type { TApiKeyItem } from 'data/apiKey';
import type { ApiKey } from 'pages/api/types/account';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import DeleteButton from 'ui/shared/DeleteButton';
import EditButton from 'ui/shared/EditButton';
interface Props {
item: TApiKeyItem;
onEditClick: (data: TApiKeyItem) => void;
onDeleteClick: (data: TApiKeyItem) => void;
item: ApiKey;
onEditClick: (item: ApiKey) => void;
onDeleteClick: (item: ApiKey) => void;
}
const WatchlistTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
......@@ -28,11 +29,11 @@ const WatchlistTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
}, [ item, onDeleteClick ]);
return (
<Tr alignItems="top" key={ item.token }>
<Tr alignItems="top" key={ item.api_key }>
<Td>
<HStack>
<Text fontSize="md" fontWeight={ 600 }>{ item.token }</Text>
<CopyToClipboard text={ item.token }/>
<Text fontSize="md" fontWeight={ 600 }>{ item.api_key }</Text>
<CopyToClipboard text={ item.api_key }/>
</HStack>
<Text fontSize="sm" marginTop={ 0.5 } variant="secondary">{ item.name }</Text>
</Td>
......
import { Text } from '@chakra-ui/react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import React, { useCallback } from 'react';
import type { ApiKey, ApiKeys } from 'pages/api/types/account';
import DeleteModal from 'ui/shared/DeleteModal';
type Props = {
isOpen: boolean;
onClose: () => void;
name?: string;
data: ApiKey;
}
const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, name }) => {
const onDelete = useCallback(() => {
const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const queryClient = useQueryClient();
const deleteApiKey = () => {
return fetch(`/api/account/api-keys/${ data.api_key }`, { method: 'DELETE' });
};
const mutation = useMutation(deleteApiKey, {
onSuccess: async() => {
queryClient.setQueryData([ 'api-keys' ], (prevData: ApiKeys | undefined) => {
return prevData?.filter((item) => item.api_key !== data.api_key);
});
onClose();
},
// eslint-disable-next-line no-console
console.log('delete', name);
}, [ name ]);
onError: console.error,
});
const onDelete = useCallback(() => {
mutation.mutate(data);
}, [ data, mutation ]);
const renderText = useCallback(() => {
return (
<Text display="flex">API key for<Text fontWeight="600" whiteSpace="pre">{ ` "${ name || 'name' }" ` }</Text>will be deleted</Text>
<Text display="flex">API key for<Text fontWeight="600" whiteSpace="pre">{ ` "${ data.name || 'name' }" ` }</Text>will be deleted</Text>
);
}, [ name ]);
}, [ data.name ]);
return (
<DeleteModal
isOpen={ isOpen }
......@@ -27,6 +49,7 @@ const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, name }) => {
onDelete={ onDelete }
title="Remove API key"
renderContent={ renderText }
pending={ mutation.isLoading }
/>
);
};
......
import { Box, Button, HStack, Link, Text, useDisclosure } from '@chakra-ui/react';
import { Box, Button, HStack, Link, Text, Spinner, useDisclosure } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react';
import type { TApiKeyItem } from 'data/apiKey';
import { apiKey } from 'data/apiKey';
import type { ApiKey, ApiKeys } from 'pages/api/types/account';
import { space } from 'lib/html-entities';
import ApiKeyModal from 'ui/apiKey/ApiKeyModal/ApiKeyModal';
import ApiKeyTable from 'ui/apiKey/ApiKeyTable/ApiKeyTable';
......@@ -12,14 +13,22 @@ import Page from 'ui/shared/Page/Page';
const DATA_LIMIT = 3;
const ApiKeys: React.FC = () => {
const ApiKeysPage: React.FC = () => {
const apiKeyModalProps = useDisclosure();
const deleteModalProps = useDisclosure();
const [ apiKeyModalData, setApiKeyModalData ] = useState<TApiKeyItem>();
const [ deleteModalData, setDeleteModalData ] = useState<string>();
const [ apiKeyModalData, setApiKeyModalData ] = useState<ApiKey>();
const [ deleteModalData, setDeleteModalData ] = useState<ApiKey>();
const { data, isLoading, isError } = useQuery<unknown, unknown, ApiKeys>([ 'api-keys' ], async() => {
const response = await fetch('/api/account/api-keys');
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
});
const onEditClick = useCallback((data: TApiKeyItem) => {
const onEditClick = useCallback((data: ApiKey) => {
setApiKeyModalData(data);
apiKeyModalProps.onOpen();
}, [ apiKeyModalProps ]);
......@@ -29,8 +38,8 @@ const ApiKeys: React.FC = () => {
apiKeyModalProps.onClose();
}, [ apiKeyModalProps ]);
const onDeleteClick = useCallback((data: TApiKeyItem) => {
setDeleteModalData(data.name);
const onDeleteClick = useCallback((data: ApiKey) => {
setDeleteModalData(data);
deleteModalProps.onOpen();
}, [ deleteModalProps ]);
......@@ -39,19 +48,17 @@ const ApiKeys: React.FC = () => {
deleteModalProps.onClose();
}, [ deleteModalProps ]);
const canAdd = apiKey.length < DATA_LIMIT;
const content = (() => {
if (isLoading || isError) {
return <Spinner/>;
}
return (
<Page>
<Box h="100%">
<AccountPageHeader text="API keys"/>
<Text marginBottom={ 12 }>
Create API keys to use for your RPC and EthRPC API requests. For more information, see { space }
<Link href="#">“How to use a Blockscout API key”</Link>.
</Text>
{ Boolean(apiKey.length) && (
const canAdd = data.length < DATA_LIMIT;
return (
<>
{ data.length > 0 && (
<ApiKeyTable
data={ apiKey }
data={ data }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
limit={ DATA_LIMIT }
......@@ -72,11 +79,24 @@ const ApiKeys: React.FC = () => {
</Text>
) }
</HStack>
</>
);
})();
return (
<Page>
<Box h="100%">
<AccountPageHeader text="API keys"/>
<Text marginBottom={ 12 }>
Create API keys to use for your RPC and EthRPC API requests. For more information, see { space }
<Link href="#">“How to use a Blockscout API key”</Link>.
</Text>
{ content }
</Box>
<ApiKeyModal { ...apiKeyModalProps } onClose={ onApiKeyModalClose } data={ apiKeyModalData }/>
<DeleteApiKeyModal { ...deleteModalProps } onClose={ onDeleteModalClose } name={ deleteModalData }/>
{ deleteModalData && <DeleteApiKeyModal { ...deleteModalProps } onClose={ onDeleteModalClose } data={ deleteModalData }/> }
</Page>
);
};
export default ApiKeys;
export default ApiKeysPage;
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment