refactor(strapi): read the Content API token server-side via a proxy - #102
Conversation
NEXT_PUBLIC_STRAPI_API_TOKEN is inlined into client bundles, so the Strapi Content API token ships to every visitor. Move it server-side: - Add a same-origin proxy route /api/strapi/<path> that injects the bearer token on the server and forwards GETs to Strapi (path is pinned to /api/, so it can't reach admin/content-type-builder). - Client hooks/components fetch the proxy with no credentials. - Server components/utils read the token from the non-public STRAPI_API_TOKEN. The existing token must be treated as compromised and rotated; renaming the env var does not un-leak the value already shipped. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Warning Review limit reached
More reviews will be available in 37 minutes and 26 seconds. Learn how PR review limits work. Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file). ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (15)
📝 WalkthroughWalkthroughThe PR centralizes Strapi CMS authentication by renaming ChangesStrapi Token Centralization
Sequence Diagram(s)Client-side data fetch through proxy (new flow) sequenceDiagram
participant Browser as Browser Component
participant ProxyRoute as /api/strapi/[...path]
participant StrapiCMS as Strapi CMS
Browser->>ProxyRoute: GET /api/strapi/posts?filters=...
ProxyRoute->>ProxyRoute: validate STRAPI_API_TOKEN + NEXT_PUBLIC_STRAPI_URL
ProxyRoute->>ProxyRoute: allowlist path segment and query keys
ProxyRoute->>StrapiCMS: GET /api/posts?filters=... + Authorization: Bearer STRAPI_API_TOKEN
StrapiCMS-->>ProxyRoute: 200 + JSON body
ProxyRoute-->>Browser: 200 + JSON body (token never exposed)
Server-component direct fetch (new flow) sequenceDiagram
participant ServerPage as Server Component / Page
participant strapiServerFetch as strapiServerFetch (server-only)
participant StrapiCMS as Strapi CMS
ServerPage->>strapiServerFetch: strapiServerFetch("posts?slug=...", { revalidate: 3600 })
strapiServerFetch->>StrapiCMS: GET NEXT_PUBLIC_STRAPI_URL/api/posts?slug=... + Authorization: Bearer STRAPI_API_TOKEN
StrapiCMS-->>strapiServerFetch: Response
strapiServerFetch-->>ServerPage: Response (parsed by caller)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both public/favicon.ico and app/favicon.ico mapped to /favicon.ico, which Next.js rejects with a 500. Drop the legacy public/ copy; the byte-identical app-router metadata file continues to serve /favicon.ico. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Split the old _utils/query.ts into two seams so the API token never reaches the browser: - strapiServer.ts: server-only low-level fetch that attaches the bearer token, for RSC/page/layout data (supported-chains/protocols/wallets, discovers, privacy-policy, terms-of-service, products, etc.) - /api/strapi/[...path] proxy: same-origin relay for client-side reads (notification, posts, newsrooms, support-articles, faq), locked to an allowlist of collections and query params - getStrapiImageUrl.ts: extracted the pure image-URL helper, updated all importers Client components/hooks now fetch /api/strapi/* with no credentials. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
app/[lang]/(resources)/wallets/[slug]/page.tsx (1)
22-27:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winConsider checking
response.okbefore parsing JSON ingenerateMetadata.Unlike
fetchSupportedWallet(used in the page component), this directstrapiServerFetchcall doesn't verifyresponse.okbefore callingresponse.json(). If Strapi returns a non-2xx status with non-JSON content, this will throw an unhandled exception.🛡️ Suggested fix
const response = await strapiServerFetch(`supported-wallets?filters[slug][$eq]=${slug}&populate=*`) + if (!response.ok) { + return notFound() + } const data = await response.json() const wallet = data.data[0] as TSupportedWalletData - if (!wallet) { - return notFound() - }Or keep the
!walletcheck as a secondary guard if you prefer.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/`[lang]/(resources)/wallets/[slug]/page.tsx around lines 22 - 27, The strapiServerFetch call in the generateMetadata function does not verify that the HTTP response was successful before parsing JSON. Add a check for response.ok after the strapiServerFetch call and before calling response.json(), returning notFound() if the response status is not successful. This ensures that non-2xx responses with non-JSON content will not cause an unhandled exception. The existing check for !wallet can remain as a secondary guard to handle cases where the API returns a successful response but with empty data.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/`[lang]/(resources)/_utils/fetchUtils.ts:
- Around line 113-115: The slug parameter is being directly interpolated into
Strapi query strings without encoding, which allows reserved characters like &,
[, and ] to mutate the query semantics and return incorrect results. In
app/[lang]/(resources)/_utils/fetchUtils.ts, apply encodeURIComponent(slug) at
all slug interpolation sites in the fetchDiscoverBySlug function (lines 113-115,
166, 182, 198) where the slug is embedded in the query string. Additionally,
apply the same encoding pattern to the fetchSupportedChain and
fetchSupportedWallet functions at their respective slug interpolation points.
Also apply encodeURIComponent(slug) in
app/[lang]/(resources)/protocols/[slug]/page.tsx at line 23 where slug is used
in the query string.
- Around line 165-220: The fetch helper functions fetchSupportedProtocol,
fetchSupportedChain, fetchSupportedWallet, and fetchDiscovers lack error
handling for network failures and JSON parsing errors. Wrap the body of each
function in a try/catch block that catches any thrown errors and returns null on
failure, ensuring these functions maintain their promised contract of returning
null when they fail, preventing unhandled errors from bubbling up as 500
responses.
In `@app/`[lang]/(resources)/blog/(withNavigation)/categories/[category]/page.tsx:
- Around line 11-12: Route parameters are being directly interpolated into
Strapi query strings without proper encoding, enabling query parameter injection
attacks. You need to encode all user-controlled route parameters before
interpolating them into the query string. At
app/[lang]/(resources)/blog/(withNavigation)/categories/[category]/page.tsx#L11-L12,
encode the `category` parameter using `encodeURIComponent()` before
interpolating it into the strapiServerFetch query string. At
app/[lang]/(resources)/blog/(withNavigation)/tags/[tag]/page.tsx#L8-L9, apply
the same encoding to the `tag` parameter. At
app/[lang]/(resources)/blog/[slug]/layout.tsx#L15-L16, encode the `slug`
parameter before interpolation. At
app/[lang]/(resources)/chains/[slug]/page.tsx#L24-L24, encode the `slug`
parameter in the metadata fetch query. At
app/[lang]/(resources)/chains/[slug]/page.tsx#L66-L66, update the
`fetchSupportedChain(slug)` helper function to ensure it encodes the `slug`
parameter internally so the caller does not need to encode it separately. At
app/[lang]/(resources)/newsroom/(withNavigation)/categories/[category]/page.tsx#L10-L11,
encode the `category` parameter before interpolation. At
app/[lang]/(resources)/newsroom/(withNavigation)/tags/[tag]/page.tsx#L8-L9,
encode the `tag` parameter before interpolation. At
app/[lang]/(resources)/newsroom/[slug]/layout.tsx#L15-L16, encode the `slug`
parameter before interpolation.
In `@app/`[lang]/(terms)/_components/utils.ts:
- Around line 20-26: The strapiServerFetch call at
app/[lang]/(terms)/_components/utils.ts lines 20-26 and the similar call in
getTermsOfServiceItems at lines 43-49 both handle HTTP errors with the !res.ok
check but do not handle exceptions thrown during the fetch operation or JSON
parsing. Wrap both blocks with try/catch statements that catch any thrown errors
and return an empty array [] as the fallback, ensuring both functions
consistently return [] on all failure modes (HTTP errors, network errors, and
parse errors).
In `@app/api/strapi/`[...path]/route.ts:
- Around line 53-64: The upstream fetch call to the Strapi target is unguarded
and will throw on network failures, bypassing the route's error contract. Wrap
the fetch call to target (which includes the Authorization header with
strapiToken) in a try/catch block. In the catch handler, return a controlled
NextResponse with status 502 and a proper JSON error message to handle transport
failures gracefully and maintain consistent error handling.
---
Outside diff comments:
In `@app/`[lang]/(resources)/wallets/[slug]/page.tsx:
- Around line 22-27: The strapiServerFetch call in the generateMetadata function
does not verify that the HTTP response was successful before parsing JSON. Add a
check for response.ok after the strapiServerFetch call and before calling
response.json(), returning notFound() if the response status is not successful.
This ensures that non-2xx responses with non-JSON content will not cause an
unhandled exception. The existing check for !wallet can remain as a secondary
guard to handle cases where the API returns a successful response but with empty
data.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: cbebdced-f196-4c1b-9612-e7ace98f6b4b
⛔ Files ignored due to path filters (1)
public/favicon.icois excluded by!**/*.ico
📒 Files selected for processing (42)
.env.local.sampleapp/[lang]/(core-products)/_components/ProductHero.tsxapp/[lang]/(core-products)/_components/fetchUtils.tsapp/[lang]/(core-products)/defi-wallet/page.tsxapp/[lang]/(core-products)/earn/page.tsxapp/[lang]/(core-products)/mobile-app/page.tsxapp/[lang]/(core-products)/trade/page.tsxapp/[lang]/(resources)/_components/DiscoverFeature.tsxapp/[lang]/(resources)/_components/ResourceCard.tsxapp/[lang]/(resources)/_utils/fetchUtils.tsapp/[lang]/(resources)/blog/(withNavigation)/categories/[category]/page.tsxapp/[lang]/(resources)/blog/(withNavigation)/tags/[tag]/page.tsxapp/[lang]/(resources)/blog/[slug]/layout.tsxapp/[lang]/(resources)/chains/[slug]/page.tsxapp/[lang]/(resources)/discover/[slug]/page.tsxapp/[lang]/(resources)/discover/page.tsxapp/[lang]/(resources)/newsroom/(withNavigation)/categories/[category]/page.tsxapp/[lang]/(resources)/newsroom/(withNavigation)/tags/[tag]/page.tsxapp/[lang]/(resources)/newsroom/[slug]/layout.tsxapp/[lang]/(resources)/protocols/[slug]/page.tsxapp/[lang]/(resources)/wallets/[slug]/page.tsxapp/[lang]/(terms)/_components/utils.tsapp/[lang]/_components/BlogPost.tsxapp/[lang]/_components/ElementCard.tsxapp/[lang]/_components/NewsPost.tsxapp/[lang]/_components/Notification.tsxapp/[lang]/_components/Popup.tsxapp/[lang]/_components/StrapiFAQ.tsxapp/[lang]/_components/strapi/cards-row/Card.tsxapp/[lang]/_components/strapi/products/CarouselCard.tsxapp/[lang]/_components/strapi/products/ChainBubblesCard.tsxapp/[lang]/_components/strapi/products/Grid.tsxapp/[lang]/_components/strapi/products/GridDisplaced.tsxapp/[lang]/_components/strapi/products/GridLadder.tsxapp/[lang]/_components/strapi/templates/ChainFeatures.tsxapp/[lang]/_hooks/useFetchNewsroom.tsxapp/[lang]/_hooks/useFetchPosts.tsxapp/[lang]/_hooks/useFetchSupportArticles.tsapp/[lang]/_utils/getStrapiImageUrl.tsapp/[lang]/_utils/query.tsapp/[lang]/_utils/strapiServer.tsapp/api/strapi/[...path]/route.ts
💤 Files with no reviewable changes (1)
- app/[lang]/_utils/query.ts
Address CodeRabbit review on #102: - encodeURIComponent() on every user-controlled slug/category/tag before it is interpolated into a server-side Strapi query string, so reserved characters (& [ ] =) can't inject extra filters/fields - wrap the resource and terms fetch helpers in try/catch so transport/JSON failures return the helper's null/[] contract instead of bubbling as 500s (matches the existing fetchAllFAQs pattern); return ?? null on empty arrays - proxy route returns a controlled 502 when Strapi is unreachable instead of an unhandled fetch throw Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wrap slug/category/tag/type in encodeURIComponent before interpolating into the /api/strapi proxy query strings, closing the residual filter-injection gap (the proxy validates query keys, not values). Preserves the existing space->underscore substitution for category/tag/type. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Description
The Strapi Content API token is currently read in client components, so it gets inlined into the browser bundle. Move it server-side. No change to what users see.
app/api/strapi/[...path]/route.tsinjects the token on the server and forwards GETs to Strapi. The upstream path is pinned to/api/.useFetchPosts,useFetchNewsroom,useFetchSupportArticles,Notification) call/api/strapi/...with no auth header.STRAPI_API_TOKEN(wasNEXT_PUBLIC_STRAPI_API_TOKEN).Deploy note
Set
STRAPI_API_TOKEN(noNEXT_PUBLIC_prefix) in the deploy env before this ships — without it the proxy returns 500 and server-rendered content fails.Manual QA
STRAPI_API_TOKENandNEXT_PUBLIC_STRAPI_URL, thenyarn dev./api/strapi/...with no auth header.Verification
tsc --noEmitpasses locally. Didn't run end-to-end here (no Strapi in this environment).Summary by CodeRabbit
Refactor
Security