-
Notifications
You must be signed in to change notification settings - Fork 0
Feat/razorpay subscription #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: payment
Are you sure you want to change the base?
Changes from all commits
9a15bc7
bcb31c2
8387672
78b5486
877ede3
732bb3f
76c2b08
d853b3f
afaeea9
3832a75
a995527
4e9587c
b4cd485
90d2e46
1a824ff
f68066c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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. | ||||||||||||||||||
|
|
||||||||||||||||||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Specify a language for the fenced project-structure block. The fence at Line 62 should include a language to satisfy markdownlint ( Proposed fix-```
+```text
app/ # Next.js App Router routes (auth, dashboard, API handlers)
...
-```
+```📝 Committable suggestion
Suggested change
🧰 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 |
||||||||||||||||||
|
|
||||||||||||||||||
| 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. | ||||||||||||||||||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # Find and read the subscription creation route
find . -type f -name "route.ts" | grep -i subscription | head -20Repository: 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 -10Repository: BhavyaB19/codehorse Length of output: 307 🏁 Script executed: # Read the subscription creation route to see full context
wc -l ./app/api/subscription/create/route.tsRepository: BhavyaB19/codehorse Length of output: 105 🏁 Script executed: # Read the prisma schema to understand user model
cat ./prisma/schema.prismaRepository: BhavyaB19/codehorse Length of output: 3659 🏁 Script executed: # Read the full subscription creation route
cat -n ./app/api/subscription/create/route.tsRepository: BhavyaB19/codehorse Length of output: 3418 Add guard to prevent duplicate subscription creation on retried requests. The current check at line 41 only blocks 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| subscriptionId: subscription.id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| keyId: process.env.NEXT_PUBLIC_RAZORPAY_KEY_ID, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+76
to
+79
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validate checkout key before returning success payload. Line 78 can return 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 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error("Error creating subscription:", error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { error: "Failed to create subscription" }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { status: 500 } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: cat -n app/api/subscription/verify/route.ts | head -100Repository: 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 2Repository: 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 -20Repository: 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 -80Repository: 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 -80Repository: 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.tsRepository: BhavyaB19/codehorse Length of output: 3418 🏁 Script executed: # Search for timing-safe comparison patterns in codebase
rg "timingSafeEqual" --type tsRepository: 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 -iRepository: 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 fRepository: BhavyaB19/codehorse Length of output: 86 🏁 Script executed: # Check if razorpaySubscriptionId has any constraints
rg "razorpaySubscriptionId" --all-types -B 2 -A 2Repository: 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 2Repository: 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 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 |
||
|
|
||
| return NextResponse.json({ success: true }); | ||
| } catch (error) { | ||
| console.error("Error verifying payment:", error); | ||
| return NextResponse.json( | ||
| { error: "Payment verification failed" }, | ||
| { status: 500 } | ||
| ); | ||
| } | ||
| } | ||
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
🤖 Prompt for AI Agents