Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 61 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,36 +1,74 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
# CodeHorse

## Getting Started
CodeHorse is an AI-assisted code review and repository intelligence platform. It connects to your GitHub repositories, indexes the codebase with Pinecone-powered RAG, and uses Gemini-backed reviewers to leave detailed comments on pull requests while surfacing personal activity insights inside a Next.js dashboard.

First, run the development server:
## Why it exists
- Eliminate the wait for code reviews by automatically posting actionable AI feedback on every PR.
- Keep a personal overview of commits, pull requests, and generated reviews via an activity dashboard.
- Centralize repository management (connect/disconnect, usage tracking) without leaving the browser.
- Blend contextual retrieval, structured review prompts, and GitHub webhooks so feedback stays relevant to the codebase.

```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
## Feature highlights
- **Dashboard analytics**: Charts, contribution graph, and counters for repos, commits, PRs, and AI reviews ([app/dashboard](app/dashboard/page.tsx)).
- **Repository manager**: Infinite-scroll view of GitHub repos with one-click connect that provisions webhooks and triggers indexing jobs ([app/dashboard/repository/page.tsx]).
- **AI review history**: Access recently generated reviews, status, and deep links back to GitHub ([app/dashboard/reviews/page.tsx]).
- **Background jobs with Inngest**: `repository.connected` events stream files into Pinecone; `pr.review.requested` events fetch diffs, RAG context, and post Gemini reviews back to GitHub ([inngest/functions](inngest/functions/index.ts)).
- **Stack**: Next.js App Router (16), React 19, TypeScript, Prisma + PostgreSQL, BetterAuth (GitHub OAuth), Pinecone, Inngest, TanStack Query, Tailwind CSS 4.

## Prerequisites
- Node.js 20+
- PostgreSQL database URL (Neon, Supabase, etc.)
- Pinecone index (dimensions must match your embeddings config)
- GitHub OAuth app (for BetterAuth) and GitHub App/webhook secret for PR events
- Optional: `ngrok` or similar tunnel so GitHub can reach your local `/api/webhooks/github`

## Environment variables

| Variable | Required | Purpose |
| --- | --- | --- |
| `NEXT_PUBLIC_APP_BASE_URL` | ✅ | Public origin used for auth redirects and webhook URLs. |
| `BETTER_AUTH_URL` | ✅ | Same as base URL unless proxied; consumed by the BetterAuth client. |
| `DATABASE_URL` | ✅ | PostgreSQL connection string used by Prisma. |
| `GITHUB_CLIENT_ID` / `GITHUB_CLIENT_SECRET` | ✅ (prod) | GitHub OAuth credentials for production. |
| `GITHUB_CLIENT_ID_DEV` / `GITHUB_CLIENT_SECRET_DEV` | ✅ (dev) | Separate OAuth creds for local development. |
| `PINECONE_DB_API_KEY` | ✅ | API key used to talk to your Pinecone index. |

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
> Tip: keep `NEXT_PUBLIC_APP_BASE_URL` and `BETTER_AUTH_URL` in sync (`http://localhost:3000` during development). Update GitHub OAuth callback + homepage URLs whenever you change tunnels or deploy to Vercel.
Comment on lines +27 to +36

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add required Razorpay environment variables to setup docs.

The env table is missing payment variables required by this migration. That makes local/prod setup incomplete for subscriptions/webhooks.

Proposed documentation patch
 | `PINECONE_DB_API_KEY` | ✅ | API key used to talk to your Pinecone index. |
+| `RAZORPAY_KEY_ID` | ✅ | Razorpay API key ID used for subscription creation and checkout. |
+| `RAZORPAY_KEY_SECRET` | ✅ | Razorpay API secret used for server-side API calls/signature checks. |
+| `RAZORPAY_WEBHOOK_SECRET` | ✅ | Secret for verifying Razorpay webhook signatures. |
+| `RAZORPAY_PLAN_ID` | ✅ | Razorpay plan ID used when creating subscriptions. |
🤖 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 `@README.md` around lines 27 - 36, Update the environment variable table in
README.md to include Razorpay variables so subscription/webhook setup is
complete: add RAZORPAY_KEY_ID (✅, Razorpay API key ID for creating orders),
RAZORPAY_KEY_SECRET (✅, Razorpay API secret for signing requests), and
RAZORPAY_WEBHOOK_SECRET (✅, secret used to verify incoming Razorpay webhooks),
and note the expected use of RAZORPAY_WEBHOOK_URL for registering webhook
endpoints; keep these entries consistent with existing entries like
NEXT_PUBLIC_APP_BASE_URL and BETTER_AUTH_URL and update the tip about keeping
callback/webhook URLs in sync for local/dev and production.


You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
## Local development

This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
```bash
# Install deps & generate the Prisma client
npm install

# Apply database schema (creates tables + seed state if configured)
npx prisma migrate dev

## Learn More
# Run the Next.js dev server
npm run dev

To learn more about Next.js, take a look at the following resources:
# In another terminal, start the Inngest dev server for background jobs
npx inngest dev

- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
# (Optional) expose the app publicly so GitHub can deliver webhooks
ngrok http 3000
```

You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
- Connecting a repo from the dashboard will call GitHub, store metadata in Prisma, enqueue `repository.connected`, and immediately start Pinecone indexing.
- Opening or updating a PR will hit the `/api/webhooks/github` route, fire `pr.review.requested`, gather diff/context, generate the Gemini review, post it back to GitHub, and persist the review in PostgreSQL for the dashboard.

## Deploy on Vercel
## Project structure

The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
```
app/ # Next.js App Router routes (auth, dashboard, API handlers)
components/ # Reusable UI primitives (Radix-based)
lib/ # Auth, DB, Pinecone clients, shared utilities
module/ # Domain modules (github integration, AI/RAG utils, dashboard)
inngest/ # Background functions for indexing + review generation
prisma/ # Prisma schema and migrations
```
Comment on lines +62 to +69

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Specify a language for the fenced project-structure block.

The fence at Line 62 should include a language to satisfy markdownlint (MD040).

Proposed fix
-```
+```text
 app/                # Next.js App Router routes (auth, dashboard, API handlers)
 ...
-```
+```
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
```
app/ # Next.js App Router routes (auth, dashboard, API handlers)
components/ # Reusable UI primitives (Radix-based)
lib/ # Auth, DB, Pinecone clients, shared utilities
module/ # Domain modules (github integration, AI/RAG utils, dashboard)
inngest/ # Background functions for indexing + review generation
prisma/ # Prisma schema and migrations
```
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 62-62: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 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 `@README.md` around lines 62 - 69, The fenced project-structure code block in
README.md is missing a language specifier; update the opening fence from ``` to
```text so markdownlint MD040 is satisfied (edit the block that contains the
directory listing under the app/components/lib/module/inngest/prisma comment
lines).


Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
## Deployment notes
- Set `NEXT_PUBLIC_APP_BASE_URL` and `BETTER_AUTH_URL` to your production domain before deploying to Vercel.
- Recreate the GitHub OAuth + webhook URLs inside your GitHub app to point at the live domain.
- Provision the same Pinecone index + PostgreSQL database that you used locally; run `npx prisma migrate deploy` as part of your CI/CD workflow.
87 changes: 87 additions & 0 deletions app/api/subscription/create/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import prisma from "@/lib/db";
import razorpay from "@/module/payment/config/razorpay";

/**
* POST /api/subscription/create
*
* Creates a Razorpay subscription for the authenticated user.
* - Validates user session
* - Checks if user already has an active subscription
* - Creates a Razorpay subscription using the plan ID from env
* - Saves the razorpaySubscriptionId on the User record
* - Returns subscriptionId + keyId for the frontend checkout modal
*/
export async function POST(req: NextRequest) {
try {
const session = await auth.api.getSession({
headers: req.headers,
});

if (!session?.user) {
return NextResponse.json(
{ error: "Unauthorized" },
{ status: 401 }
);
}

// Check if user already has an active subscription
const user = await prisma.user.findUnique({
where: { id: session.user.id },
});

if (!user) {
return NextResponse.json(
{ error: "User not found" },
{ status: 404 }
);
}

if (user.subscriptionTier === "PRO" && user.subscriptionStatus === "ACTIVE") {
return NextResponse.json(
{ error: "You already have an active subscription" },
{ status: 400 }
);
}

const planId = process.env.RAZORPAY_PLAN_ID;
if (!planId) {
console.error("RAZORPAY_PLAN_ID is not configured");
return NextResponse.json(
{ error: "Payment configuration error" },
{ status: 500 }
);
}

// Create a Razorpay subscription
const subscription = await razorpay().subscriptions.create({
plan_id: planId,
customer_notify: 1,
total_count: 12, // 12 monthly billing cycles
notes: {
userId: session.user.id,
userEmail: session.user.email,
},
});

// Save the subscription ID on the user record
await prisma.user.update({
where: { id: session.user.id },
data: {
razorpaySubscriptionId: subscription.id,
},
});
Comment on lines +41 to +74

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

# Find and read the subscription creation route
find . -type f -name "route.ts" | grep -i subscription | head -20

Repository: BhavyaB19/codehorse

Length of output: 141


🏁 Script executed:

# Also check for the user schema
find . -type f \( -name "*.ts" -o -name "*.prisma" \) | xargs grep -l "razorpaySubscriptionId" | head -10

Repository: BhavyaB19/codehorse

Length of output: 307


🏁 Script executed:

# Read the subscription creation route to see full context
wc -l ./app/api/subscription/create/route.ts

Repository: BhavyaB19/codehorse

Length of output: 105


🏁 Script executed:

# Read the prisma schema to understand user model
cat ./prisma/schema.prisma

Repository: BhavyaB19/codehorse

Length of output: 3659


🏁 Script executed:

# Read the full subscription creation route
cat -n ./app/api/subscription/create/route.ts

Repository: BhavyaB19/codehorse

Length of output: 3418


Add guard to prevent duplicate subscription creation on retried requests.

The current check at line 41 only blocks PRO + ACTIVE; users with an existing razorpaySubscriptionId can still trigger new Razorpay subscriptions on retried requests. The subscription creation (lines 58-66) is non-idempotent, and a retry will create a duplicate Razorpay subscription, with the database only tracking the latest ID. Add a guard to block requests when razorpaySubscriptionId exists and subscription status is neither expired nor canceled:

Suggested guard
         if (user.subscriptionTier === "PRO" && user.subscriptionStatus === "ACTIVE") {
             return NextResponse.json(
                 { error: "You already have an active subscription" },
                 { status: 400 }
             );
         }
+        if (user.razorpaySubscriptionId && user.subscriptionStatus !== "EXPIRED" && user.subscriptionStatus !== "CANCELED") {
+            return NextResponse.json(
+                { error: "Subscription is already in progress" },
+                { status: 409 }
+            );
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (user.subscriptionTier === "PRO" && user.subscriptionStatus === "ACTIVE") {
return NextResponse.json(
{ error: "You already have an active subscription" },
{ status: 400 }
);
}
const planId = process.env.RAZORPAY_PLAN_ID;
if (!planId) {
console.error("RAZORPAY_PLAN_ID is not configured");
return NextResponse.json(
{ error: "Payment configuration error" },
{ status: 500 }
);
}
// Create a Razorpay subscription
const subscription = await razorpay().subscriptions.create({
plan_id: planId,
customer_notify: 1,
total_count: 12, // 12 monthly billing cycles
notes: {
userId: session.user.id,
userEmail: session.user.email,
},
});
// Save the subscription ID on the user record
await prisma.user.update({
where: { id: session.user.id },
data: {
razorpaySubscriptionId: subscription.id,
},
});
if (user.subscriptionTier === "PRO" && user.subscriptionStatus === "ACTIVE") {
return NextResponse.json(
{ error: "You already have an active subscription" },
{ status: 400 }
);
}
if (user.razorpaySubscriptionId && user.subscriptionStatus !== "EXPIRED" && user.subscriptionStatus !== "CANCELED") {
return NextResponse.json(
{ error: "Subscription is already in progress" },
{ status: 409 }
);
}
const planId = process.env.RAZORPAY_PLAN_ID;
if (!planId) {
console.error("RAZORPAY_PLAN_ID is not configured");
return NextResponse.json(
{ error: "Payment configuration error" },
{ status: 500 }
);
}
// Create a Razorpay subscription
const subscription = await razorpay().subscriptions.create({
plan_id: planId,
customer_notify: 1,
total_count: 12, // 12 monthly billing cycles
notes: {
userId: session.user.id,
userEmail: session.user.email,
},
});
// Save the subscription ID on the user record
await prisma.user.update({
where: { id: session.user.id },
data: {
razorpaySubscriptionId: subscription.id,
},
});
🤖 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/api/subscription/create/route.ts` around lines 41 - 74, Current code
allows creating a new Razorpay subscription on retries because it only checks
user.subscriptionTier and subscriptionStatus; add a guard before calling
razorpay().subscriptions.create to block if user.razorpaySubscriptionId is set
and that existing subscription is not in a terminal state (e.g., "cancelled" or
"expired"). Specifically, if session.user has razorpaySubscriptionId, fetch the
existing subscription via razorpay().subscriptions.fetch(existingId) and if its
status is not a terminal state return a 400 JSON error (same pattern as the
other early returns); only call razorpay().subscriptions.create and then
prisma.user.update( { data: { razorpaySubscriptionId: subscription.id } } ) when
there is no existing active/non-terminal subscription.


return NextResponse.json({
subscriptionId: subscription.id,
keyId: process.env.NEXT_PUBLIC_RAZORPAY_KEY_ID,
});
Comment on lines +76 to +79

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate checkout key before returning success payload.

Line 78 can return undefined for keyId, causing client checkout initialization failure after a successful server call.

Suggested fix
+        const keyId = process.env.NEXT_PUBLIC_RAZORPAY_KEY_ID;
+        if (!keyId) {
+            return NextResponse.json(
+                { error: "Payment configuration error" },
+                { status: 500 }
+            );
+        }
         return NextResponse.json({
             subscriptionId: subscription.id,
-            keyId: process.env.NEXT_PUBLIC_RAZORPAY_KEY_ID,
+            keyId,
         });
🤖 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/api/subscription/create/route.ts` around lines 76 - 79, The response is
returning NEXT_PUBLIC_RAZORPAY_KEY_ID which may be undefined; update the handler
that returns NextResponse.json({ subscriptionId: subscription.id, keyId:
process.env.NEXT_PUBLIC_RAZORPAY_KEY_ID }) to validate the env var before
responding: check process.env.NEXT_PUBLIC_RAZORPAY_KEY_ID (or throw/return a
500/400 error) and only include keyId when present, otherwise log an explicit
error and return an error response; reference the return block that uses
subscription.id and process.env.NEXT_PUBLIC_RAZORPAY_KEY_ID in route.ts to
locate where to add the validation and error handling.

} catch (error) {
console.error("Error creating subscription:", error);
return NextResponse.json(
{ error: "Failed to create subscription" },
{ status: 500 }
);
}
}
78 changes: 78 additions & 0 deletions app/api/subscription/verify/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import prisma from "@/lib/db";
import crypto from "crypto";

/**
* POST /api/subscription/verify
*
* Verifies the Razorpay payment signature after the checkout modal completes.
* This prevents users from spoofing a successful payment on the frontend.
*
* Flow:
* 1. Frontend sends razorpay_payment_id, razorpay_subscription_id, razorpay_signature
* 2. We compute HMAC-SHA256(payment_id + "|" + subscription_id) using our key_secret
* 3. Compare with the signature from Razorpay
* 4. On match: activate the subscription in our DB
*/
export async function POST(req: NextRequest) {
try {
const session = await auth.api.getSession({
headers: req.headers,
});

if (!session?.user) {
return NextResponse.json(
{ error: "Unauthorized" },
{ status: 401 }
);
}

const body = await req.json();
const {
razorpay_payment_id,
razorpay_subscription_id,
razorpay_signature,
} = body;

if (!razorpay_payment_id || !razorpay_subscription_id || !razorpay_signature) {
return NextResponse.json(
{ error: "Missing required payment fields" },
{ status: 400 }
);
}

// Verify signature
const keySecret = process.env.RAZORPAY_KEY_SECRET!;
const generatedSignature = crypto
.createHmac("sha256", keySecret)
.update(`${razorpay_payment_id}|${razorpay_subscription_id}`)
.digest("hex");

if (generatedSignature !== razorpay_signature) {
console.error("Razorpay signature verification failed");
return NextResponse.json(
{ error: "Payment verification failed" },
{ status: 400 }
);
}

// Signature is valid — activate the subscription
await prisma.user.update({
where: { id: session.user.id },
data: {
subscriptionTier: "PRO",
subscriptionStatus: "ACTIVE",
razorpaySubscriptionId: razorpay_subscription_id,
},
});
Comment on lines +45 to +68

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

cat -n app/api/subscription/verify/route.ts | head -100

Repository: BhavyaB19/codehorse

Length of output: 3158


🏁 Script executed:

# Search for other references to razorpaySubscriptionId in the codebase
rg "razorpaySubscriptionId" --type ts --type tsx -B 2 -A 2

Repository: BhavyaB19/codehorse

Length of output: 91


🏁 Script executed:

# Search for subscription-related endpoints or middleware
fd -type f -name "*.ts" -o -name "*.tsx" | xargs rg "subscription" | grep -i "route\|endpoint\|post\|get" | head -20

Repository: BhavyaB19/codehorse

Length of output: 2202


🏁 Script executed:

# Search for where subscriptions are created and how subscriptionId is stored
rg "subscriptionId" -B 3 -A 3 | head -80

Repository: BhavyaB19/codehorse

Length of output: 2516


🏁 Script executed:

# Check the webhook handler to see how it validates subscription ownership
cat -n app/api/webhooks/razorpay/route.ts | head -80

Repository: BhavyaB19/codehorse

Length of output: 3489


🏁 Script executed:

# Check the create endpoint to see if it stores razorpaySubscriptionId
cat -n app/api/subscription/create/route.ts

Repository: BhavyaB19/codehorse

Length of output: 3418


🏁 Script executed:

# Search for timing-safe comparison patterns in codebase
rg "timingSafeEqual" --type ts

Repository: BhavyaB19/codehorse

Length of output: 45


🏁 Script executed:

# Quick check: Does Razorpay have any client-side vs server-side validation patterns?
# Also check if there are any database constraints on razorpaySubscriptionId
rg "unique\|constraint\|index" app/api/subscription/ --type ts -i

Repository: BhavyaB19/codehorse

Length of output: 45


🏁 Script executed:

# Check the Prisma schema to see if there's a unique constraint on razorpaySubscriptionId
fd -type f -name "schema.prisma"

Repository: BhavyaB19/codehorse

Length of output: 234


🏁 Script executed:

# Find schema.prisma file
find . -name "schema.prisma" -type f

Repository: BhavyaB19/codehorse

Length of output: 86


🏁 Script executed:

# Check if razorpaySubscriptionId has any constraints
rg "razorpaySubscriptionId" --all-types -B 2 -A 2

Repository: BhavyaB19/codehorse

Length of output: 477


🏁 Script executed:

cat -n prisma/schema.prisma | grep -A 30 "model User"

Repository: BhavyaB19/codehorse

Length of output: 1689


🏁 Script executed:

# Also search for razorpaySubscriptionId in the schema
rg "razorpaySubscriptionId" prisma/schema.prisma -B 2 -A 2

Repository: BhavyaB19/codehorse

Length of output: 234


🏁 Script executed:

# Verify the flow one more time - check if verify is ALWAYS called after create in the same session
cat -n app/dashboard/subscription/page.tsx | grep -A 30 "handler:"

Repository: BhavyaB19/codehorse

Length of output: 1613


Validate subscription ownership and use timing-safe signature comparison.

The verify endpoint accepts razorpay_subscription_id from the request body without checking that it belongs to the authenticated user. An attacker could intercept a valid payment signature and replay it in their own authenticated session to claim another user's subscription. Additionally, line 52 uses direct string comparison (!==) instead of timing-safe comparison for the cryptographic signature—use crypto.timingSafeEqual to mitigate timing attacks. Finally, line 46 uses a non-null assertion on the environment variable; validate it exists first to prevent runtime crashes.

Suggested hardening
-        const keySecret = process.env.RAZORPAY_KEY_SECRET!;
+        const keySecret = process.env.RAZORPAY_KEY_SECRET;
+        if (!keySecret) {
+            return NextResponse.json({ error: "Payment configuration error" }, { status: 500 });
+        }
+
+        const user = await prisma.user.findUnique({
+            where: { id: session.user.id },
+            select: { razorpaySubscriptionId: true },
+        });
+        if (!user?.razorpaySubscriptionId || user.razorpaySubscriptionId !== razorpay_subscription_id) {
+            return NextResponse.json({ error: "Subscription mismatch" }, { status: 400 });
+        }
+
         const generatedSignature = crypto
             .createHmac("sha256", keySecret)
             .update(`${razorpay_payment_id}|${razorpay_subscription_id}`)
             .digest("hex");
 
-        if (generatedSignature !== razorpay_signature) {
+        const valid =
+            generatedSignature.length === razorpay_signature.length &&
+            crypto.timingSafeEqual(
+                Buffer.from(generatedSignature, "utf8"),
+                Buffer.from(razorpay_signature, "utf8")
+            );
+        if (!valid) {
             console.error("Razorpay signature verification failed");
             return NextResponse.json(
                 { error: "Payment verification failed" },
                 { status: 400 }
             );
🤖 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/api/subscription/verify/route.ts` around lines 45 - 68, The handler
currently uses process.env.RAZORPAY_KEY_SECRET! and a plain string compare on
generatedSignature and accepts razorpay_subscription_id from the request body
without verifying ownership; fix by first validating RAZORPAY_KEY_SECRET exists
and returning a 500 if missing, compute the HMAC into a Buffer and compare to
the incoming razorpay_signature using crypto.timingSafeEqual (convert both to
Buffers) instead of !==, and before calling prisma.user.update verify that the
razorpay_subscription_id actually belongs to session.user.id (e.g., query the
subscription record or user by razorpaySubscriptionId) and only then update
subscriptionTier/subscriptionStatus via prisma.user.update; also return
appropriate 400/403 errors for mismatches.


return NextResponse.json({ success: true });
} catch (error) {
console.error("Error verifying payment:", error);
return NextResponse.json(
{ error: "Payment verification failed" },
{ status: 500 }
);
}
}
6 changes: 0 additions & 6 deletions app/api/webhooks/polar/route.ts

This file was deleted.

Loading