Skip to content

feat: Backend Modernization (Stripe + Sanity + Security)#31

Open
devin-ai-integration[bot] wants to merge 2 commits into
mainfrom
devin/1776433885-backend-modernization
Open

feat: Backend Modernization (Stripe + Sanity + Security)#31
devin-ai-integration[bot] wants to merge 2 commits into
mainfrom
devin/1776433885-backend-modernization

Conversation

@devin-ai-integration
Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration Bot commented Apr 17, 2026

Summary

Upgrades backend dependencies, fixes a critical security issue, adds request validation, and improves code quality in the API layer.

Security:

  • Renamed NEXT_PUBLIC_STRIPE_SECRET_KEYSTRIPE_SECRET_KEY in .env and pages/api/stripe.js to prevent the Stripe secret key from being bundled into client-side JavaScript by Next.js
  • Added .env.example documenting expected env var names (without values)

Dependency upgrades:

  • stripe: v8 → v22 (with explicit apiVersion: '2026-03-25.dahlia')
  • @sanity/client: v3 → v7 (sanityClient()createClient())
  • @sanity/image-url: v1 → v2 (imageUrlBuilder()createImageUrlBuilder())
  • Added zod v4 for runtime request validation

API improvements (pages/api/stripe.js):

  • Added Zod schema validation for the cart items request body, returning 400 on invalid input
  • Replaced hardcoded string manipulation for Sanity image URLs with @sanity/image-url builder
  • Fixed error response to return { error: message } JSON object instead of a raw string
  • Fixed floating-point unit_amount calculation: item.price * 100Math.round(item.price * 100) to prevent Stripe API rejections for decimal prices like 19.99

Cleanup:

  • Removed console.log(product) from pages/product/[slug].js

npm run build and npm run lint both pass. No frontend components, styling, or state management were changed.

Review & Testing Checklist for Human

  • Zod validation schema matches frontend cart shape: Confirm the Zod schema (name: string, price: number, quantity: int, image[].asset._ref: string) matches what StateContext actually sends from the cart. A mismatch would cause all checkout requests to 400. Trace the cart object shape from context/StateContext through to the fetch('/api/stripe', ...) call.
  • createImageUrlBuilder(client).image(ref).url() produces correct image URLs: The old code did manual string replacement (image- → CDN prefix, -webp.webp) which only handled webp images. Verify the @sanity/image-url builder generates equivalent URLs for the Sanity asset refs actually stored in your dataset. Compare a sample URL from the old method vs. the new builder output.
  • End-to-end checkout test: Add a product to cart, proceed to Stripe checkout, and confirm the session is created successfully with correct line items and images. This is the only way to validate the Stripe + Sanity + Zod changes work together end-to-end.
  • Stripe v8→v22 compatibility: Verify that checkout sessions API params (submit_type, shipping_options, adjustable_quantity) are still valid in Stripe v22. The Stripe Node.js changelog documents breaking changes across major versions.
  • No other references to NEXT_PUBLIC_STRIPE_SECRET_KEY: Search the codebase to confirm nothing else reads the old env var name.

Notes

  • The .env file in the repo still contains real test API keys (Stripe and Sanity). This is a pre-existing issue not addressed by this PR — consider moving secrets out of version control entirely.
  • Zod v4 was installed (latest). Its safeParse API is compatible with v3 patterns used here, but error message formatting may differ slightly from v3.
  • Pre-existing lint warnings (img elements, missing passHref, etc.) were intentionally left alone per ticket scope.

Link to Devin session: https://app.devin.ai/sessions/d4a32de551a8496f95010f0e89db7c41
Requested by: @Colhodm


Open with Devin

…ion, fix security issues

- Rename NEXT_PUBLIC_STRIPE_SECRET_KEY to STRIPE_SECRET_KEY to prevent client-side exposure
- Add .env.example with expected env var names (no values)
- Upgrade stripe from v8 to v22 with apiVersion option
- Upgrade @sanity/client from v3 to v7 (createClient named export)
- Upgrade @sanity/image-url to v2 (createImageUrlBuilder named export)
- Add Zod schema validation for cart items in stripe API route
- Replace hardcoded image URL string replacement with @sanity/image-url builder
- Fix error response to return proper JSON object ({ error: message })
- Remove console.log(product) from product detail page

Co-Authored-By: Arjun Mishra <arjunsaxmishra@gmail.com>
@devin-ai-integration
Copy link
Copy Markdown
Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

devin-ai-integration[bot]

This comment was marked as resolved.

Co-Authored-By: Arjun Mishra <arjunsaxmishra@gmail.com>
Copy link
Copy Markdown
Author

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 8 additional findings in Devin Review.

Open in Devin Review

Comment thread pages/api/stripe.js
Comment on lines +12 to +25
const cartItemSchema = z.object({
name: z.string(),
price: z.number().positive(),
quantity: z.number().int().positive(),
image: z.array(
z.object({
asset: z.object({
_ref: z.string(),
}),
})
).min(1),
});

const cartSchema = z.array(cartItemSchema).min(1);
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

🔴 Zod validation trusts client-supplied price, enabling price manipulation attacks

The new Zod schema at pages/api/stripe.js:12-23 validates that price is a positive number, but still uses this client-supplied value directly for Stripe's unit_amount at line 54: unit_amount: Math.round(item.price * 100). An attacker can POST to /api/stripe with a crafted payload containing price: 0.01 for any product and complete checkout at an arbitrary price. The validation should instead accept a product identifier (e.g., a Sanity _id or slug), fetch the canonical price from Sanity server-side using the imported client, and use that for the Stripe line item. The name field has the same issue. This is pre-existing but directly relevant since this PR's purpose is to add request validation to secure this endpoint.

Prompt for agents
The Zod schema validates that price is a positive number and name is a string, but both values come from the untrusted client and are used directly in the Stripe checkout session (unit_amount at line 54, product name at line 51). An attacker can send any price they want.

To fix this, the cart items sent from the client (see components/Cart.jsx:23 where cartItems are JSON-stringified) should include a product identifier (like _id or slug) instead of (or in addition to) the price. The API handler should then use the already-imported Sanity client (line 3) to fetch the real product data from Sanity, e.g.:

  const productIds = result.data.map(item => item._id);
  const products = await client.fetch('*[_type == "product" && _id in $ids]', { ids: productIds });

Then use the server-fetched price and name for each line item in the Stripe session, matching by _id. Only the quantity (and possibly image selection) should be trusted from the client.

This requires changes in both pages/api/stripe.js (the schema and the mapping logic) and potentially components/Cart.jsx (to ensure product IDs are included in the request body).
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Valid concern — this is a pre-existing issue (the original code also trusts client-supplied price and name). Fixing it properly requires modifying components/Cart.jsx to include product identifiers in the request body, which is out of scope for this PR (ticket explicitly states: "Do NOT change any frontend components"). This would be a good follow-up ticket for server-side price verification.

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.

1 participant