feat: Backend Modernization (Stripe + Sanity + Security)#31
feat: Backend Modernization (Stripe + Sanity + Security)#31devin-ai-integration[bot] wants to merge 2 commits into
Conversation
…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 EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
Co-Authored-By: Arjun Mishra <arjunsaxmishra@gmail.com>
| 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); |
There was a problem hiding this comment.
🔴 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).
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
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.
Summary
Upgrades backend dependencies, fixes a critical security issue, adds request validation, and improves code quality in the API layer.
Security:
NEXT_PUBLIC_STRIPE_SECRET_KEY→STRIPE_SECRET_KEYin.envandpages/api/stripe.jsto prevent the Stripe secret key from being bundled into client-side JavaScript by Next.js.env.exampledocumenting expected env var names (without values)Dependency upgrades:
stripe: v8 → v22 (with explicitapiVersion: '2026-03-25.dahlia')@sanity/client: v3 → v7 (sanityClient()→createClient())@sanity/image-url: v1 → v2 (imageUrlBuilder()→createImageUrlBuilder())zodv4 for runtime request validationAPI improvements (
pages/api/stripe.js):@sanity/image-urlbuilder{ error: message }JSON object instead of a raw stringunit_amountcalculation:item.price * 100→Math.round(item.price * 100)to prevent Stripe API rejections for decimal prices like19.99Cleanup:
console.log(product)frompages/product/[slug].jsnpm run buildandnpm run lintboth pass. No frontend components, styling, or state management were changed.Review & Testing Checklist for Human
name: string,price: number,quantity: int,image[].asset._ref: string) matches whatStateContextactually sends from the cart. A mismatch would cause all checkout requests to 400. Trace the cart object shape fromcontext/StateContextthrough to thefetch('/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-urlbuilder 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.submit_type,shipping_options,adjustable_quantity) are still valid in Stripe v22. The Stripe Node.js changelog documents breaking changes across major versions.NEXT_PUBLIC_STRIPE_SECRET_KEY: Search the codebase to confirm nothing else reads the old env var name.Notes
.envfile 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.safeParseAPI is compatible with v3 patterns used here, but error message formatting may differ slightly from v3.Link to Devin session: https://app.devin.ai/sessions/d4a32de551a8496f95010f0e89db7c41
Requested by: @Colhodm