Commit 69d6c7b4 authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into 422-errors

parents cf84a4b9 c0f406be
......@@ -44,6 +44,8 @@ NEXT_PUBLIC_AD_ADBUTLER_ON=__PLACEHOLDER_FORNEXT_PUBLIC_AD_ADBUTLER_ON__
NEXT_PUBLIC_API_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_HOST__
NEXT_PUBLIC_API_BASE_PATH=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_BASE_PATH__
NEXT_PUBLIC_STATS_API_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_STATS_API_HOST__
NEXT_PUBLIC_API_PROTOCOL=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_PROTOCOL__
NEXT_PUBLIC_API_PORT=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_PORT__
# external services config
NEXT_PUBLIC_SENTRY_DSN=__PLACEHOLDER_FOR_NEXT_PUBLIC_SENTRY_DSN__
......
......@@ -27,6 +27,14 @@ const baseUrl = [
].filter(Boolean).join('');
const authUrl = getEnvValue(process.env.NEXT_PUBLIC_AUTH_URL) || baseUrl;
const apiHost = getEnvValue(process.env.NEXT_PUBLIC_API_HOST);
const apiSchema = getEnvValue(process.env.NEXT_PUBLIC_API_PROTOCOL) || 'https';
const apiPort = getEnvValue(process.env.NEXT_PUBLIC_API_PORT);
const apiEndpoint = apiHost ? [
apiSchema || 'https',
'://',
apiHost,
apiPort && ':' + apiPort,
].filter(Boolean).join('') : 'https://blockscout.com';
const logoutUrl = (() => {
try {
......@@ -91,7 +99,7 @@ const config = Object.freeze({
},
api: {
host: apiHost,
endpoint: apiHost ? `https://${ apiHost }` : 'https://blockscout.com',
endpoint: apiEndpoint,
socket: apiHost ? `wss://${ apiHost }` : 'wss://blockscout.com',
basePath: stripTrailingSlash(getEnvValue(process.env.NEXT_PUBLIC_API_BASE_PATH) || ''),
},
......
This diff is collapsed.
......@@ -2,10 +2,11 @@ import { compile } from 'path-to-regexp';
import appConfig from 'configs/app/config';
import type { ApiResource } from './resources';
import { RESOURCES } from './resources';
import type { ApiResource, ResourceName } from './resources';
export default function buildUrl(
resource: ApiResource,
_resource: ApiResource | ResourceName,
pathParams?: Record<string, string | undefined>,
queryParams?: Record<string, string | number | undefined>,
) {
......@@ -22,6 +23,7 @@ export default function buildUrl(
// will need to change the condition if there are more micro services that need authentication and DB state changes
const needProxy = appConfig.host !== appConfig.api.host;
const resource: ApiResource = typeof _resource === 'string' ? RESOURCES[_resource] : _resource;
const baseUrl = needProxy ? appConfig.baseUrl : (resource.endpoint || appConfig.api.endpoint);
const basePath = resource.basePath !== undefined ? resource.basePath : appConfig.api.basePath;
const path = needProxy ? '/node-api/proxy' + basePath + resource.path : basePath + resource.path;
......
import { compile } from 'path-to-regexp';
import appConfig from 'configs/app/config';
import { RESOURCES } from './resources';
import type { ApiResource, ResourceName } from './resources';
export default function buildUrlNode(
_resource: ApiResource | ResourceName,
pathParams?: Record<string, string | undefined>,
queryParams?: Record<string, string | number | undefined>,
) {
const resource: ApiResource = typeof _resource === 'string' ? RESOURCES[_resource] : _resource;
const baseUrl = resource.endpoint || appConfig.api.endpoint;
const basePath = resource.basePath !== undefined ? resource.basePath : appConfig.api.basePath;
const path = basePath + resource.path;
const url = new URL(compile(path)(pathParams), baseUrl);
queryParams && Object.entries(queryParams).forEach(([ key, value ]) => {
value && url.searchParams.append(key, String(value));
});
return url.toString();
}
import type { NextApiRequest } from 'next';
export default function getSearchParams(req: NextApiRequest) {
const searchParams: Record<string, string> = {};
Object.entries(req.query).forEach(([ key, value ]) => {
searchParams[key] = Array.isArray(value) ? value.join(',') : (value || '');
});
return new URLSearchParams(searchParams).toString();
}
import type { NextApiRequest } from 'next';
import appConfig from 'configs/app/config';
export default function getUrlWithNetwork(_req: NextApiRequest, path: string) {
return [
appConfig.api.basePath,
path,
]
.filter((segment) => segment !== '' && segment !== '/')
.join('');
}
import type { NextApiRequest, NextApiResponse } from 'next';
import fetchFactory from 'lib/api/fetch';
import getUrlWithNetwork from 'lib/api/getUrlWithNetwork';
import { httpLogger } from 'lib/api/logger';
type Methods = 'GET' | 'POST' | 'PUT' | 'DELETE';
export default function createHandler(getUrl: (_req: NextApiRequest) => string, allowedMethods: Array<Methods>, apiEndpoint?: string) {
const handler = async(_req: NextApiRequest, res: NextApiResponse) => {
httpLogger(_req, res);
if (!_req.method || !allowedMethods.includes(_req.method as Methods)) {
res.setHeader('Allow', allowedMethods);
res.status(405).end(`Method ${ _req.method } Not Allowed`);
return;
}
const isBodyDisallowed = _req.method === 'GET' || _req.method === 'HEAD';
const url = apiEndpoint ? `/api${ getUrl(_req) }` : getUrlWithNetwork(_req, `/api${ getUrl(_req) }`);
const fetch = fetchFactory(_req, apiEndpoint);
const response = await fetch(url, {
method: _req.method,
body: isBodyDisallowed ? undefined : _req.body,
});
if (response.status === 200) {
const data = await response.json();
res.status(200).json(data);
return;
}
let responseError;
const defaultError = { statusText: response.statusText, status: response.status };
try {
const error = await response.json() as { errors: unknown };
responseError = error?.errors || defaultError;
} catch (error) {
responseError = defaultError;
}
httpLogger.logger.error({ err: responseError, url: _req.url });
res.status(500).json(responseError);
};
return handler;
}
......@@ -2,25 +2,20 @@ import type { NextApiRequest } from 'next';
import type { RequestInit, Response } from 'node-fetch';
import nodeFetch from 'node-fetch';
import appConfig from 'configs/app/config';
import { httpLogger } from 'lib/api/logger';
import * as cookies from 'lib/cookies';
// first arg can be only a string
// FIXME migrate to RequestInfo later if needed
export default function fetchFactory(
_req: NextApiRequest,
apiEndpoint: string = appConfig.api.endpoint,
) {
return function fetch(path: string, init?: RequestInit): Promise<Response> {
const csrfToken = _req.headers['x-csrf-token']?.toString();
// first arg can be only a string
// FIXME migrate to RequestInfo later if needed
return function fetch(url: string, init?: RequestInit): Promise<Response> {
const headers = {
accept: 'application/json',
'content-type': 'application/json',
cookie: `${ cookies.NAMES.API_TOKEN }=${ _req.cookies[cookies.NAMES.API_TOKEN] }`,
...(csrfToken ? { 'x-csrf-token': csrfToken } : {}),
};
const url = new URL(path, apiEndpoint);
httpLogger.logger.info({
message: 'Trying to call API',
......@@ -28,7 +23,7 @@ export default function fetchFactory(
req: _req,
});
return nodeFetch(url.toString(), {
return nodeFetch(url, {
headers,
...init,
});
......
......@@ -35,6 +35,9 @@ export interface ApiResource {
export const RESOURCES = {
// ACCOUNT
csrf: {
path: '/api/account/v1/get_csrf',
},
user_info: {
path: '/api/account/v1/user/info',
},
......@@ -198,7 +201,6 @@ export type ResourcePaginationKey<R extends ResourceName> = typeof RESOURCES[R]
export const resourceKey = (x: keyof typeof RESOURCES) => x;
export interface ResourceError<T = unknown> {
error?: T;
payload?: T;
status: Response['status'];
statusText: Response['statusText'];
......
......@@ -5,6 +5,7 @@ import React from 'react';
import type { CsrfData } from 'types/client/account';
import type { ResourceError } from 'lib/api/resources';
import { getResourceKey } from 'lib/api/useApiQuery';
export interface Params {
method?: RequestInit['method'];
......@@ -15,7 +16,7 @@ export interface Params {
export default function useFetch() {
const queryClient = useQueryClient();
const { token } = queryClient.getQueryData<CsrfData>([ 'csrf' ]) || {};
const { token } = queryClient.getQueryData<CsrfData>(getResourceKey('csrf')) || {};
return React.useCallback(<Success, Error>(path: string, params?: Params): Promise<Success | ResourceError<Error>> => {
const reqParams = {
......@@ -42,8 +43,6 @@ export default function useFetch() {
return response.json().then(
(jsonError) => Promise.reject({
// DEPRECATED
error: jsonError as Error,
payload: jsonError as Error,
status: response.status,
statusText: response.statusText,
......
......@@ -9,6 +9,7 @@
},
"scripts": {
"dev": "next dev",
"dev:local": "./node_modules/.bin/dotenv -e ./configs/envs/.env.localhost -e ./configs/envs/.env.secrets next dev | ./node_modules/.bin/pino-pretty",
"dev:poa_core": "./node_modules/.bin/dotenv -e ./configs/envs/.env.poa_core -e ./configs/envs/.env.common -e ./configs/envs/.env.secrets next dev | ./node_modules/.bin/pino-pretty",
"dev:goerli": "./node_modules/.bin/dotenv -e ./configs/envs/.env.goerli -e ./configs/envs/.env.common -e ./configs/envs/.env.secrets next dev | ./node_modules/.bin/pino-pretty",
"build": "next build",
......
......@@ -23,7 +23,7 @@ function MyApp({ Component, pageProps }: AppProps) {
refetchOnWindowFocus: false,
retry: (failureCount, _error) => {
const error = _error as ResourceError<{ status: number }>;
const status = error?.status || error?.error?.status;
const status = error?.status || error?.payload?.status;
if (status && status >= 400 && status < 500) {
// don't do retry for client error responses
return false;
......
import type { NextApiRequest, NextApiResponse } from 'next';
import fetchFactory from 'lib/api/fetch';
import getUrlWithNetwork from 'lib/api/getUrlWithNetwork';
import buildUrlNode from 'lib/api/buildUrlNode';
import { httpLogger } from 'lib/api/logger';
import fetchFactory from 'lib/api/nodeFetch';
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);
const url = buildUrlNode('csrf');
const response = await fetchFactory(_req)(url);
if (response.status === 200) {
const token = response.headers.get('x-bs-account-csrf');
......
......@@ -2,7 +2,8 @@ import _pick from 'lodash/pick';
import _pickBy from 'lodash/pickBy';
import type { NextApiRequest, NextApiResponse } from 'next';
import fetchFactory from 'lib/api/fetch';
import appConfig from 'configs/app/config';
import fetchFactory from 'lib/api/nodeFetch';
const handler = async(_req: NextApiRequest, res: NextApiResponse) => {
if (!_req.url) {
......@@ -10,8 +11,12 @@ const handler = async(_req: NextApiRequest, res: NextApiResponse) => {
return;
}
const response = await fetchFactory(_req, _req.headers['x-endpoint']?.toString())(
const url = new URL(
_req.url.replace(/^\/node-api\/proxy/, ''),
_req.headers['x-endpoint']?.toString() || appConfig.api.endpoint,
);
const response = await fetchFactory(_req)(
url.toString(),
_pickBy(_pick(_req, [ 'body', 'method' ]), Boolean),
);
......
......@@ -57,11 +57,11 @@ const BlockDetails = () => {
}
if (isError) {
if (error?.error?.status === 404) {
if (error?.payload?.status === 404) {
return <span>This block has not been processed yet.</span>;
}
if (error?.error?.status === 422) {
if (error?.payload?.status === 422) {
throw Error('Invalid block number', { cause: error as unknown as Error });
}
......
......@@ -2,8 +2,11 @@ import { Flex } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React from 'react';
import appConfig from 'configs/app/config';
import buildUrl from 'lib/api/buildUrl';
import { getResourceKey } from 'lib/api/useApiQuery';
import * as cookies from 'lib/cookies';
// import useFetch from 'lib/hooks/useFetch';
import useFetch from 'lib/hooks/useFetch';
import AppError from 'ui/shared/AppError/AppError';
import ErrorBoundary from 'ui/shared/ErrorBoundary';
import ErrorInvalidTxHash from 'ui/shared/ErrorInvalidTxHash';
......@@ -24,12 +27,12 @@ const Page = ({
hideMobileHeaderOnScrollDown,
isHomePage,
}: Props) => {
// const customFetch = useFetch();
useQuery([ 'csrf' ], async() => {
// const nodeApiResponse = await customFetch('/node-api/csrf');
const apiResponse = await fetch('https://blockscout-main.test.aws-k8s.blockscout.com/api/account/v1/get_csrf', { credentials: 'include' });
const nodeApiFetch = useFetch();
useQuery(getResourceKey('csrf'), async() => {
if (appConfig.host === appConfig.api.host) {
const url = buildUrl('csrf');
const apiResponse = await fetch(url, { credentials: 'include' });
const csrfFromHeader = apiResponse.headers.get('x-bs-account-csrf');
// eslint-disable-next-line no-console
console.log('>>> RESPONSE HEADERS <<<');
......@@ -40,6 +43,9 @@ const Page = ({
} ]);
return csrfFromHeader ? { token: csrfFromHeader } : undefined;
}
return nodeApiFetch('/node-api/csrf');
}, {
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