Skip to content

refactor(strapi): read the Content API token server-side via a proxy - #102

Merged
kaladinlight merged 6 commits into
developfrom
refactor/strapi-token-server-side
Jun 16, 2026
Merged

refactor(strapi): read the Content API token server-side via a proxy#102
kaladinlight merged 6 commits into
developfrom
refactor/strapi-token-server-side

Conversation

@0xApotheosis

@0xApotheosis 0xApotheosis commented Jun 12, 2026

Copy link
Copy Markdown
Member

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.

  • New same-origin route app/api/strapi/[...path]/route.ts injects the token on the server and forwards GETs to Strapi. The upstream path is pinned to /api/.
  • Client hooks/components (useFetchPosts, useFetchNewsroom, useFetchSupportArticles, Notification) call /api/strapi/... with no auth header.
  • Server components/utils read the token from STRAPI_API_TOKEN (was NEXT_PUBLIC_STRAPI_API_TOKEN).

Deploy note

Set STRAPI_API_TOKEN (no NEXT_PUBLIC_ prefix) in the deploy env before this ships — without it the proxy returns 500 and server-rendered content fails.

Manual QA

  • Set STRAPI_API_TOKEN and NEXT_PUBLIC_STRAPI_URL, then yarn dev.
  • Open the blog index, newsroom index, and support search; confirm lists load and pagination/filter/sort work (converted client hooks).
  • Confirm the notification banner renders on the homepage.
  • In DevTools Network, confirm client calls hit /api/strapi/... with no auth header.
  • Load a blog post and a wallet/chain/protocol page (server-rendered) and confirm content renders.

Verification

tsc --noEmit passes locally. Didn't run end-to-end here (no Strapi in this environment).

Summary by CodeRabbit

  • Refactor

    • Centralized Strapi API integration by creating server-side fetch utilities and a unified API proxy route.
    • Reorganized image URL generation into a dedicated utility module.
  • Security

    • Moved Strapi API token from public environment variables to server-only, preventing client-side exposure.

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>
@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@kaladinlight, we couldn't start this review because you've reached your PR review rate limit.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9123875f-7f0e-47f8-8b5e-34dc918c08c2

📥 Commits

Reviewing files that changed from the base of the PR and between af4b1a3 and 9fed418.

📒 Files selected for processing (15)
  • app/[lang]/(resources)/_utils/fetchUtils.ts
  • app/[lang]/(resources)/blog/(withNavigation)/categories/[category]/page.tsx
  • app/[lang]/(resources)/blog/(withNavigation)/tags/[tag]/page.tsx
  • app/[lang]/(resources)/blog/[slug]/layout.tsx
  • app/[lang]/(resources)/chains/[slug]/page.tsx
  • app/[lang]/(resources)/newsroom/(withNavigation)/categories/[category]/page.tsx
  • app/[lang]/(resources)/newsroom/(withNavigation)/tags/[tag]/page.tsx
  • app/[lang]/(resources)/newsroom/[slug]/layout.tsx
  • app/[lang]/(resources)/protocols/[slug]/page.tsx
  • app/[lang]/(resources)/wallets/[slug]/page.tsx
  • app/[lang]/(terms)/_components/utils.ts
  • app/[lang]/_hooks/useFetchNewsroom.tsx
  • app/[lang]/_hooks/useFetchPosts.tsx
  • app/[lang]/_hooks/useFetchSupportArticles.ts
  • app/api/strapi/[...path]/route.ts
📝 Walkthrough

Walkthrough

The PR centralizes Strapi CMS authentication by renaming NEXT_PUBLIC_STRAPI_API_TOKEN to STRAPI_API_TOKEN, introducing a server-only strapiServerFetch utility and a validated Next.js proxy route at app/api/strapi/[...path]. All server components are refactored to use the shared helper; client-side hooks and components are redirected through the proxy. The old query.ts module is deleted and getStrapiImageUrl is extracted to its own utility.

Changes

Strapi Token Centralization

Layer / File(s) Summary
Server-only Strapi fetch utilities and env config
.env.local.sample, app/[lang]/_utils/strapiServer.ts, app/[lang]/_utils/getStrapiImageUrl.ts, app/[lang]/_utils/query.ts
STRAPI_API_TOKEN replaces NEXT_PUBLIC_STRAPI_API_TOKEN in the sample config. strapiServerFetch is added as a server-only helper using that token. getStrapiImageUrl is extracted to its own module. The entire query.ts is deleted.
Strapi proxy API route
app/api/strapi/[...path]/route.ts
New GET handler proxies same-origin requests to Strapi, enforcing an allowlist for path segments and query parameter keys, injecting the server-side bearer token, and returning the upstream response body and status.
Server fetch helpers refactored
app/[lang]/(resources)/_utils/fetchUtils.ts, app/[lang]/(core-products)/_components/fetchUtils.ts, app/[lang]/(terms)/_components/utils.ts
Both fetchUtils modules become server-only and switch to strapiServerFetch. New slug-lookup helpers fetchSupportedProtocol, fetchSupportedChain, fetchSupportedWallet, and fetchDiscovers are added to the resources module. Terms utilities are similarly updated.
Server pages and layouts switched
app/[lang]/(resources)/blog/..., app/[lang]/(resources)/newsroom/..., app/[lang]/(resources)/chains/[slug]/page.tsx, app/[lang]/(resources)/protocols/[slug]/page.tsx, app/[lang]/(resources)/wallets/[slug]/page.tsx, app/[lang]/(resources)/discover/...
All server-component pages and layouts replace inline fetch+auth-header construction with strapiServerFetch or the new slug-fetch helpers, preserving existing query strings, notFound() handling, and rendering logic.
Client hooks and components redirected through proxy
app/[lang]/_hooks/useFetchPosts.tsx, app/[lang]/_hooks/useFetchNewsroom.tsx, app/[lang]/_hooks/useFetchSupportArticles.ts, app/[lang]/_components/Notification.tsx, app/[lang]/_components/StrapiFAQ.tsx
Client-side hooks and components replace direct Strapi base URLs and Authorization headers with relative /api/strapi/* paths, delegating authentication to the proxy route.
getStrapiImageUrl import path updates
app/[lang]/_components/BlogPost.tsx, app/[lang]/_components/NewsPost.tsx, app/[lang]/_components/Popup.tsx, app/[lang]/_components/ElementCard.tsx, app/[lang]/_components/strapi/..., app/[lang]/(core-products)/*/page.tsx, app/[lang]/(resources)/_components/..., app/[lang]/(resources)/discover/[slug]/page.tsx
Mechanical import path change from _utils/query to _utils/getStrapiImageUrl across all components and pages that use the helper.

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)
Loading

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)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 The token hid in plain sight no more,
Now tucked server-side behind a door.
A proxy stands guard with an allowlist neat,
No bearer exposed for the browser to meet.
The old query.ts hops off to retire—
New helpers emerge, both cleaner and sprier! 🔐

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely describes the main objective: moving Strapi API token handling to server-side via a proxy.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/strapi-token-server-side

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
kaladinlight and others added 2 commits June 16, 2026 10:33
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>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Consider checking response.ok before parsing JSON in generateMetadata.

Unlike fetchSupportedWallet (used in the page component), this direct strapiServerFetch call doesn't verify response.ok before calling response.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 !wallet check 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

📥 Commits

Reviewing files that changed from the base of the PR and between 2f978eb and af4b1a3.

⛔ Files ignored due to path filters (1)
  • public/favicon.ico is excluded by !**/*.ico
📒 Files selected for processing (42)
  • .env.local.sample
  • app/[lang]/(core-products)/_components/ProductHero.tsx
  • app/[lang]/(core-products)/_components/fetchUtils.ts
  • app/[lang]/(core-products)/defi-wallet/page.tsx
  • app/[lang]/(core-products)/earn/page.tsx
  • app/[lang]/(core-products)/mobile-app/page.tsx
  • app/[lang]/(core-products)/trade/page.tsx
  • app/[lang]/(resources)/_components/DiscoverFeature.tsx
  • app/[lang]/(resources)/_components/ResourceCard.tsx
  • app/[lang]/(resources)/_utils/fetchUtils.ts
  • app/[lang]/(resources)/blog/(withNavigation)/categories/[category]/page.tsx
  • app/[lang]/(resources)/blog/(withNavigation)/tags/[tag]/page.tsx
  • app/[lang]/(resources)/blog/[slug]/layout.tsx
  • app/[lang]/(resources)/chains/[slug]/page.tsx
  • app/[lang]/(resources)/discover/[slug]/page.tsx
  • app/[lang]/(resources)/discover/page.tsx
  • app/[lang]/(resources)/newsroom/(withNavigation)/categories/[category]/page.tsx
  • app/[lang]/(resources)/newsroom/(withNavigation)/tags/[tag]/page.tsx
  • app/[lang]/(resources)/newsroom/[slug]/layout.tsx
  • app/[lang]/(resources)/protocols/[slug]/page.tsx
  • app/[lang]/(resources)/wallets/[slug]/page.tsx
  • app/[lang]/(terms)/_components/utils.ts
  • app/[lang]/_components/BlogPost.tsx
  • app/[lang]/_components/ElementCard.tsx
  • app/[lang]/_components/NewsPost.tsx
  • app/[lang]/_components/Notification.tsx
  • app/[lang]/_components/Popup.tsx
  • app/[lang]/_components/StrapiFAQ.tsx
  • app/[lang]/_components/strapi/cards-row/Card.tsx
  • app/[lang]/_components/strapi/products/CarouselCard.tsx
  • app/[lang]/_components/strapi/products/ChainBubblesCard.tsx
  • app/[lang]/_components/strapi/products/Grid.tsx
  • app/[lang]/_components/strapi/products/GridDisplaced.tsx
  • app/[lang]/_components/strapi/products/GridLadder.tsx
  • app/[lang]/_components/strapi/templates/ChainFeatures.tsx
  • app/[lang]/_hooks/useFetchNewsroom.tsx
  • app/[lang]/_hooks/useFetchPosts.tsx
  • app/[lang]/_hooks/useFetchSupportArticles.ts
  • app/[lang]/_utils/getStrapiImageUrl.ts
  • app/[lang]/_utils/query.ts
  • app/[lang]/_utils/strapiServer.ts
  • app/api/strapi/[...path]/route.ts
💤 Files with no reviewable changes (1)
  • app/[lang]/_utils/query.ts

Comment thread app/[lang]/(resources)/_utils/fetchUtils.ts
Comment thread app/[lang]/(resources)/_utils/fetchUtils.ts
Comment thread app/[lang]/(resources)/blog/(withNavigation)/categories/[category]/page.tsx Outdated
Comment thread app/[lang]/(terms)/_components/utils.ts Outdated
Comment thread app/api/strapi/[...path]/route.ts Outdated
kaladinlight and others added 2 commits June 16, 2026 10:53
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>
@kaladinlight
kaladinlight merged commit 5d258a1 into develop Jun 16, 2026
2 checks passed
@kaladinlight
kaladinlight deleted the refactor/strapi-token-server-side branch June 16, 2026 16:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants