From ad3e6a520b1676d01f13966111034acde2eeb868 Mon Sep 17 00:00:00 2001 From: Anton Sauchyk Date: Fri, 22 May 2026 17:26:15 +0200 Subject: [PATCH 1/3] SEC-359: Replace compare app with embedded Grafana dashboard Deprecates the in-app comparison flow (compare-single, compare-double, injection-start, injection-result-double) in favor of an iframe-embedded view of the existing Grafana Cloud public dashboards at chainstack.grafana.net. The 31 chain/region public dashboards already use target=_self on every nav link, so users can switch chains and regions without leaving compare.chainstack.com. Paired with the backend deprecation in performance-tool-backend (feature/SEC-359-deprecate-prod-backend) which scales the vulnerable /scenarios API to 0 replicas. Changes: - src/app/page.js: rewritten as a thin iframe shell with the Ethereum global dashboard as the entry point; users navigate to other chains/regions inside the iframe. - src/app/{compare-single,compare-double,injection-start, injection-result-double}/: deleted (now-dead routes). - next.config.js: drop wide-open /api/* CORS, add CSP that allowlists chainstack.grafana.net for frame-src, set frame-ancestors 'none', add HSTS / Referrer-Policy / nosniff. Redirect the four removed routes to / for legacy bookmarks. - src/app/store/store.js: emptied; no entities needed once the form flow is gone. - .env.sample: drop NEXT_PUBLIC_BACKEND_APP_URL. - src/components/{Bento,Faq,Performance,ProtocolIcon,ResultCard}: deleted. ResultCard imported removed store exports, the rest were only used by the deleted pages. Known limitation, tracked separately: chainstack.grafana.net currently returns X-Frame-Options: deny on public-dashboard responses, so the iframe will not render until Grafana Cloud support enables tenant-scoped frame-ancestors for https://compare.chainstack.com. Open a ticket referencing SEC-359 with that origin (plus any preview origin) before merging. The "Open dashboard in a new tab" fallback link below the iframe lets the page remain usable in the meantime. Build verified locally with npm run build (Next.js 14, route / prerendered as static, 64.2 kB). --- .env.sample | 7 +- next.config.js | 45 ++- src/app/compare-double/page.js | 401 ------------------- src/app/compare-single/page.js | 183 --------- src/app/injection-result-double/page.js | 284 ------------- src/app/injection-start/page.js | 95 ----- src/app/page.js | 269 ++----------- src/app/store/store.js | 178 +------- src/components/Bento/Bento.js | 114 ------ src/components/Faq/FaqAccordion.js | 70 ---- src/components/Faq/FaqBasic.js | 31 -- src/components/Faq/post.mdx | 11 - src/components/Icons/BarChartIcon.js | 22 - src/components/Icons/Customization.js | 22 - src/components/Icons/ExplainResultsIcon.js | 21 - src/components/Icons/Profiling.js | 22 - src/components/Performance/Compare.js | 54 --- src/components/Performance/Preformance.js | 50 --- src/components/ProtocolIcon/ProtocolIcon.js | 129 ------ src/components/ProtocolIcon/aptos.svg | 1 - src/components/ProtocolIcon/arbitrum.svg | 1 - src/components/ProtocolIcon/aurora.svg | 1 - src/components/ProtocolIcon/avalanche.svg | 1 - src/components/ProtocolIcon/base.svg | 1 - src/components/ProtocolIcon/bitcoin.svg | 1 - src/components/ProtocolIcon/bnb.svg | 1 - src/components/ProtocolIcon/cronos.svg | 1 - src/components/ProtocolIcon/ethereum.svg | 1 - src/components/ProtocolIcon/fantom.svg | 1 - src/components/ProtocolIcon/filecoin.svg | 1 - src/components/ProtocolIcon/fuse.svg | 1 - src/components/ProtocolIcon/gnosis.svg | 1 - src/components/ProtocolIcon/harmony.svg | 1 - src/components/ProtocolIcon/near.svg | 1 - src/components/ProtocolIcon/optimism.svg | 1 - src/components/ProtocolIcon/polygonPOS.svg | 1 - src/components/ProtocolIcon/polygonZkEvm.svg | 1 - src/components/ProtocolIcon/ronin.svg | 1 - src/components/ProtocolIcon/scroll.svg | 1 - src/components/ProtocolIcon/solana.svg | 1 - src/components/ProtocolIcon/starknet.svg | 1 - src/components/ProtocolIcon/tezos.svg | 1 - src/components/ProtocolIcon/zkSync.svg | 1 - src/components/ResultCard/ResultCard.js | 161 -------- 44 files changed, 68 insertions(+), 2125 deletions(-) delete mode 100644 src/app/compare-double/page.js delete mode 100644 src/app/compare-single/page.js delete mode 100644 src/app/injection-result-double/page.js delete mode 100644 src/app/injection-start/page.js delete mode 100644 src/components/Bento/Bento.js delete mode 100644 src/components/Faq/FaqAccordion.js delete mode 100644 src/components/Faq/FaqBasic.js delete mode 100644 src/components/Faq/post.mdx delete mode 100644 src/components/Icons/BarChartIcon.js delete mode 100644 src/components/Icons/Customization.js delete mode 100644 src/components/Icons/ExplainResultsIcon.js delete mode 100644 src/components/Icons/Profiling.js delete mode 100644 src/components/Performance/Compare.js delete mode 100644 src/components/Performance/Preformance.js delete mode 100644 src/components/ProtocolIcon/ProtocolIcon.js delete mode 100644 src/components/ProtocolIcon/aptos.svg delete mode 100644 src/components/ProtocolIcon/arbitrum.svg delete mode 100644 src/components/ProtocolIcon/aurora.svg delete mode 100644 src/components/ProtocolIcon/avalanche.svg delete mode 100644 src/components/ProtocolIcon/base.svg delete mode 100644 src/components/ProtocolIcon/bitcoin.svg delete mode 100644 src/components/ProtocolIcon/bnb.svg delete mode 100644 src/components/ProtocolIcon/cronos.svg delete mode 100644 src/components/ProtocolIcon/ethereum.svg delete mode 100644 src/components/ProtocolIcon/fantom.svg delete mode 100644 src/components/ProtocolIcon/filecoin.svg delete mode 100644 src/components/ProtocolIcon/fuse.svg delete mode 100644 src/components/ProtocolIcon/gnosis.svg delete mode 100644 src/components/ProtocolIcon/harmony.svg delete mode 100644 src/components/ProtocolIcon/near.svg delete mode 100644 src/components/ProtocolIcon/optimism.svg delete mode 100644 src/components/ProtocolIcon/polygonPOS.svg delete mode 100644 src/components/ProtocolIcon/polygonZkEvm.svg delete mode 100644 src/components/ProtocolIcon/ronin.svg delete mode 100644 src/components/ProtocolIcon/scroll.svg delete mode 100644 src/components/ProtocolIcon/solana.svg delete mode 100644 src/components/ProtocolIcon/starknet.svg delete mode 100644 src/components/ProtocolIcon/tezos.svg delete mode 100644 src/components/ProtocolIcon/zkSync.svg delete mode 100644 src/components/ResultCard/ResultCard.js diff --git a/.env.sample b/.env.sample index 28af456..ada836c 100644 --- a/.env.sample +++ b/.env.sample @@ -1,5 +1,2 @@ -# requires full url -NEXT_PUBLIC_BACKEND_APP_URL = 'http://0.0.0.0:8000/api/...' - -# client app public URL -NEXT_PUBLIC_CLIENT_DOMAIN = \ No newline at end of file +# client app public URL (used in og:image and og:url metadata) +NEXT_PUBLIC_CLIENT_DOMAIN = diff --git a/next.config.js b/next.config.js index 9099da9..2dca4da 100644 --- a/next.config.js +++ b/next.config.js @@ -1,32 +1,41 @@ /** @type {import('next').NextConfig} */ +const GRAFANA_DASHBOARD_URL = + 'https://chainstack.grafana.net/public-dashboards/65c0fcb02f994faf845d4ec095771bd0?orgId=1'; + +const cspDirectives = [ + "default-src 'self'", + "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.segment.com https://www.googletagmanager.com https://www.google-analytics.com", + "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com", + "font-src 'self' https://fonts.gstatic.com data:", + "img-src 'self' data: https:", + "frame-src https://chainstack.grafana.net", + "connect-src 'self' https://api.segment.io https://*.segment.io https://www.google-analytics.com", + "frame-ancestors 'none'", + "base-uri 'self'", + "form-action 'self'", +].join('; '); + const nextConfig = { async headers() { return [ { - source: '/api/:path*', + source: '/(.*)', headers: [ - { key: 'Access-Control-Allow-Credentials', value: 'true' }, - { key: 'Access-Control-Allow-Origin', value: '*' }, - { - key: 'Access-Control-Allow-Methods', - value: 'GET,DELETE,PATCH,POST,PUT', - }, - { - key: 'Access-Control-Allow-Headers', - value: - 'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version', - }, + { key: 'Content-Security-Policy', value: cspDirectives }, + { key: 'X-Content-Type-Options', value: 'nosniff' }, + { key: 'Referrer-Policy', value: 'no-referrer-when-downgrade' }, + { key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' }, ], }, ]; }, async redirects() { return [ - { - source: '/dashboard', - destination: 'https://chainstack.grafana.net/public-dashboards/65c0fcb02f994faf845d4ec095771bd0?orgId=1', - permanent: true - } + { source: '/compare-single', destination: '/', permanent: true }, + { source: '/compare-double', destination: '/', permanent: true }, + { source: '/injection-start', destination: '/', permanent: true }, + { source: '/injection-result-double', destination: '/', permanent: true }, + { source: '/dashboard', destination: GRAFANA_DASHBOARD_URL, permanent: true }, ]; }, reactStrictMode: false, @@ -39,4 +48,4 @@ const withMDX = require('@next/mdx')({ }, }); -module.exports = withMDX(nextConfig); \ No newline at end of file +module.exports = withMDX(nextConfig); diff --git a/src/app/compare-double/page.js b/src/app/compare-double/page.js deleted file mode 100644 index 64efc4a..0000000 --- a/src/app/compare-double/page.js +++ /dev/null @@ -1,401 +0,0 @@ -'use client'; -import { useEffect, useState } from 'react'; - -import Header from '@/components/Header/Header'; -import ResultCard from '@/components/ResultCard/ResultCard'; -import ExplainResultsIcon from '../../components/Icons/ExplainResultsIcon'; - -import { CodeIcon, ShareIcon } from '@iconicicons/react'; -import { Button, Loading, Badge } from '@lemonsqueezy/wedges'; -import { ClipboardIcon, CheckIcon, PlusIcon } from '@iconicicons/react'; -import { Chart } from 'react-google-charts'; -import Link from 'next/link'; - -import { - NODE_ENDPOINT, - NODE_ENDPOINT_2, - SET_NODE_ENDPOINT, - SET_NODE_ENDPOINT_2, - METHODS, - METHODS_2, - SET_METHOD_RESPONSE_DATA, - SET_METHOD_RESPONSE_DATA_2, - GET_METHODS_NAMES, -} from '../store/store'; - -import { useSearchParams } from 'next/navigation'; - -const Result = () => { - const searchParams = useSearchParams(); - let url1 = searchParams.get('url1'); - let url2 = searchParams.get('url2'); - - const nodeEndpoint = url1 ? url1 : NODE_ENDPOINT.use(); - const nodeEndpoint2 = url2 ? url2 : NODE_ENDPOINT_2.use(); - const methods = METHODS.use(); - const methods2 = METHODS_2.use(); - const methodsNames = GET_METHODS_NAMES.use(); - - const [copiedToClipboard, setCopiedToClipboard] = useState(false); - const [copiedToClipboard2, setCopiedToClipboard2] = useState(false); - const [compareLinkCopiedToClipboard, setCompareLinkCopiedToClipboard] = - useState(false); - const [chartData, setChartData] = useState(null); - const [chartData2, setChartData2] = useState(null); - const [explainIsDisabled, setExplainIsDisabled] = useState(false); - - const downloadJson = () => { - const jsonString = `data:text/json;chatset=utf-8,${encodeURIComponent( - JSON.stringify([ - { - endpoint: nodeEndpoint, - results: methods.map((method) => { - return { - method: method.method_used, - results: method.data, - }; - }), - }, - { - endpoint: nodeEndpoint2, - results: methods2.map((method) => { - return { - method: method.method_used, - results: method.data, - }; - }), - }, - ]) - )}`; - const link = document.createElement('a'); - link.href = jsonString; - link.download = 'data.json'; - - link.click(); - }; - - useEffect(() => { - if ( - methods.every((item) => Object.keys(item.data).length != 0) === true && - methods2.every((item) => Object.keys(item.data).length != 0) === true - ) { - let chart = [methods[1], methods2[1]]; - 2; - let chart2 = [methods[0], methods2[0]]; - - setChartData([ - [ - '', - ...chart.map((item, index) => { - return `Endpoint ${index + 1}`; - }), - ], - [ - '', - ...chart.map((item) => { - if ( - Object.keys(item.data).length != 0 && - item.data.hasOwnProperty('error') === false - ) { - return +item.data.blocks_per_seconds.toFixed(2); - } else { - return 0; - } - }), - ], - ]); - - setChartData2([ - [ - '', - ...chart2.map((item, index) => { - return `Endpoint ${index + 1}`; - }), - ], - [ - '', - ...chart2.map((item) => { - if ( - Object.keys(item.data).length != 0 && - item.data.hasOwnProperty('error') === false - ) { - return +item.data.blocks_per_seconds.toFixed(2); - } else { - return 0; - } - }), - ], - ]); - } - }, [methods, methods2]); - - let grid = 'grid grid-cols-2 gap-10'; - - return ( -
-
- - {/* URLS */} -
- {[ - { - endpoint: nodeEndpoint, - copied: copiedToClipboard, - clip(value) { - setCopiedToClipboard(value); - }, - }, - { - endpoint: nodeEndpoint2, - copied: copiedToClipboard2, - clip(value) { - setCopiedToClipboard2(value); - }, - }, - ].map((item, index) => { - return ( -
-
[{index + 1}]
-
- {item.endpoint} -
-
- ); - })} -
- {/* URLS */} - - {/* eth_getBlockByNumber */} -
- {[ - { - config: methods[0], - endpoint: nodeEndpoint, - setResponse: SET_METHOD_RESPONSE_DATA, - }, - { - config: methods2[0], - endpoint: nodeEndpoint2, - setResponse: SET_METHOD_RESPONSE_DATA_2, - }, - ].map((item, index) => { - return ( - - ); - })} -
- {/* eth_getBlockByNumber */} - - {/* eth_call */} -
- {[ - { - config: { ...methods[1] }, - endpoint: nodeEndpoint, - setResponse: SET_METHOD_RESPONSE_DATA, - }, - { - config: { ...methods2[1] }, - endpoint: nodeEndpoint2, - setResponse: SET_METHOD_RESPONSE_DATA_2, - }, - ].map((item, index) => { - return ( - - ); - })} -
- {/* eth_call */} - -
-

- Compare results -

- -
-
- } color="blue" stroke className=""> - {methodsNames[1]} - -
Blocks per second
-
- {!chartData ? ( -
- -
- ) : ( - - )} -
-
-
- } color="blue" stroke className=""> - {methodsNames[0]} - -
Blocks per second
-
- {!chartData2 ? ( -
- -
- ) : ( - - )} -
- -
- - - - - -
- - -

- Learn how Chainstack Compare works -
under the hood and why we built it ↗. -

-
-
-
- ); -}; - -export default Result; diff --git a/src/app/compare-single/page.js b/src/app/compare-single/page.js deleted file mode 100644 index 1f6b8d8..0000000 --- a/src/app/compare-single/page.js +++ /dev/null @@ -1,183 +0,0 @@ -'use client'; -import { useEffect, useState } from 'react'; - -import Header from '@/components/Header/Header'; -import ResultCard from '@/components/ResultCard/ResultCard'; -import ExplainResultsIcon from '../../components/Icons/ExplainResultsIcon'; - -import { Button, Loading } from '@lemonsqueezy/wedges'; -import { ClipboardIcon, CheckIcon, PlusIcon } from '@iconicicons/react'; -import { Chart } from 'react-google-charts'; -import Link from 'next/link'; - -import { - NODE_ENDPOINT, - METHODS, - SET_METHOD_RESPONSE_DATA, -} from '../store/store'; - -const Result = () => { - const nodeEndpoint = NODE_ENDPOINT.use(); - const methods = METHODS.use(); - - const [copiedToClipboard, setCopiedToClipboard] = useState(false); - const [chartData, setChartData] = useState(null); - const [explainIsDisabled, setExplainIsDisabled] = useState(true); - - const downloadJson = () => { - const jsonString = `data:text/json;chatset=utf-8,${encodeURIComponent( - JSON.stringify( - methods.map((method) => { - return { - method: method.method_used, - results: method.data, - }; - }) - ) - )}`; - const link = document.createElement('a'); - link.href = jsonString; - link.download = 'data.json'; - - link.click(); - }; - - useEffect(() => { - if (methods.every((item) => Object.keys(item.data).length != 0) === true) { - setChartData([ - ['', ...methods.map((item) => item.method_used)], - [ - '', - ...methods.map((item) => { - if ( - Object.keys(item.data).length != 0 && - item.data.hasOwnProperty('error') === false - ) { - return +item.data.blocks_per_seconds.toFixed(2); - } else { - return 0; - } - }), - ], - ]); - setExplainIsDisabled(false); - } - }, [methods]); - - return ( -
-
-
-
-
{nodeEndpoint}
-
- {methods.map((item, index) => { - return ( - - ); - })} - -
- {!chartData ? ( -
- -
- ) : ( - - )} -
- -
- - - - -
- - -

- Learn how Chainstack Compare works -
under the hood and why we built it ↗. -

-
-
-
- ); -}; - -export default Result; diff --git a/src/app/injection-result-double/page.js b/src/app/injection-result-double/page.js deleted file mode 100644 index 8db746d..0000000 --- a/src/app/injection-result-double/page.js +++ /dev/null @@ -1,284 +0,0 @@ -'use client'; -import { useEffect, useState } from 'react'; -import ResultCard from '@/components/ResultCard/ResultCard'; -import { Button } from '@lemonsqueezy/wedges'; -import { ClipboardIcon, CheckIcon } from '@iconicicons/react'; - -import Link from 'next/link'; -import Script from 'next/script'; - -import { - NODE_ENDPOINT, - NODE_ENDPOINT_2, - METHODS, - METHODS_2, - SET_METHOD_RESPONSE_DATA, - SET_METHOD_RESPONSE_DATA_2, - GET_METHODS_NAMES, -} from '../store/store'; - -const Result = () => { - const nodeEndpoint = NODE_ENDPOINT.use(); - const nodeEndpoint2 = NODE_ENDPOINT_2.use(); - const methods = METHODS.use(); - const methods2 = METHODS_2.use(); - const methodsNames = GET_METHODS_NAMES.use(); - - const [copiedToClipboard, setCopiedToClipboard] = useState(false); - const [copiedToClipboard2, setCopiedToClipboard2] = useState(false); - const [chartData, setChartData] = useState(null); - const [chartData2, setChartData2] = useState(null); - const [explainIsDisabled, setExplainIsDisabled] = useState(false); - - const downloadJson = () => { - const jsonString = `data:text/json;chatset=utf-8,${encodeURIComponent( - JSON.stringify([ - { - endpoint: nodeEndpoint, - results: methods.map((method) => { - return { - method: method.method_used, - results: method.data, - }; - }), - }, - { - endpoint: nodeEndpoint2, - results: methods2.map((method) => { - return { - method: method.method_used, - results: method.data, - }; - }), - }, - ]) - )}`; - const link = document.createElement('a'); - link.href = jsonString; - link.download = 'data.json'; - - link.click(); - }; - - useEffect(() => { - if ( - methods.every((item) => Object.keys(item.data).length != 0) === true && - methods2.every((item) => Object.keys(item.data).length != 0) === true - ) { - let chart = [methods[1], methods2[1]]; - let chart2 = [methods[0], methods2[0]]; - - setChartData([ - [ - '', - ...chart.map((item, index) => { - return `Endpoint ${index + 1}`; - }), - ], - [ - '', - ...chart.map((item) => { - if ( - Object.keys(item.data).length != 0 && - item.data.hasOwnProperty('error') === false - ) { - return +item.data.blocks_per_seconds.toFixed(2); - } else { - return 0; - } - }), - ], - ]); - - setChartData2([ - [ - '', - ...chart2.map((item, index) => { - return `Endpoint ${index + 1}`; - }), - ], - [ - '', - ...chart2.map((item) => { - if ( - Object.keys(item.data).length != 0 && - item.data.hasOwnProperty('error') === false - ) { - return +item.data.blocks_per_seconds.toFixed(2); - } else { - return 0; - } - }), - ], - ]); - } - }, [methods, methods2]); - - let grid = 'grid grid-cols-2 gap-10'; - - return ( - //
- <> -
- {/* URLS */} -
- {[ - { - endpoint: nodeEndpoint, - copied: copiedToClipboard, - clip(value) { - setCopiedToClipboard(value); - }, - }, - { - endpoint: nodeEndpoint2, - copied: copiedToClipboard2, - clip(value) { - setCopiedToClipboard2(value); - }, - }, - ].map((item, index) => { - return ( -
-
[{index + 1}]
-
- {item.endpoint} -
-
- ); - })} -
- {/* URLS */} - - {/* eth_getBlockByNumber */} -
- {[ - { - config: methods[0], - endpoint: nodeEndpoint, - setResponse: SET_METHOD_RESPONSE_DATA, - }, - { - config: methods2[0], - endpoint: nodeEndpoint2, - setResponse: SET_METHOD_RESPONSE_DATA_2, - }, - ].map((item, index) => { - return ( - - ); - })} -
- {/* eth_getBlockByNumber */} - - {/* eth_call */} -
- {[ - { - config: { ...methods[1] }, - endpoint: nodeEndpoint, - setResponse: SET_METHOD_RESPONSE_DATA, - }, - { - config: { ...methods2[1] }, - endpoint: nodeEndpoint2, - setResponse: SET_METHOD_RESPONSE_DATA_2, - }, - ].map((item, index) => { - return ( - - ); - })} -
- {/* eth_call */} - -
- {/*

- Full results -

*/} - - - - {/* - - */} -
-
-