Skip to content

Commit 9c1da48

Browse files
authored
Migrate from cookies to headers for scam tokens and bypassing rate li… (#3118)
Migrate from cookies to headers for scam tokens and bypassing rate limits Resolves #3092
1 parent ad45445 commit 9c1da48

File tree

8 files changed

+59
-38
lines changed

8 files changed

+59
-38
lines changed

configs/app/app.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ const baseUrl = [
1010
appPort && ':' + appPort,
1111
].filter(Boolean).join('');
1212
const isDev = getEnvValue('NEXT_PUBLIC_APP_ENV') === 'development';
13+
const isReview = getEnvValue('NEXT_PUBLIC_APP_ENV') === 'review';
1314
const isPw = getEnvValue('NEXT_PUBLIC_APP_INSTANCE') === 'pw';
1415
const spriteHash = getEnvValue('NEXT_PUBLIC_ICON_SPRITE_HASH');
1516

1617
const app = Object.freeze({
1718
isDev,
19+
isReview,
1820
isPw,
1921
protocol: appSchema,
2022
host: appHost,

lib/api/isNeedProxy.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import config from 'configs/app';
22

3-
// FIXME
4-
// I was not able to figure out how to send CORS with credentials from localhost
5-
// unsuccessfully tried different ways, even custom local dev domain
6-
// so for local development we have to use next.js api as proxy server
73
export default function isNeedProxy() {
84
if (config.app.useProxy) {
95
return true;
106
}
117

12-
return config.app.host === 'localhost' && config.app.host !== config.apis.general.host;
8+
// ONLY FOR DEV OR REVIEW ENVIRONMENTS (NOT PRODUCTION)
9+
// because we have some resources that require credentials, we need to use the proxy
10+
// otherwise the cross-origin requests with "credentials: include" will be blocked
11+
return config.app.isDev || config.app.isReview;
1312
}

lib/api/useApiFetch.tsx

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,9 @@ import { useQueryClient } from '@tanstack/react-query';
22
import { omit, pickBy } from 'es-toolkit';
33
import React from 'react';
44

5-
import type { ApiName } from './types';
65
import type { CsrfData } from 'types/client/account';
76
import type { ChainConfig } from 'types/multichain';
87

9-
import config from 'configs/app';
108
import isBodyAllowed from 'lib/api/isBodyAllowed';
119
import isNeedProxy from 'lib/api/isNeedProxy';
1210
import { getResourceKey } from 'lib/api/useApiQuery';
@@ -18,22 +16,6 @@ import buildUrl from './buildUrl';
1816
import getResourceParams from './getResourceParams';
1917
import type { ResourceName, ResourcePathParams } from './resources';
2018

21-
function needCredentials(apiName: ApiName) {
22-
if (![ 'general' ].includes(apiName)) {
23-
return false;
24-
}
25-
26-
// currently, the cookies are used only for the following features
27-
if (
28-
config.features.account.isEnabled ||
29-
config.UI.views.token.hideScamTokensEnabled
30-
) {
31-
return true;
32-
}
33-
34-
return false;
35-
}
36-
3719
export interface Params<R extends ResourceName> {
3820
pathParams?: ResourcePathParams<R>;
3921
queryParams?: Record<string, string | Array<string> | number | boolean | undefined | null>;
@@ -53,21 +35,46 @@ export default function useApiFetch() {
5335
{ pathParams, queryParams, fetchParams, logError, chain }: Params<R> = {},
5436
) => {
5537
const apiToken = cookies.get(cookies.NAMES.API_TOKEN);
38+
const apiTempToken = cookies.get(cookies.NAMES.API_TEMP_TOKEN);
39+
const showScamTokens = cookies.get(cookies.NAMES.SHOW_SCAM_TOKENS) === 'true';
40+
5641
const { api, apiName, resource } = getResourceParams(resourceName, chain);
5742
const url = buildUrl(resourceName, pathParams, queryParams, undefined, chain);
5843
const withBody = isBodyAllowed(fetchParams?.method);
5944
const headers = pickBy({
6045
'x-endpoint': isNeedProxy() ? api.endpoint : undefined,
6146
Authorization: [ 'admin', 'contractInfo' ].includes(apiName) ? apiToken : undefined,
62-
'x-csrf-token': withBody && csrfToken ? csrfToken : undefined,
47+
...(apiName === 'general' ? {
48+
'api-v2-temp-token': apiTempToken,
49+
'show-scam-tokens': showScamTokens ? 'true' : undefined,
50+
'x-csrf-token': withBody && csrfToken ? csrfToken : undefined,
51+
} : {}),
6352
...resource.headers,
6453
...fetchParams?.headers,
6554
}, Boolean) as HeadersInit;
6655

6756
return fetch<SuccessType, ErrorType>(
6857
url,
6958
{
70-
credentials: needCredentials(apiName) ? 'include' : 'same-origin',
59+
// Things to remember:
60+
//
61+
// A: Currently, we use only one API-related cookie, "_explorer_key," which is for the account feature.
62+
// We include credentials only for core API requests.
63+
// Note that some APIs may share the same origin with the core API, but they don't require credentials (e.g the Stats API).
64+
//
65+
// B: We cannot limit the routes for which credentials should be sent exclusively to the "/account/**" routes.
66+
// This is because the watchlist names and private tags preloading will not function on the API side.
67+
// Therefore, we include credentials for all core API routes.
68+
//
69+
// C: We cannot include credentials in cross-origin requests.
70+
// In this case, we must explicitly list all the origins allowed to make requests in the "Access-Control-Allow-Origin" header,
71+
// which is challenging for our devops and backend teams. Thus, we do not use the "include" option here.
72+
// And because of this, the account feature in cross-origin setup will not work.
73+
//
74+
// Considering all of the above, we use:
75+
// - The "same-origin" option for all core API requests
76+
// - The "omit" option for all other requests
77+
credentials: apiName === 'general' ? 'same-origin' : 'omit',
7178
headers,
7279
...(fetchParams ? omit(fetchParams, [ 'headers' ]) : {}),
7380
},

lib/cookies.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { isBrowser } from 'toolkit/utils/isBrowser';
55
export enum NAMES {
66
NAV_BAR_COLLAPSED = 'nav_bar_collapsed',
77
API_TOKEN = '_explorer_key',
8+
API_TEMP_TOKEN = 'api_temp_token',
89
REWARDS_API_TOKEN = 'rewards_api_token',
910
REWARDS_REFERRAL_CODE = 'rewards_ref_code',
1011
TXS_SORT = 'txs_sort',

nextjs/getServerSideProps/guards.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import type { Route } from 'nextjs-routes';
66
import type { Props } from 'nextjs/getServerSideProps/handlers';
77

88
import config from 'configs/app';
9-
import isNeedProxy from 'lib/api/isNeedProxy';
109

1110
export type Guard = (chainConfig: typeof config) => <Pathname extends Route['pathname'] = never>(context: GetServerSidePropsContext) =>
1211
Promise<GetServerSidePropsResult<Props<Pathname>> | undefined>;
@@ -159,8 +158,8 @@ export const dataAvailability: Guard = (chainConfig: typeof config) => async() =
159158
}
160159
};
161160

162-
export const login: Guard = () => async() => {
163-
if (!isNeedProxy()) {
161+
export const login: Guard = (chainConfig: typeof config) => async() => {
162+
if (!chainConfig.app.isReview && !chainConfig.app.isDev) {
164163
return {
165164
notFound: true,
166165
};

nextjs/utils/fetchProxy.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ export default function fetchFactory(
2727
'user-agent',
2828
'Authorization', // the old value, just in case
2929
'authorization', // Node.js automatically lowercases headers
30+
'show-scam-tokens',
31+
'api-v2-temp-token',
3032
// feature flags
3133
'updated-gas-oracle',
3234
]) as Record<string, string | undefined>,

pages/api/proxy.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const handler = async(nextReq: NextApiRequest, nextRes: NextApiResponse) => {
2828
'x-ratelimit-limit',
2929
'x-ratelimit-remaining',
3030
'x-ratelimit-reset',
31+
'api-v2-temp-token',
3132
];
3233

3334
HEADERS_TO_PROXY.forEach((header) => {

ui/shared/AppError/custom/AppErrorTooManyRequests.tsx

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ import React from 'react';
33

44
import config from 'configs/app';
55
import buildUrl from 'lib/api/buildUrl';
6-
import useFetch from 'lib/hooks/useFetch';
6+
import * as cookies from 'lib/cookies';
77
import { Button } from 'toolkit/chakra/button';
88
import { toaster } from 'toolkit/chakra/toaster';
9-
import { SECOND } from 'toolkit/utils/consts';
9+
import { DAY, SECOND } from 'toolkit/utils/consts';
1010
import { apos } from 'toolkit/utils/htmlEntities';
1111
import ReCaptcha from 'ui/shared/reCaptcha/ReCaptcha';
1212
import useReCaptcha from 'ui/shared/reCaptcha/useReCaptcha';
@@ -31,7 +31,6 @@ const AppErrorTooManyRequests = ({ bypassOptions, reset }: Props) => {
3131

3232
const [ timeLeft, setTimeLeft ] = React.useState(reset ? Math.ceil(Number(reset) / SECOND) : undefined);
3333

34-
const fetch = useFetch();
3534
const recaptcha = useReCaptcha();
3635

3736
const handleSubmit = React.useCallback(async() => {
@@ -42,17 +41,28 @@ const AppErrorTooManyRequests = ({ bypassOptions, reset }: Props) => {
4241
throw new Error('ReCaptcha is not solved');
4342
}
4443

45-
const url = buildUrl('general:api_v2_key');
44+
const url = buildUrl('general:api_v2_key', undefined, { in_header: true });
4645

47-
await fetch(url, {
46+
const response = await fetch(url, {
4847
method: 'POST',
49-
body: { recaptcha_response: token },
48+
body: JSON.stringify({ recaptcha_response: token }),
5049
headers: {
5150
'recaptcha-v2-response': token,
5251
},
53-
credentials: 'include',
54-
}, {
55-
resource: 'general:api_v2_key',
52+
});
53+
54+
if (!response.ok) {
55+
throw new Error(response.statusText);
56+
}
57+
58+
const apiTempToken = response.headers.get('api-v2-temp-token');
59+
60+
if (!apiTempToken) {
61+
throw new Error('API temp token is not found');
62+
}
63+
64+
cookies.set(cookies.NAMES.API_TEMP_TOKEN, apiTempToken, {
65+
expires: reset ? Number(reset) / DAY : 1 / 24,
5666
});
5767

5868
window.location.reload();
@@ -64,7 +74,7 @@ const AppErrorTooManyRequests = ({ bypassOptions, reset }: Props) => {
6474
type: 'error',
6575
});
6676
}
67-
}, [ recaptcha, fetch ]);
77+
}, [ recaptcha, reset ]);
6878

6979
React.useEffect(() => {
7080
if (reset === undefined) {

0 commit comments

Comments
 (0)