From 9a15bc7c33d3cb92a83e8edb5046912a6a804437 Mon Sep 17 00:00:00 2001 From: BhavyaB19 Date: Sun, 15 Feb 2026 13:02:04 +0530 Subject: [PATCH 01/15] postinstall added for vercel --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index a0f132d..4fcba94 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { + "postinstall": "prisma generate", "dev": "next dev", "build": "next build", "start": "next start", From bcb31c253ffe8b5b72e96665297f36b76db2ef0d Mon Sep 17 00:00:00 2001 From: BhavyaB19 Date: Sun, 15 Feb 2026 13:28:01 +0530 Subject: [PATCH 02/15] resizable component commented --- components/ui/resizable.tsx | 100 ++++++++++++++++++------------------ 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/components/ui/resizable.tsx b/components/ui/resizable.tsx index 12bbd0b..085bae4 100644 --- a/components/ui/resizable.tsx +++ b/components/ui/resizable.tsx @@ -1,56 +1,56 @@ -"use client" +// "use client" -import * as React from "react" -import { GripVerticalIcon } from "lucide-react" -import * as ResizablePrimitive from "react-resizable-panels" +// import * as React from "react" +// import { GripVerticalIcon } from "lucide-react" +// import * as ResizablePrimitive from "react-resizable-panels" -import { cn } from "@/lib/utils" +// import { cn } from "@/lib/utils" -function ResizablePanelGroup({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} +// function ResizablePanelGroup({ +// className, +// ...props +// }: React.ComponentProps) { +// return ( +// +// ) +// } -function ResizablePanel({ - ...props -}: React.ComponentProps) { - return -} +// function ResizablePanel({ +// ...props +// }: React.ComponentProps) { +// return +// } -function ResizableHandle({ - withHandle, - className, - ...props -}: React.ComponentProps & { - withHandle?: boolean -}) { - return ( - div]:rotate-90", - className - )} - {...props} - > - {withHandle && ( -
- -
- )} -
- ) -} +// function ResizableHandle({ +// withHandle, +// className, +// ...props +// }: React.ComponentProps & { +// withHandle?: boolean +// }) { +// return ( +// div]:rotate-90", +// className +// )} +// {...props} +// > +// {withHandle && ( +//
+// +//
+// )} +//
+// ) +// } -export { ResizablePanelGroup, ResizablePanel, ResizableHandle } +// export { ResizablePanelGroup, ResizablePanel, ResizableHandle } From 83876729828bd8dbb6932216ee637a34fad809bd Mon Sep 17 00:00:00 2001 From: BhavyaB19 Date: Sun, 15 Feb 2026 13:37:43 +0530 Subject: [PATCH 03/15] token as string added --- inngest/functions/review.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/inngest/functions/review.ts b/inngest/functions/review.ts index de912d9..38ec6d4 100644 --- a/inngest/functions/review.ts +++ b/inngest/functions/review.ts @@ -5,7 +5,6 @@ import { generateText } from "ai"; import { openai } from "@ai-sdk/openai"; import { google } from "@ai-sdk/google"; import prisma from "@/lib/db"; -import { success } from "zod"; export const generateReview = inngest.createFunction( {id: "generate-review", concurrency: 5}, @@ -63,7 +62,7 @@ export const generateReview = inngest.createFunction( return text; }) await step.run("post-comment", async () => { - await postReviewComment(token, owner, repo, prNumber, review); + await postReviewComment(token as string, owner, repo, prNumber, review); }) await step.run("save-review", async () => { From 78b54864d165a35beb6c61ee3f9454f4aa049c69 Mon Sep 17 00:00:00 2001 From: BhavyaB19 Date: Sun, 15 Feb 2026 14:06:09 +0530 Subject: [PATCH 04/15] deployment fix --- app/dashboard/reviews/page.tsx | 2 +- app/layout.tsx | 25 ++++++++++++++----------- lib/db.ts | 8 ++++---- module/ai/actions/index.ts | 3 +++ 4 files changed, 22 insertions(+), 16 deletions(-) diff --git a/app/dashboard/reviews/page.tsx b/app/dashboard/reviews/page.tsx index 358d9ce..6351869 100644 --- a/app/dashboard/reviews/page.tsx +++ b/app/dashboard/reviews/page.tsx @@ -9,7 +9,7 @@ import { getReviews } from "@/module/review/actions"; import { formatDistanceToNow } from "date-fns"; const ReviewsPage = () => { - const { data: reviews, isLoading } = useQuery({ + const { data: reviews = [], isLoading } = useQuery({ queryKey: ["reviews"], queryFn: async () => { return await getReviews() diff --git a/app/layout.tsx b/app/layout.tsx index 65fb3fa..04f200f 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -4,6 +4,7 @@ import "./globals.css"; import { ThemeProvider } from "@/components/providers/theme-providers"; import { QueryProvider } from "@/components/providers/query-provider"; import { Toaster } from "@/components/ui/sonner"; +import { Suspense } from "react"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -30,17 +31,19 @@ export default function RootLayout({ - - - {children} - - - + + + + {children} + + + + diff --git a/lib/db.ts b/lib/db.ts index 66d20bd..3dfe48d 100644 --- a/lib/db.ts +++ b/lib/db.ts @@ -10,11 +10,11 @@ const prismaClientSingleton = () => { return new PrismaClient({adapter}) } -declare const globalThis: { - prismaGlobal: ReturnType -} & typeof globalThis; +declare global { + var prismaGlobal: ReturnType | undefined; +} -const prisma = globalThis.prismaGlobal || prismaClientSingleton(); +const prisma = globalThis.prismaGlobal ?? prismaClientSingleton(); if (process.env.NODE_ENV !== 'production') { globalThis.prismaGlobal = prisma; diff --git a/module/ai/actions/index.ts b/module/ai/actions/index.ts index 108ee48..8234c27 100644 --- a/module/ai/actions/index.ts +++ b/module/ai/actions/index.ts @@ -35,6 +35,9 @@ export const reviewPullRequest = async (owner: string, repo: string, prNumber: n } const token = githubAccount.accessToken; + if (!token) { + throw new Error(`GitHub access token not found for user ${repository.user.name}`); + } const {title} = await getPullRequestDiff(token, owner, repo, prNumber); await inngest.send({ From 877ede3280d2314380ca9ae8cfe3310fdf1f545c Mon Sep 17 00:00:00 2001 From: BhavyaB19 Date: Tue, 17 Feb 2026 21:39:24 +0530 Subject: [PATCH 05/15] added deployed link to trustedOrigins --- lib/auth.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/auth.ts b/lib/auth.ts index a6866d8..194c6f0 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -17,6 +17,7 @@ export const auth = betterAuth({ } }, trustedOrigins: [ + "https://codehorse-ps8j.vercel.app", "http://localhost:3000", "https://thecate-mayme-unlingering.ngrok-free.dev", "http://thecate-mayme-unlingering.ngrok-free.dev", From 732bb3f3bfe92712e0f6df09ecbdb60959b33da3 Mon Sep 17 00:00:00 2001 From: BhavyaB19 Date: Wed, 18 Mar 2026 16:19:23 +0530 Subject: [PATCH 06/15] readme update --- README.md | 84 ++++++++++++++++++++++++++++++++++++++--------------- lib/auth.ts | 9 ++++-- 2 files changed, 68 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index e215bc4..5917030 100644 --- a/README.md +++ b/README.md @@ -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 +``` -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. diff --git a/lib/auth.ts b/lib/auth.ts index 194c6f0..ec1c657 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -3,6 +3,7 @@ import { prismaAdapter } from "better-auth/adapters/prisma"; import prisma from "./db"; const isHttps = (process.env.NEXT_PUBLIC_APP_BASE_URL?.startsWith("https")); +const isProd = process.env.NODE_ENV === "production"; export const auth = betterAuth({ baseURL: process.env.NEXT_PUBLIC_APP_BASE_URL || "http://localhost:3000", @@ -11,8 +12,12 @@ export const auth = betterAuth({ }), socialProviders: { github: { - clientId: process.env.GITHUB_CLIENT_ID!, - clientSecret: process.env.GITHUB_CLIENT_SECRET, + clientId: isProd + ? process.env.GITHUB_CLIENT_ID! + : process.env.GITHUB_CLIENT_ID_DEV!, + clientSecret: isProd + ? process.env.GITHUB_CLIENT_SECRET! + : process.env.GITHUB_CLIENT_SECRET_DEV!, scope: ['repo'], } }, From 76c2b082ebae2651676d728e844530c514f0b91d Mon Sep 17 00:00:00 2001 From: BhavyaB19 Date: Wed, 18 Mar 2026 21:47:27 +0530 Subject: [PATCH 07/15] overview added --- codehorse_overview.md | 254 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 254 insertions(+) create mode 100644 codehorse_overview.md diff --git a/codehorse_overview.md b/codehorse_overview.md new file mode 100644 index 0000000..e679c1d --- /dev/null +++ b/codehorse_overview.md @@ -0,0 +1,254 @@ +# 🐴 CodeHorse — AI-Powered Code Review Platform + +> **Automated, context-aware code reviews on every pull request — powered by AI, driven by your codebase.** + +CodeHorse is an AI-assisted **code review and repository intelligence platform** that connects to your GitHub repositories, understands your codebase through vector-based retrieval (RAG), and automatically posts rich, detailed AI-generated reviews on every pull request — all accessible through a sleek Next.js dashboard. + +--- + +## 🎯 What Problem Does It Solve? + +| Pain Point | How CodeHorse Fixes It | +|---|---| +| Waiting hours/days for human code reviews | AI reviews are generated **instantly** when a PR is opened or updated | +| Reviewers lacking full codebase context | **RAG-powered retrieval** pulls in relevant code context before generating feedback | +| No centralized view of review history | A **dashboard** tracks all repositories, commits, PRs, and AI reviews in one place | +| Manual webhook/repo setup | **One-click connect** provisions GitHub webhooks and triggers codebase indexing automatically | + +--- + +## ✨ Features + +### 1. 📊 Dashboard Analytics +A personal overview of your entire coding activity: +- **Stat Cards** — Total connected repositories, commits (last year), pull requests (all time), and AI reviews generated +- **Contribution Graph** — GitHub-style heatmap visualizing your coding frequency over the past year +- **Activity Overview Chart** — Monthly bar chart breaking down commits, PRs, and AI reviews side by side + +### 2. 📁 Repository Manager +Browse and manage all your GitHub repositories: +- **Infinite-scroll listing** of all repositories (public + private) fetched via the GitHub API +- **Search & filter** repos by name in real-time +- **One-click "Connect"** button that: + - Stores repo metadata in PostgreSQL + - Creates a GitHub webhook for `pull_request` events + - Fires a background job to **index the entire codebase** into Pinecone +- Shows language badges, star counts, and connection status + +### 3. 🤖 Automated AI Code Reviews +The core feature — fully automated, context-aware reviews: +- Triggered automatically when a PR is **opened** or **synchronized** (pushed to) +- GitHub delivers a webhook → CodeHorse fetches the PR diff → RAG retrieves relevant codebase context → Gemini generates a structured review → the review is **posted back to GitHub** as a comment +- Reviews are also **persisted in the database** for the dashboard + +### 4. 📝 Review History +Access all previously generated AI reviews: +- Lists every review with PR title, repository name, PR number, and status badge (`Completed`, `Pending`, `Failed`) +- Shows a preview of the review content +- Deep links back to the **pull request on GitHub** +- Timestamps showing how long ago each review was generated + +### 5. ⚙️ Settings +Manage your account and connected repositories: +- **Profile Form** — View/update your account details +- **Repository List** — View and manage which repositories are connected, with the ability to disconnect (removing the webhook) + +### 6. 🔐 Authentication +Secure GitHub OAuth login: +- Powered by **BetterAuth** with GitHub as the social provider +- Grants `repo` scope access for reading private repos, managing webhooks, and posting comments +- Supports separate OAuth credentials for development vs. production environments + +--- + +## 🧠 How AI Reviews Work (The Pipeline) + +```mermaid +sequenceDiagram + participant Dev as Developer + participant GH as GitHub + participant WH as Webhook Endpoint + participant INN as Inngest + participant PC as Pinecone + participant AI as Gemini 2.5 Flash + participant DB as PostgreSQL + + Dev->>GH: Opens / Updates a PR + GH->>WH: Sends pull_request webhook + WH->>INN: Fires "pr.review.requested" event + INN->>GH: Fetches PR diff, title, description + INN->>PC: Queries for relevant codebase context (RAG) + PC-->>INN: Returns top-K matching code snippets + INN->>AI: Sends structured prompt with diff + context + AI-->>INN: Returns detailed review in Markdown + INN->>GH: Posts review as a PR comment + INN->>DB: Saves review record with status +``` + +--- + +## 📋 What a Generated Review Includes + +Each AI review is structured with the following sections: + +| Section | Description | +|---|---| +| **1. Walkthrough** | A file-by-file explanation of every change in the PR | +| **2. Sequence Diagram** | A Mermaid JS diagram visualizing the flow of changes (when applicable) | +| **3. Summary** | Brief, high-level overview of what the PR does | +| **4. Strengths** | What was done well — clean patterns, good practices, smart decisions | +| **5. Issues** | Potential bugs, security risks, code smells, or logic errors | +| **6. Suggestions** | Actionable recommendations for improvement | +| **7. Poem** | A fun, creative poem summarizing the code changes 🎭 | + +> [!NOTE] +> Reviews are generated by **Google Gemini 2.5 Flash** and are context-aware thanks to Pinecone RAG — meaning the AI understands your codebase, not just the diff. + +--- + +## 🛠️ Tech Stack + +### Frontend +| Technology | Purpose | +|---|---| +| **Next.js 16** (App Router) | Full-stack React framework with server components and API routes | +| **React 19** | UI library | +| **TypeScript** | Type safety across the entire codebase | +| **Tailwind CSS 4** | Utility-first styling | +| **Radix UI** | Accessible, unstyled UI primitives (Dialog, Dropdown, Tabs, etc.) | +| **shadcn/ui** | Pre-built component library on top of Radix | +| **Recharts** | Dashboard charts (bar charts for activity overview) | +| **react-activity-calendar** | GitHub-style contribution heatmap | +| **Lucide React** | Icon library | +| **next-themes** | Dark/light mode support | +| **Jotai** | Lightweight state management | +| **TanStack Query** | Server state management, caching, and infinite scroll | +| **React Hook Form + Zod** | Form handling with schema validation | +| **Sonner** | Toast notifications | +| **cmdk** | Command palette UI | + +### Backend & Infrastructure +| Technology | Purpose | +|---|---| +| **Next.js API Routes** | REST endpoints for webhooks and auth | +| **Prisma 7** | ORM for PostgreSQL with type-safe queries | +| **PostgreSQL** | Primary database (Users, Sessions, Repositories, Reviews) | +| **BetterAuth** | Authentication library with GitHub OAuth provider | +| **Inngest** | Background job orchestration (codebase indexing, review generation) | +| **Octokit** | Official GitHub API client for repos, PRs, webhooks, and GraphQL queries | + +### AI & RAG +| Technology | Purpose | +|---|---| +| **Google Gemini 2.5 Flash** | LLM for generating code reviews | +| **OpenAI text-embedding-3-small** | Embedding model for vectorizing code files | +| **Pinecone** | Vector database for storing and querying code embeddings | +| **Vercel AI SDK** | Unified interface for LLM and embedding calls | + +--- + +## 📁 Project Structure + +``` +codehorse/ +├── app/ # Next.js App Router +│ ├── (auth)/ # Auth pages (login/signup) +│ ├── api/ +│ │ ├── auth/ # BetterAuth API routes +│ │ ├── inngest/ # Inngest webhook handler +│ │ └── webhooks/github/ # GitHub webhook receiver +│ └── dashboard/ +│ ├── page.tsx # Main dashboard (stats, charts, contribution graph) +│ ├── repository/ # Repository manager (browse, search, connect) +│ ├── reviews/ # AI review history +│ └── settings/ # Profile & connected repos management +├── components/ +│ ├── app-sidebar.tsx # Main navigation sidebar +│ └── ui/ # shadcn/ui component library +├── module/ +│ ├── ai/ # AI utilities (RAG, embeddings, review actions) +│ ├── auth/ # Auth components (logout button) +│ ├── dashboard/ # Dashboard actions & components +│ ├── github/ # GitHub API integration (Octokit wrappers) +│ ├── repository/ # Repository hooks & components +│ ├── review/ # Review data fetching actions +│ └── settings/ # Settings forms & repository list +├── inngest/ +│ ├── client.ts # Inngest client instance +│ └── functions/ +│ ├── index.ts # indexRepo — indexes codebase into Pinecone +│ └── review.ts # generateReview — full AI review pipeline +├── lib/ +│ ├── auth.ts # BetterAuth configuration +│ ├── auth-client.ts # Client-side auth helpers +│ ├── db.ts # Prisma client instance +│ ├── pinecone.ts # Pinecone client instance +│ └── utils.ts # Shared utilities +└── prisma/ + └── schema.prisma # Database schema (User, Session, Account, Repository, Review) +``` + +--- + +## 🗄️ Database Models + +```mermaid +erDiagram + User ||--o{ Session : has + User ||--o{ Account : has + User ||--o{ Repository : owns + Repository ||--o{ Review : has + + User { + string id PK + string name + string email + boolean emailVerified + string image + datetime createdAt + datetime updatedAt + } + + Repository { + string id PK + bigint githubId UK + string name + string owner + string fullName + string url + string userId FK + } + + Review { + string id PK + string repositoryId FK + int prNumber + string prTitle + string prUrl + text review + string status + datetime createdAt + } +``` + +--- + +## 🚀 Current Status + +> [!IMPORTANT] +> CodeHorse is currently in **active development (v0.1.0)**. The core review pipeline is functional — connecting repos, indexing codebases, and generating AI reviews on PRs all work end-to-end. + +### What's Working Now +- ✅ GitHub OAuth authentication +- ✅ Repository browsing with infinite scroll +- ✅ One-click repo connect with webhook provisioning +- ✅ Automatic codebase indexing into Pinecone +- ✅ AI review generation on PR open/update +- ✅ Review posted back to GitHub as a PR comment +- ✅ Dashboard with stats, contribution graph, and activity charts +- ✅ Review history page with status tracking +- ✅ Dark/light mode toggle +- ✅ Settings page with profile form and repository management + +### Planned / In Progress +- 🔲 Subscription/billing system (page exists in sidebar) From d853b3f929ef1a7a75ad0ee98bfd7cd38077b85e Mon Sep 17 00:00:00 2001 From: BhavyaB19 Date: Wed, 29 Apr 2026 01:27:53 +0530 Subject: [PATCH 08/15] website title changed Co-authored-by: Copilot --- app/layout.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/layout.tsx b/app/layout.tsx index 04f200f..af1708c 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -17,8 +17,8 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "PR Reviewer", + description: "A tool to help you review pull requests faster and more efficiently.", }; export default function RootLayout({ From afaeea922c15553c4ec0bb69ade26170b92facc5 Mon Sep 17 00:00:00 2001 From: BhavyaB19 Date: Wed, 29 Apr 2026 01:33:50 +0530 Subject: [PATCH 09/15] inngest version update --- package-lock.json | 130 ++++++++++++++++++++++++++++++++++------------ package.json | 2 +- 2 files changed, 98 insertions(+), 34 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4916be2..f26dcb9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "codehorse", "version": "0.1.0", + "hasInstallScript": true, "dependencies": { "@ai-sdk/google": "^3.0.1", "@ai-sdk/openai": "^3.0.26", @@ -49,7 +50,7 @@ "date-fns": "^4.1.0", "dotenv": "^17.2.3", "embla-carousel-react": "^8.6.0", - "inngest": "^3.48.1", + "inngest": "^4.2.5", "input-otp": "^1.4.2", "jotai": "^2.16.0", "lucide-react": "^0.562.0", @@ -6036,6 +6037,90 @@ "react": "^18 || ^19" } }, + "node_modules/@traceloop/ai-semantic-conventions": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@traceloop/ai-semantic-conventions/-/ai-semantic-conventions-0.20.0.tgz", + "integrity": "sha512-bvivhZU6U8TW4TKktYnjdTi+7GE4WxI8epaGjawalSKDunmxaA+4UVFQ+4tSCBvp2Scby+gnYNaTZSrtABfOlQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.9.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@traceloop/instrumentation-anthropic": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@traceloop/instrumentation-anthropic/-/instrumentation-anthropic-0.20.0.tgz", + "integrity": "sha512-xQcPxVrKr3yT9+ZEM3skYXikJc/ocZlGDIcsBQ3mMwL3Weq1QL7jx/uGLXvrSO2Yh0DWUjWI6Q/oiRCEUM6P8w==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/core": "^2.0.1", + "@opentelemetry/instrumentation": "^0.203.0", + "@opentelemetry/semantic-conventions": "^1.36.0", + "@traceloop/ai-semantic-conventions": "0.20.0", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@traceloop/instrumentation-anthropic/node_modules/@opentelemetry/api-logs": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz", + "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@traceloop/instrumentation-anthropic/node_modules/@opentelemetry/instrumentation": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.203.0.tgz", + "integrity": "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.203.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@traceloop/instrumentation-anthropic/node_modules/import-in-the-middle": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.15.0.tgz", + "integrity": "sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA==", + "license": "Apache-2.0", + "dependencies": { + "acorn": "^8.14.0", + "acorn-import-attributes": "^1.9.5", + "cjs-module-lexer": "^1.2.2", + "module-details-from-path": "^1.0.3" + } + }, + "node_modules/@traceloop/instrumentation-anthropic/node_modules/require-in-the-middle": { + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.5.2.tgz", + "integrity": "sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "module-details-from-path": "^1.0.3", + "resolve": "^1.22.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -6877,15 +6962,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ansi-regex": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -7496,6 +7572,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -9076,7 +9153,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -9382,6 +9458,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -9459,7 +9536,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -9588,9 +9664,9 @@ "license": "ISC" }, "node_modules/inngest": { - "version": "3.48.1", - "resolved": "https://registry.npmjs.org/inngest/-/inngest-3.48.1.tgz", - "integrity": "sha512-Taz1ft9zHln/w9Skvmq7VO6UUuFnCDNvXGiz5VPWurlboWvtLrHQ9R6+Zmln8FNzvp6wlhS1FMmpW513NmB2kQ==", + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/inngest/-/inngest-4.2.5.tgz", + "integrity": "sha512-lCLFUb/j/U8KEz2nbfbAHC4GN89po0jwDrq3At7H1j8wAd6N2aM1RYenEMpqo3GvPc1Wk4m8lmoxII14Y6IXHg==", "license": "Apache-2.0", "dependencies": { "@bufbuild/protobuf": "^2.2.3", @@ -9604,17 +9680,16 @@ "@opentelemetry/resources": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-base": ">=2.0.0 <3.0.0", "@standard-schema/spec": "^1.0.0", + "@traceloop/instrumentation-anthropic": "^0.20.0", "@types/debug": "^4.1.12", "@types/ms": "~2.1.0", "canonicalize": "^1.0.8", - "chalk": "^4.1.2", "cross-fetch": "^4.0.0", "debug": "^4.3.4", "hash.js": "^1.1.7", "json-stringify-safe": "^5.0.1", "ms": "^2.1.3", "serialize-error-cjs": "^0.1.3", - "strip-ansi": "^5.2.0", "temporal-polyfill": "^0.2.5", "ulid": "^2.3.0", "zod": "^3.25.0" @@ -9632,6 +9707,7 @@ "hono": ">=4.2.7", "koa": ">=2.14.2", "next": ">=12.0.0", + "react": ">=18.0.0", "typescript": ">=5.8.0", "zod": "^3.25.0 || ^4.0.0" }, @@ -9663,6 +9739,9 @@ "next": { "optional": true }, + "react": { + "optional": true + }, "typescript": { "optional": true } @@ -9822,7 +9901,6 @@ "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -11325,7 +11403,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, "license": "MIT" }, "node_modules/pathe": { @@ -12037,7 +12114,6 @@ "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", - "dev": true, "license": "MIT", "dependencies": { "is-core-module": "^2.16.1", @@ -12664,18 +12740,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^4.1.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -12726,6 +12790,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -12738,7 +12803,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" diff --git a/package.json b/package.json index 4fcba94..30d0e95 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "date-fns": "^4.1.0", "dotenv": "^17.2.3", "embla-carousel-react": "^8.6.0", - "inngest": "^3.48.1", + "inngest": "^4.2.5", "input-otp": "^1.4.2", "jotai": "^2.16.0", "lucide-react": "^0.562.0", From 3832a75e18fa3ae9d48f3993859701c12ed10dcb Mon Sep 17 00:00:00 2001 From: BhavyaB19 Date: Wed, 29 Apr 2026 01:37:33 +0530 Subject: [PATCH 10/15] inngest v4 to v3 --- package-lock.json | 38 +++++++++++++++++++++++++++----------- package.json | 2 +- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index f26dcb9..de6ff02 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,7 +50,7 @@ "date-fns": "^4.1.0", "dotenv": "^17.2.3", "embla-carousel-react": "^8.6.0", - "inngest": "^4.2.5", + "inngest": "^3.54.0", "input-otp": "^1.4.2", "jotai": "^2.16.0", "lucide-react": "^0.562.0", @@ -6962,6 +6962,15 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -7572,7 +7581,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -9458,7 +9466,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -9664,9 +9671,9 @@ "license": "ISC" }, "node_modules/inngest": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/inngest/-/inngest-4.2.5.tgz", - "integrity": "sha512-lCLFUb/j/U8KEz2nbfbAHC4GN89po0jwDrq3At7H1j8wAd6N2aM1RYenEMpqo3GvPc1Wk4m8lmoxII14Y6IXHg==", + "version": "3.54.0", + "resolved": "https://registry.npmjs.org/inngest/-/inngest-3.54.0.tgz", + "integrity": "sha512-EBMgyRZt9rWUmc9GUdxznbO1CmyXKZi2CZPNxNTyZJyjZMXFhPiftq/lTKjhYXD9/WlqaVFVB2K7o8vDAkGs6w==", "license": "Apache-2.0", "dependencies": { "@bufbuild/protobuf": "^2.2.3", @@ -9684,12 +9691,14 @@ "@types/debug": "^4.1.12", "@types/ms": "~2.1.0", "canonicalize": "^1.0.8", + "chalk": "^4.1.2", "cross-fetch": "^4.0.0", "debug": "^4.3.4", "hash.js": "^1.1.7", "json-stringify-safe": "^5.0.1", "ms": "^2.1.3", "serialize-error-cjs": "^0.1.3", + "strip-ansi": "^5.2.0", "temporal-polyfill": "^0.2.5", "ulid": "^2.3.0", "zod": "^3.25.0" @@ -9707,7 +9716,6 @@ "hono": ">=4.2.7", "koa": ">=2.14.2", "next": ">=12.0.0", - "react": ">=18.0.0", "typescript": ">=5.8.0", "zod": "^3.25.0 || ^4.0.0" }, @@ -9739,9 +9747,6 @@ "next": { "optional": true }, - "react": { - "optional": true - }, "typescript": { "optional": true } @@ -12740,6 +12745,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -12790,7 +12807,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" diff --git a/package.json b/package.json index 30d0e95..5f64355 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "date-fns": "^4.1.0", "dotenv": "^17.2.3", "embla-carousel-react": "^8.6.0", - "inngest": "^4.2.5", + "inngest": "^3.54.0", "input-otp": "^1.4.2", "jotai": "^2.16.0", "lucide-react": "^0.562.0", From a99552765a1b64c557f74fe14efa8d679d461e32 Mon Sep 17 00:00:00 2001 From: BhavyaB19 Date: Fri, 1 May 2026 00:14:38 +0530 Subject: [PATCH 11/15] feat: razorpay-subscription integrated --- app/api/subscription/create/route.ts | 87 +++ app/api/subscription/verify/route.ts | 78 +++ app/api/webhooks/polar/route.ts | 6 - app/api/webhooks/razorpay/route.ts | 172 ++++++ app/dashboard/subscription/page.tsx | 437 ++++++++------ components/app-sidebar.tsx | 4 +- lib/auth-client.ts | 5 +- lib/auth.ts | 78 +-- module/payment/actions/cancel.ts | 50 ++ module/payment/actions/index.ts | 63 +- module/payment/config/polar.ts | 6 - module/payment/config/razorpay.ts | 8 + module/payment/lib/subscription.ts | 9 +- package-lock.json | 563 ++++-------------- package.json | 3 +- .../migration.sql | 13 + prisma/schema.prisma | 4 +- scripts/create-razorpay-plan.ts | 47 ++ 18 files changed, 889 insertions(+), 744 deletions(-) create mode 100644 app/api/subscription/create/route.ts create mode 100644 app/api/subscription/verify/route.ts delete mode 100644 app/api/webhooks/polar/route.ts create mode 100644 app/api/webhooks/razorpay/route.ts create mode 100644 module/payment/actions/cancel.ts delete mode 100644 module/payment/config/polar.ts create mode 100644 module/payment/config/razorpay.ts create mode 100644 prisma/migrations/20260430225835_replace_polar_with_razorpay/migration.sql create mode 100644 scripts/create-razorpay-plan.ts diff --git a/app/api/subscription/create/route.ts b/app/api/subscription/create/route.ts new file mode 100644 index 0000000..8624e75 --- /dev/null +++ b/app/api/subscription/create/route.ts @@ -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, + }, + }); + + return NextResponse.json({ + subscriptionId: subscription.id, + keyId: process.env.NEXT_PUBLIC_RAZORPAY_KEY_ID, + }); + } catch (error) { + console.error("Error creating subscription:", error); + return NextResponse.json( + { error: "Failed to create subscription" }, + { status: 500 } + ); + } +} diff --git a/app/api/subscription/verify/route.ts b/app/api/subscription/verify/route.ts new file mode 100644 index 0000000..7d29892 --- /dev/null +++ b/app/api/subscription/verify/route.ts @@ -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, + }, + }); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error("Error verifying payment:", error); + return NextResponse.json( + { error: "Payment verification failed" }, + { status: 500 } + ); + } +} diff --git a/app/api/webhooks/polar/route.ts b/app/api/webhooks/polar/route.ts deleted file mode 100644 index af8081e..0000000 --- a/app/api/webhooks/polar/route.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { NextResponse } from "next/server"; - - -export async function POST(request: Request) { - return NextResponse.json({received: true}) -} \ No newline at end of file diff --git a/app/api/webhooks/razorpay/route.ts b/app/api/webhooks/razorpay/route.ts new file mode 100644 index 0000000..c327ac1 --- /dev/null +++ b/app/api/webhooks/razorpay/route.ts @@ -0,0 +1,172 @@ +import { NextRequest, NextResponse } from "next/server"; +import crypto from "crypto"; +import prisma from "@/lib/db"; + +/** + * POST /api/webhooks/razorpay + * + * Handles asynchronous Razorpay webhook events for subscription lifecycle management. + * These webhooks are the source of truth for subscription status — more reliable than + * frontend callbacks since they're server-to-server. + * + * Webhook events handled: + * - subscription.activated → tier=PRO, status=ACTIVE + * - subscription.charged → tier=PRO, status=ACTIVE (renewal) + * - subscription.pending → status=PENDING + * - subscription.halted → tier=FREE, status=EXPIRED + * - subscription.cancelled → tier=FREE, status=CANCELED + * - subscription.completed → tier=FREE, status=EXPIRED + * - subscription.paused → status=PAUSED + * - subscription.resumed → status=ACTIVE + * + * Setup: Configure this URL in Razorpay Dashboard → Webhooks + * URL: https://your-domain.com/api/webhooks/razorpay + */ +export async function POST(req: NextRequest) { + try { + const rawBody = await req.text(); + const signature = req.headers.get("x-razorpay-signature"); + + if (!signature) { + return NextResponse.json( + { error: "Missing signature" }, + { status: 400 } + ); + } + + // Verify webhook signature + const webhookSecret = process.env.RAZORPAY_WEBHOOK_SECRET!; + const expectedSignature = crypto + .createHmac("sha256", webhookSecret) + .update(rawBody) + .digest("hex"); + + if (expectedSignature !== signature) { + console.error("Razorpay webhook signature verification failed"); + return NextResponse.json( + { error: "Invalid signature" }, + { status: 400 } + ); + } + + const payload = JSON.parse(rawBody); + const event = payload.event; + const subscriptionEntity = payload.payload?.subscription?.entity; + + if (!subscriptionEntity) { + // Not a subscription event, acknowledge it + return NextResponse.json({ received: true }); + } + + const razorpaySubscriptionId = subscriptionEntity.id; + + // Find the user by their Razorpay subscription ID + const user = await prisma.user.findFirst({ + where: { razorpaySubscriptionId }, + }); + + if (!user) { + console.warn( + `Webhook received for unknown subscription: ${razorpaySubscriptionId}` + ); + // Return 200 to prevent Razorpay from retrying + return NextResponse.json({ received: true }); + } + + // Map Razorpay events to our subscription state + switch (event) { + case "subscription.activated": + await prisma.user.update({ + where: { id: user.id }, + data: { + subscriptionTier: "PRO", + subscriptionStatus: "ACTIVE", + }, + }); + break; + + case "subscription.charged": + // Successful renewal payment + await prisma.user.update({ + where: { id: user.id }, + data: { + subscriptionTier: "PRO", + subscriptionStatus: "ACTIVE", + }, + }); + break; + + case "subscription.pending": + // Payment failed, retries may follow + await prisma.user.update({ + where: { id: user.id }, + data: { + subscriptionStatus: "PENDING", + }, + }); + break; + + case "subscription.halted": + // All retries exhausted + await prisma.user.update({ + where: { id: user.id }, + data: { + subscriptionTier: "FREE", + subscriptionStatus: "EXPIRED", + }, + }); + break; + + case "subscription.cancelled": + await prisma.user.update({ + where: { id: user.id }, + data: { + subscriptionTier: "FREE", + subscriptionStatus: "CANCELED", + }, + }); + break; + + case "subscription.completed": + // All billing cycles completed + await prisma.user.update({ + where: { id: user.id }, + data: { + subscriptionTier: "FREE", + subscriptionStatus: "EXPIRED", + }, + }); + break; + + case "subscription.paused": + await prisma.user.update({ + where: { id: user.id }, + data: { + subscriptionStatus: "PAUSED", + }, + }); + break; + + case "subscription.resumed": + await prisma.user.update({ + where: { id: user.id }, + data: { + subscriptionTier: "PRO", + subscriptionStatus: "ACTIVE", + }, + }); + break; + + default: + console.log(`Unhandled Razorpay webhook event: ${event}`); + } + + return NextResponse.json({ received: true }); + } catch (error) { + console.error("Error processing Razorpay webhook:", error); + return NextResponse.json( + { error: "Webhook processing failed" }, + { status: 500 } + ); + } +} diff --git a/app/dashboard/subscription/page.tsx b/app/dashboard/subscription/page.tsx index 61a775d..4e1b577 100644 --- a/app/dashboard/subscription/page.tsx +++ b/app/dashboard/subscription/page.tsx @@ -4,14 +4,21 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -import { Check, X, Loader2, ExternalLink, RefreshCw } from "lucide-react"; -import { checkout, customer } from "@/lib/auth-client"; +import { Check, X, Loader2, RefreshCw, CreditCard, Sparkles, Shield, Zap } from "lucide-react"; import { useSearchParams } from "next/navigation"; import { useQuery } from "@tanstack/react-query"; import { useState, useEffect } from "react"; import { toast } from "sonner" import { getSubscriptionData, syncSubscriptionStatus } from "@/module/payment/actions"; +import { cancelSubscription } from "@/module/payment/actions/cancel"; import { Spinner } from "@/components/ui/spinner"; +import Script from "next/script"; + +declare global { + interface Window { + Razorpay: any; + } +} const PLAN_FEATURES = { free: [ @@ -34,7 +41,7 @@ const PLAN_FEATURES = { export default function SubscriptionPage() { const [checkoutLoading, setCheckoutLoading] = useState(false); - const [portalLoading, setPortalLoading] = useState(false); + const [cancelLoading, setCancelLoading] = useState(false); const [syncLoading, setSyncLoading] = useState(false); const searchParams = useSearchParams(); const success = searchParams.get("success"); @@ -86,7 +93,7 @@ export default function SubscriptionPage() { ) } - if (!data.user) { + if (!data?.user) { return (
@@ -109,216 +116,304 @@ export default function SubscriptionPage() { toast.success("Subscription status synced successfully"); refetch(); } else { - toast.error("Failed to sync subscription status"); + toast.error(result.message || "Failed to sync subscription status"); } } catch (error) { toast.error("Failed to sync subscription status"); } finally { setSyncLoading(false); } - } + + /** + * Razorpay Checkout Flow: + * 1. Call our API to create a Razorpay subscription → get subscriptionId + * 2. Open Razorpay checkout modal with the subscriptionId + * 3. On success, send payment details to our verify API + * 4. Refetch subscription data to reflect the change + */ const handleUpgrade = async() => { try { setCheckoutLoading(true); - await checkout({ - slug: "Codehorse" - }) + + // Step 1: Create subscription on backend + const response = await fetch("/api/subscription/create", { + method: "POST", + }); + + if (!response.ok) { + const errorData = await response.json(); + toast.error(errorData.error || "Failed to initiate checkout"); + return; + } + + const { subscriptionId, keyId } = await response.json(); + + // Step 2: Open Razorpay checkout modal + const options = { + key: keyId, + subscription_id: subscriptionId, + name: "CodeHorse", + description: "Pro Monthly Subscription", + handler: async (response: { + razorpay_payment_id: string; + razorpay_subscription_id: string; + razorpay_signature: string; + }) => { + // Step 3: Verify payment on backend + try { + const verifyResponse = await fetch("/api/subscription/verify", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(response), + }); + + if (verifyResponse.ok) { + toast.success("Subscription activated successfully!"); + refetch(); + } else { + toast.error("Payment verification failed. Please contact support."); + } + } catch { + toast.error("Payment verification failed. Please contact support."); + } + }, + prefill: { + name: data.user?.name, + email: data.user?.email, + }, + theme: { + color: "#6366f1", + }, + modal: { + ondismiss: () => { + setCheckoutLoading(false); + }, + }, + }; + + const razorpayInstance = new window.Razorpay(options); + razorpayInstance.open(); } catch (error) { console.error("Failed to initiate checkout:", error); - setCheckoutLoading(false); + toast.error("Failed to initiate checkout. Please try again."); } finally { setCheckoutLoading(false); } } - const handleManageSubscription = async() => { + const handleCancelSubscription = async() => { try { - setPortalLoading(true) - await customer.portal(); + setCancelLoading(true); + const result = await cancelSubscription(); + if (result.success) { + toast.success("Subscription cancelled successfully"); + refetch(); + } else { + toast.error(result.message || "Failed to cancel subscription"); + } } catch (error) { - console.error("Failed to open portal:", error); - setPortalLoading(false); + toast.error("Failed to cancel subscription"); } finally { - setPortalLoading(false); + setCancelLoading(false); } - } return ( -
-
-
-

Subscription Plans

-

Choose the perfect plan for your needs

+ <> +