Commit ea7e636d authored by tom's avatar tom

first two resources and hook

parent 67b6ddab
import appConfig from 'configs/app/config';
import { RESOURCES } from './resources';
export default function buildUrl(resource: keyof typeof RESOURCES, params?: Record<string, string>) {
const base = appConfig.host === 'localhost' ? appConfig.baseUrl : appConfig.api.endpoint;
const path = appConfig.host === 'localhost' ?
'/proxy' + appConfig.api.basePath + RESOURCES[resource].path :
appConfig.api.basePath + RESOURCES[resource].path;
const url = new URL(path, base);
params && Object.entries(params).forEach(([ key, value ]) => {
url.searchParams.append(key, value);
});
return url.toString();
}
import type { UserInfo } from 'types/api/account';
import type { CsrfData } from 'types/client/account';
export const RESOURCES = {
user_info: {
path: '/api/account/v1/user/info',
queryKey: 'user_info',
},
csrf: {
path: '/api/account/v1/get_csrf',
queryKey: 'csrf',
},
};
export const resourceKey = (x: keyof typeof RESOURCES) => x;
export type ResourcePayload<Q extends keyof typeof RESOURCES> =
Q extends 'user_info' ? UserInfo :
Q extends 'csrf' ? CsrfData : never;
export interface ResourceError {
error?: {
status?: number;
statusText?: string;
};
}
import type { UseQueryOptions } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import useFetch from 'lib/hooks/useFetch';
import buildUrl from './buildUrl';
import type { RESOURCES, ResourcePayload, ResourceError } from './resources';
export default function useApi<R extends keyof typeof RESOURCES>(
resource: R,
queryOptions?: Omit<UseQueryOptions<unknown, ResourceError, ResourcePayload<R>>, 'queryKey' | 'queryFn'>,
) {
const fetch = useFetch();
return useQuery<unknown, ResourceError, ResourcePayload<R>>([ resource ], async() => {
const url = buildUrl(resource);
return fetch(url, { credentials: 'include' });
}, queryOptions);
}
...@@ -4,6 +4,8 @@ import React from 'react'; ...@@ -4,6 +4,8 @@ import React from 'react';
import type { CsrfData } from 'types/client/account'; import type { CsrfData } from 'types/client/account';
import { resourceKey, RESOURCES } from 'lib/api/resources';
export interface ErrorType<T> { export interface ErrorType<T> {
error?: T; error?: T;
status: Response['status']; status: Response['status'];
...@@ -18,7 +20,7 @@ interface Params { ...@@ -18,7 +20,7 @@ interface Params {
export default function useFetch() { export default function useFetch() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { token } = queryClient.getQueryData<CsrfData>([ 'csrf' ]) || {}; const { token } = queryClient.getQueryData<CsrfData>([ resourceKey('csrf') ]) || {};
return React.useCallback(<Success, Error>(path: string, params?: Params): Promise<Success | ErrorType<Error>> => { return React.useCallback(<Success, Error>(path: string, params?: Params): Promise<Success | ErrorType<Error>> => {
const reqParams = { const reqParams = {
...@@ -29,6 +31,7 @@ export default function useFetch() { ...@@ -29,6 +31,7 @@ export default function useFetch() {
}; };
return fetch(path, reqParams).then(response => { return fetch(path, reqParams).then(response => {
if (!response.ok) { if (!response.ok) {
const error = { const error = {
status: response.status, status: response.status,
...@@ -48,6 +51,10 @@ export default function useFetch() { ...@@ -48,6 +51,10 @@ export default function useFetch() {
); );
} else { } else {
if (path.includes(RESOURCES.csrf.path)) {
return Promise.resolve({ token: response.headers.get('x-bs-account-csrf') } as Success);
}
return response.json() as Promise<Success>; return response.json() as Promise<Success>;
} }
}); });
......
import { useQuery } from '@tanstack/react-query'; import useApi from 'lib/api/useApi';
import * as cookies from 'lib/cookies';
import type { UserInfo } from 'types/api/account';
import { QueryKeys } from 'types/client/queries';
import appConfig from 'configs/app/config';
// import * as cookies from 'lib/cookies';
import useFetch from 'lib/hooks/useFetch';
interface Error {
error?: {
status?: number;
statusText?: string;
};
}
export default function useFetchProfileInfo() { export default function useFetchProfileInfo() {
const fetch = useFetch(); return useApi('user_info', {
return useQuery<unknown, Error, UserInfo>([ QueryKeys.profile ], async() => {
const url = new URL(`/proxy/poa/core/api/account/v1/user/info`, appConfig.baseUrl);
return fetch(url.toString(), { credentials: 'include' });
}, {
refetchOnMount: false, refetchOnMount: false,
// enabled: Boolean(cookies.get(cookies.NAMES.API_TOKEN)), enabled: Boolean(cookies.get(cookies.NAMES.API_TOKEN)),
}); });
} }
...@@ -23,7 +23,7 @@ function MyApp({ Component, pageProps }: AppProps) { ...@@ -23,7 +23,7 @@ function MyApp({ Component, pageProps }: AppProps) {
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
retry: (failureCount, _error) => { retry: (failureCount, _error) => {
const error = _error as ErrorType<{ status: number }>; const error = _error as ErrorType<{ status: number }>;
const status = error?.error?.status; const status = error?.status || error?.error?.status;
if (status && status >= 400 && status < 500) { if (status && status >= 400 && status < 500) {
// don't do retry for client error responses // don't do retry for client error responses
return false; return false;
......
import type { NextApiRequest, NextApiResponse } from 'next';
import fetchFactory from 'lib/api/fetch';
import getUrlWithNetwork from 'lib/api/getUrlWithNetwork';
import { httpLogger } from 'lib/api/logger';
export default async function csrfHandler(_req: NextApiRequest, res: NextApiResponse) {
httpLogger(_req, res);
const url = getUrlWithNetwork(_req, `/api/account/v1/get_csrf`);
const fetch = fetchFactory(_req);
const response = await fetch(url);
if (response.status === 200) {
const token = response.headers.get('x-bs-account-csrf');
res.status(200).json({ token });
return;
}
const responseError = { statusText: response.statusText, status: response.status };
httpLogger.logger.error({ err: responseError, url: _req.url });
res.status(500).json(responseError);
}
import handler from 'lib/api/handler';
const profileHandler = handler(() => '/account/v1/user/info', [ 'GET' ]);
export default profileHandler;
...@@ -15,6 +15,12 @@ const handler = async(_req: NextApiRequest, res: NextApiResponse) => { ...@@ -15,6 +15,12 @@ const handler = async(_req: NextApiRequest, res: NextApiResponse) => {
_pickBy(_pick(_req, [ 'body', 'method' ]), Boolean), _pickBy(_pick(_req, [ 'body', 'method' ]), Boolean),
); );
// don't think that we have to proxy all headers, so pick only necessary ones
[ 'x-bs-account-csrf' ].forEach((headerName) => {
const headerValue = response.headers.get(headerName);
headerValue && res.setHeader(headerName, headerValue);
});
res.status(response.status).send(response.body); res.status(response.status).send(response.body);
}; };
......
import { VStack, Textarea, Button, Alert, AlertTitle, AlertDescription, Link, Code, Flex, Box } from '@chakra-ui/react'; import { VStack, Textarea, Button, Alert, AlertTitle, AlertDescription, Code, Flex, Box } from '@chakra-ui/react';
import * as Sentry from '@sentry/react'; import * as Sentry from '@sentry/react';
import type { ChangeEvent } from 'react'; import type { ChangeEvent } from 'react';
import React from 'react'; import React from 'react';
...@@ -47,8 +47,6 @@ const Login = () => { ...@@ -47,8 +47,6 @@ const Login = () => {
}); });
}, [ toast, token ]); }, [ toast, token ]);
const prodUrl = 'https://blockscout.com/poa/core';
const handleNumIncrement = React.useCallback(() => { const handleNumIncrement = React.useCallback(() => {
for (let index = 0; index < 5; index++) { for (let index = 0; index < 5; index++) {
setNum(5); setNum(5);
...@@ -58,19 +56,15 @@ const Login = () => { ...@@ -58,19 +56,15 @@ const Login = () => {
return ( return (
<Page> <Page>
<VStack gap={ 4 } alignItems="flex-start" maxW="1000px"> <VStack gap={ 4 } alignItems="flex-start" maxW="1000px">
<PageTitle text="Vercel page"/> <PageTitle text="Login page 😂"/>
<Flex columnGap={ 2 } alignItems="center">
<Box w="50px" textAlign="center">{ num }</Box>
<Button onClick={ handleNumIncrement } size="sm">add</Button>
</Flex>
{ isFormVisible && ( { isFormVisible && (
<> <>
<Alert status="error" flexDirection="column" alignItems="flex-start"> <Alert status="error" flexDirection="column" alignItems="flex-start">
<AlertTitle fontSize="md"> <AlertTitle fontSize="md">
!!! Temporary solution for authentication !!! !!! Temporary solution for authentication on localhost !!!
</AlertTitle> </AlertTitle>
<AlertDescription mt={ 3 }> <AlertDescription mt={ 3 }>
To Sign in go to <Link href={ prodUrl } target="_blank">{ prodUrl }</Link> first, sign in there, copy obtained API token from cookie To Sign in go to production instance first, sign in there, copy obtained API token from cookie
<Code ml={ 1 }>{ cookies.NAMES.API_TOKEN }</Code> and paste it in the form below. After submitting the form you should be successfully <Code ml={ 1 }>{ cookies.NAMES.API_TOKEN }</Code> and paste it in the form below. After submitting the form you should be successfully
authenticated in current environment authenticated in current environment
</AlertDescription> </AlertDescription>
...@@ -80,6 +74,10 @@ const Login = () => { ...@@ -80,6 +74,10 @@ const Login = () => {
</> </>
) } ) }
<Button colorScheme="red" onClick={ checkSentry }>Check Sentry</Button> <Button colorScheme="red" onClick={ checkSentry }>Check Sentry</Button>
<Flex columnGap={ 2 } alignItems="center">
<Box w="50px" textAlign="center">{ num }</Box>
<Button onClick={ handleNumIncrement } size="sm">add</Button>
</Flex>
</VStack> </VStack>
</Page> </Page>
); );
......
import { Flex } from '@chakra-ui/react'; import { Flex } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import { QueryKeys } from 'types/client/queries'; import useApi from 'lib/api/useApi';
import * as cookies from 'lib/cookies'; import * as cookies from 'lib/cookies';
import useFetch from 'lib/hooks/useFetch';
import AppError from 'ui/shared/AppError/AppError'; import AppError from 'ui/shared/AppError/AppError';
import ErrorBoundary from 'ui/shared/ErrorBoundary'; import ErrorBoundary from 'ui/shared/ErrorBoundary';
import PageContent from 'ui/shared/Page/PageContent'; import PageContent from 'ui/shared/Page/PageContent';
...@@ -25,9 +22,7 @@ const Page = ({ ...@@ -25,9 +22,7 @@ const Page = ({
hideMobileHeaderOnScrollDown, hideMobileHeaderOnScrollDown,
isHomePage, isHomePage,
}: Props) => { }: Props) => {
const fetch = useFetch(); useApi('csrf', {
useQuery<unknown, unknown, unknown>([ QueryKeys.csrf ], async() => await fetch('/node-api/account/csrf'), {
enabled: Boolean(cookies.get(cookies.NAMES.API_TOKEN)), enabled: Boolean(cookies.get(cookies.NAMES.API_TOKEN)),
}); });
......
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