From 895bcd32d9107ba56807e4a34cfd463f2fecdd64 Mon Sep 17 00:00:00 2001
From: Dhruvil Mehta <68022411+dhruvilmehta@users.noreply.github.com>
Date: Tue, 2 Apr 2024 18:47:18 -0700
Subject: [PATCH] added course purchases and auth system
---
package.json | 1 +
.../20240402232925_purchase/migration.sql | 54 ++
prisma/schema.prisma | 464 ++++++++++--------
src/app/api/auth/register/route.ts | 36 ++
src/app/api/razorpay/route.ts | 81 +++
src/app/api/razorpay/verify/route.ts | 80 +++
src/app/new-courses/[courseId]/page.tsx | 52 ++
src/app/new-courses/page.tsx | 57 +++
src/app/signup/page.tsx | 15 +
src/components/Appbar.tsx | 96 ++--
src/components/CourseCard.tsx | 12 +-
src/components/Sidebar.tsx | 12 +-
src/components/Signup.tsx | 194 ++++++++
src/components/VideoPlayer.tsx | 30 +-
src/components/VideoPlayer2.tsx | 180 +++----
src/components/bookmark/BookmarkView.tsx | 12 +-
.../us-section/why-us-card/why-us-content.ts | 2 +-
src/components/razorpay/Razorpay.tsx | 102 ++++
src/db/course.ts | 18 +-
src/lib/auth.ts | 12 +-
src/lib/utils.ts | 22 +-
21 files changed, 1120 insertions(+), 412 deletions(-)
create mode 100644 prisma/migrations/20240402232925_purchase/migration.sql
create mode 100644 src/app/api/auth/register/route.ts
create mode 100644 src/app/api/razorpay/route.ts
create mode 100644 src/app/api/razorpay/verify/route.ts
create mode 100644 src/app/new-courses/[courseId]/page.tsx
create mode 100644 src/app/new-courses/page.tsx
create mode 100644 src/app/signup/page.tsx
create mode 100644 src/components/Signup.tsx
create mode 100644 src/components/razorpay/Razorpay.tsx
diff --git a/package.json b/package.json
index 118646510..785ffa96c 100644
--- a/package.json
+++ b/package.json
@@ -54,6 +54,7 @@
"next-themes": "^0.2.1",
"node-fetch": "^3.3.2",
"notion-client": "^6.16.0",
+ "razorpay": "^2.9.2",
"react": "^18",
"react-dom": "^18",
"react-hook-form": "^7.50.1",
diff --git a/prisma/migrations/20240402232925_purchase/migration.sql b/prisma/migrations/20240402232925_purchase/migration.sql
new file mode 100644
index 000000000..77efa2af6
--- /dev/null
+++ b/prisma/migrations/20240402232925_purchase/migration.sql
@@ -0,0 +1,54 @@
+-- AlterTable
+ALTER TABLE "Course" ADD COLUMN "price" INTEGER NOT NULL DEFAULT 1000;
+
+-- CreateTable
+CREATE TABLE "Receipt" (
+ "id" TEXT NOT NULL,
+ "userId" TEXT NOT NULL,
+ "courseId" INTEGER NOT NULL DEFAULT 0,
+ "razorpayOrderId" TEXT,
+
+ CONSTRAINT "Receipt_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Purchase" (
+ "id" TEXT NOT NULL,
+ "receiptId" TEXT NOT NULL,
+ "razorpay_payment_id" TEXT NOT NULL,
+ "razorpay_order_id" TEXT NOT NULL,
+ "razorpay_signature" TEXT NOT NULL,
+ "purchasedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "paymentVerified" BOOLEAN NOT NULL DEFAULT false,
+ "purchasedCourseId" INTEGER NOT NULL,
+ "purchasedById" TEXT NOT NULL,
+
+ CONSTRAINT "Purchase_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Receipt_razorpayOrderId_key" ON "Receipt"("razorpayOrderId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Purchase_receiptId_key" ON "Purchase"("receiptId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Purchase_razorpay_payment_id_key" ON "Purchase"("razorpay_payment_id");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Purchase_razorpay_order_id_key" ON "Purchase"("razorpay_order_id");
+
+-- AddForeignKey
+ALTER TABLE "Receipt" ADD CONSTRAINT "Receipt_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Receipt" ADD CONSTRAINT "Receipt_courseId_fkey" FOREIGN KEY ("courseId") REFERENCES "Course"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Purchase" ADD CONSTRAINT "Purchase_receiptId_fkey" FOREIGN KEY ("receiptId") REFERENCES "Receipt"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Purchase" ADD CONSTRAINT "Purchase_purchasedCourseId_fkey" FOREIGN KEY ("purchasedCourseId") REFERENCES "Course"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Purchase" ADD CONSTRAINT "Purchase_purchasedById_fkey" FOREIGN KEY ("purchasedById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index a4bc79d74..d410addeb 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -1,217 +1,247 @@
-generator client {
- provider = "prisma-client-js"
- binaryTargets = ["native", "linux-musl-arm64-openssl-3.0.x"]
-}
-
-datasource db {
- provider = "postgresql"
- url = env("DATABASE_URL")
-}
-
-model Course {
- id Int @id @default(autoincrement())
- appxCourseId Int
- discordRoleId String
- title String
- imageUrl String
- description String
- openToEveryone Boolean @default(false)
- slug String
- content CourseContent[]
- purchasedBy UserPurchases[]
- bookmarks Bookmark[]
-}
-
-model UserPurchases {
- user User @relation(fields: [userId], references: [id])
- userId String
- course Course @relation(fields: [courseId], references: [id])
- courseId Int
- assignedAt DateTime @default(now())
-
- @@id([userId, courseId])
-}
-
-model Content {
- id Int @id @default(autoincrement())
- type String @default("folder")
- title String
- hidden Boolean @default(false)
- description String?
- thumbnail String?
- parentId Int?
- parent Content? @relation("ContentToContent", fields: [parentId], references: [id])
- videoProgress VideoProgress[]
- children Content[] @relation("ContentToContent")
- courses CourseContent[]
- createdAt DateTime @default(now())
- VideoMetadata VideoMetadata?
- NotionMetadata NotionMetadata?
- notionMetadataId Int?
- comments Comment[]
- commentsCount Int @default(0)
- bookmark Bookmark?
-}
-
-model CourseContent {
- course Course @relation(fields: [courseId], references: [id])
- courseId Int
- content Content @relation(fields: [contentId], references: [id])
- contentId Int
-
- @@id([courseId, contentId])
-}
-
-model NotionMetadata {
- id Int @id @default(autoincrement())
- contentId Int
- content Content @relation(fields: [contentId], references: [id])
- notionId String
-
- @@unique([contentId])
-}
-
-model VideoMetadata {
- id Int @id @default(autoincrement())
- contentId Int
- video_1080p_mp4_1 String? // Link to 1080p mp4 quality video variant 1
- video_1080p_mp4_2 String? // Link to 1080p mp4 quality video variant 2
- video_1080p_mp4_3 String? // Link to 1080p mp4 quality video variant 3
- video_1080p_mp4_4 String? // Link to 1080p mp4 quality video variant 4
- video_1080p_1 String? // Link to 1080p quality video variant 1
- video_1080p_2 String? // Link to 1080p quality video variant 2
- video_1080p_3 String? // Link to 1080p quality video variant 3
- video_1080p_4 String? // Link to 1080p quality video variant 4
- video_720p_mp4_1 String? // Link to 720p mp4 quality video variant 1
- video_720p_mp4_2 String? // Link to 720p mp4 quality video variant 2
- video_720p_mp4_3 String? // Link to 720p mp4 quality video variant 3
- video_720p_mp4_4 String? // Link to 720p mp4 quality video variant 4
- video_720p_1 String? // Link to 720p quality video variant 1
- video_720p_2 String? // Link to 720p quality video variant 2
- video_720p_3 String? // Link to 720p quality video variant 3
- video_720p_4 String? // Link to 720p quality video variant 4
- video_360p_mp4_1 String? // Link to 360p mp4 quality video variant 1
- video_360p_mp4_2 String? // Link to 360p mp4 quality video variant 2
- video_360p_mp4_3 String? // Link to 360p mp4 quality video variant 3
- video_360p_mp4_4 String? // Link to 360p mp4 quality video variant 4
- video_360p_1 String? // Link to 360p quality video variant 1
- video_360p_2 String? // Link to 360p quality video variant 2
- video_360p_3 String? // Link to 360p quality video variant 3
- video_360p_4 String? // Link to 360p quality video variant 4
- subtitles String? // Link to subtitles file
- segments Json?
- content Content @relation(fields: [contentId], references: [id])
- slides String? // link to slides
- thumbnail_mosiac_url String?
- duration Int?
-
- @@unique([contentId])
-}
-
-model Session {
- id String @id @default(cuid())
- sessionToken String @unique
- userId String
- expires DateTime
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
-}
-
-model User {
- id String @id @default(cuid())
- name String?
- email String? @unique
- token String?
- sessions Session[]
- purchases UserPurchases[]
- videoProgress VideoProgress[]
- comments Comment[]
- votes Vote[]
- discordConnect DiscordConnect?
- disableDrm Boolean @default(false)
- bookmarks Bookmark[]
- password String?
- appxUserId String?
- appxUsername String?
-}
-
-model DiscordConnect {
- id String @id @default(cuid())
- username String
- discordId String @unique
- userId String @unique
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
-}
-
-model DiscordConnectBulk {
- id String @id @default(cuid())
- username String
- discordId String
- userId String
-}
-
-model VideoProgress {
- id Int @id @default(autoincrement())
- userId String
- contentId Int
- currentTimestamp Int
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
- content Content @relation(fields: [contentId], references: [id], onDelete: Cascade)
- markAsCompleted Boolean @default(false)
- updatedAt DateTime @default(now()) @updatedAt
-
- @@unique([contentId, userId])
-}
-
-model Bookmark {
- id Int @id @default(autoincrement())
- userId String
- contentId Int @unique
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
- content Content @relation(fields: [contentId], references: [id], onDelete: Cascade)
- createdAt DateTime @default(now())
- courseId Int
- course Course @relation(fields: [courseId], references: [id], onDelete: Cascade)
-}
-
-model Comment {
- id Int @id @default(autoincrement())
- content String
- commentType CommentType @default(DEFAULT)
- approved Boolean @default(false)
- contentId Int
- commentedOn Content @relation(fields: [contentId], references: [id])
- parentId Int?
- parent Comment? @relation("ParentComment", fields: [parentId], references: [id])
- children Comment[] @relation("ParentComment")
- userId String
- user User @relation(fields: [userId], references: [id])
- upvotes Int @default(0)
- downvotes Int @default(0)
- repliesCount Int @default(0)
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
- votes Vote[]
- isPinned Boolean @default(false)
-}
-
-model Vote {
- id Int @id @default(autoincrement())
- commentId Int
- comment Comment @relation(fields: [commentId], references: [id])
- userId String
- user User @relation(fields: [userId], references: [id])
- voteType VoteType // enum
- createdAt DateTime @default(now())
-
- @@unique([commentId, userId])
-}
-
-enum VoteType {
- UPVOTE
- DOWNVOTE
-}
-
-enum CommentType {
- INTRO
- DEFAULT
-}
+generator client {
+ provider = "prisma-client-js"
+ binaryTargets = ["native", "linux-musl-arm64-openssl-3.0.x"]
+}
+
+datasource db {
+ provider = "postgresql"
+ url = env("DATABASE_URL")
+}
+
+model Course {
+ id Int @id @default(autoincrement())
+ price Int @default(1000)
+ appxCourseId Int
+ discordRoleId String
+ title String
+ imageUrl String
+ description String
+ openToEveryone Boolean @default(false)
+ slug String
+ content CourseContent[]
+ purchasedBy UserPurchases[]
+ bookmarks Bookmark[]
+ purchases Purchase[]
+ receipts Receipt[]
+}
+
+model UserPurchases {
+ user User @relation(fields: [userId], references: [id])
+ userId String
+ course Course @relation(fields: [courseId], references: [id])
+ courseId Int
+ assignedAt DateTime @default(now())
+
+ @@id([userId, courseId])
+}
+
+model Content {
+ id Int @id @default(autoincrement())
+ type String @default("folder")
+ title String
+ hidden Boolean @default(false)
+ description String?
+ thumbnail String?
+ parentId Int?
+ parent Content? @relation("ContentToContent", fields: [parentId], references: [id])
+ videoProgress VideoProgress[]
+ children Content[] @relation("ContentToContent")
+ courses CourseContent[]
+ createdAt DateTime @default(now())
+ VideoMetadata VideoMetadata?
+ NotionMetadata NotionMetadata?
+ notionMetadataId Int?
+ comments Comment[]
+ commentsCount Int @default(0)
+ bookmark Bookmark?
+}
+
+model CourseContent {
+ course Course @relation(fields: [courseId], references: [id])
+ courseId Int
+ content Content @relation(fields: [contentId], references: [id])
+ contentId Int
+
+ @@id([courseId, contentId])
+}
+
+model NotionMetadata {
+ id Int @id @default(autoincrement())
+ contentId Int
+ content Content @relation(fields: [contentId], references: [id])
+ notionId String
+
+ @@unique([contentId])
+}
+
+model VideoMetadata {
+ id Int @id @default(autoincrement())
+ contentId Int
+ video_1080p_mp4_1 String? // Link to 1080p mp4 quality video variant 1
+ video_1080p_mp4_2 String? // Link to 1080p mp4 quality video variant 2
+ video_1080p_mp4_3 String? // Link to 1080p mp4 quality video variant 3
+ video_1080p_mp4_4 String? // Link to 1080p mp4 quality video variant 4
+ video_1080p_1 String? // Link to 1080p quality video variant 1
+ video_1080p_2 String? // Link to 1080p quality video variant 2
+ video_1080p_3 String? // Link to 1080p quality video variant 3
+ video_1080p_4 String? // Link to 1080p quality video variant 4
+ video_720p_mp4_1 String? // Link to 720p mp4 quality video variant 1
+ video_720p_mp4_2 String? // Link to 720p mp4 quality video variant 2
+ video_720p_mp4_3 String? // Link to 720p mp4 quality video variant 3
+ video_720p_mp4_4 String? // Link to 720p mp4 quality video variant 4
+ video_720p_1 String? // Link to 720p quality video variant 1
+ video_720p_2 String? // Link to 720p quality video variant 2
+ video_720p_3 String? // Link to 720p quality video variant 3
+ video_720p_4 String? // Link to 720p quality video variant 4
+ video_360p_mp4_1 String? // Link to 360p mp4 quality video variant 1
+ video_360p_mp4_2 String? // Link to 360p mp4 quality video variant 2
+ video_360p_mp4_3 String? // Link to 360p mp4 quality video variant 3
+ video_360p_mp4_4 String? // Link to 360p mp4 quality video variant 4
+ video_360p_1 String? // Link to 360p quality video variant 1
+ video_360p_2 String? // Link to 360p quality video variant 2
+ video_360p_3 String? // Link to 360p quality video variant 3
+ video_360p_4 String? // Link to 360p quality video variant 4
+ subtitles String? // Link to subtitles file
+ segments Json?
+ content Content @relation(fields: [contentId], references: [id])
+ slides String? // link to slides
+ thumbnail_mosiac_url String?
+ duration Int?
+
+ @@unique([contentId])
+}
+
+model Session {
+ id String @id @default(cuid())
+ sessionToken String @unique
+ userId String
+ expires DateTime
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+}
+
+model User {
+ id String @id @default(cuid())
+ name String?
+ email String? @unique
+ token String?
+ sessions Session[]
+ purchases UserPurchases[]
+ videoProgress VideoProgress[]
+ comments Comment[]
+ votes Vote[]
+ discordConnect DiscordConnect?
+ disableDrm Boolean @default(false)
+ bookmarks Bookmark[]
+ password String?
+ appxUserId String?
+ appxUsername String?
+ transactions Purchase[]
+ receipts Receipt[]
+}
+
+model Receipt{
+ id String @id @default(cuid())
+ user User @relation(fields: [userId], references: [id])
+ userId String
+ course Course @relation(fields: [courseId], references: [id])
+ courseId Int @default(0)
+ razorpayOrderId String? @unique
+ purchase Purchase?
+}
+
+model Purchase {
+ id String @id @default(cuid())
+ receipt Receipt @relation(fields: [receiptId], references: [id])
+ receiptId String @unique
+ razorpay_payment_id String @unique
+ razorpay_order_id String @unique
+ razorpay_signature String
+ purchasedAt DateTime @default(now())
+ paymentVerified Boolean @default(false)
+ purchasedCourse Course @relation(fields: [purchasedCourseId], references: [id])
+ purchasedCourseId Int
+ purchasedBy User @relation(fields: [purchasedById], references: [id])
+ purchasedById String
+}
+
+model DiscordConnect {
+ id String @id @default(cuid())
+ username String
+ discordId String @unique
+ userId String @unique
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+}
+
+model DiscordConnectBulk {
+ id String @id @default(cuid())
+ username String
+ discordId String
+ userId String
+}
+
+model VideoProgress {
+ id Int @id @default(autoincrement())
+ userId String
+ contentId Int
+ currentTimestamp Int
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+ content Content @relation(fields: [contentId], references: [id], onDelete: Cascade)
+ markAsCompleted Boolean @default(false)
+ updatedAt DateTime @default(now()) @updatedAt
+
+ @@unique([contentId, userId])
+}
+
+model Bookmark {
+ id Int @id @default(autoincrement())
+ userId String
+ contentId Int @unique
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+ content Content @relation(fields: [contentId], references: [id], onDelete: Cascade)
+ createdAt DateTime @default(now())
+ courseId Int
+ course Course @relation(fields: [courseId], references: [id], onDelete: Cascade)
+}
+
+model Comment {
+ id Int @id @default(autoincrement())
+ content String
+ commentType CommentType @default(DEFAULT)
+ approved Boolean @default(false)
+ contentId Int
+ commentedOn Content @relation(fields: [contentId], references: [id])
+ parentId Int?
+ parent Comment? @relation("ParentComment", fields: [parentId], references: [id])
+ children Comment[] @relation("ParentComment")
+ userId String
+ user User @relation(fields: [userId], references: [id])
+ upvotes Int @default(0)
+ downvotes Int @default(0)
+ repliesCount Int @default(0)
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ votes Vote[]
+ isPinned Boolean @default(false)
+}
+
+model Vote {
+ id Int @id @default(autoincrement())
+ commentId Int
+ comment Comment @relation(fields: [commentId], references: [id])
+ userId String
+ user User @relation(fields: [userId], references: [id])
+ voteType VoteType // enum
+ createdAt DateTime @default(now())
+
+ @@unique([commentId, userId])
+}
+
+enum VoteType {
+ UPVOTE
+ DOWNVOTE
+}
+
+enum CommentType {
+ INTRO
+ DEFAULT
+}
diff --git a/src/app/api/auth/register/route.ts b/src/app/api/auth/register/route.ts
new file mode 100644
index 000000000..8efbf1b82
--- /dev/null
+++ b/src/app/api/auth/register/route.ts
@@ -0,0 +1,36 @@
+import { NextRequest, NextResponse } from 'next/server';
+import db from '@/db';
+import bcrypt from 'bcrypt';
+import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';
+
+export async function POST(req: NextRequest) {
+ try {
+ const { name, email, password } = await req.json();
+
+ const hashedPassword = await bcrypt.hash(password, 10);
+ await db.user.create({
+ data: {
+ email,
+ name,
+ password: hashedPassword,
+ },
+ });
+
+ return NextResponse.json(
+ { message: 'Account created sucessfully' },
+ { status: 201 },
+ );
+ } catch (e) {
+ if (e instanceof PrismaClientKnownRequestError) {
+ return NextResponse.json(
+ { error: 'Email already taken.' },
+ { status: 400 },
+ );
+ }
+ console.log(e);
+ return NextResponse.json(
+ { error: 'Failed to parse JSON input' },
+ { status: 400 },
+ );
+ }
+}
diff --git a/src/app/api/razorpay/route.ts b/src/app/api/razorpay/route.ts
new file mode 100644
index 000000000..dddd2906e
--- /dev/null
+++ b/src/app/api/razorpay/route.ts
@@ -0,0 +1,81 @@
+import { NextRequest, NextResponse } from 'next/server';
+import Razorpay from 'razorpay';
+import db from '@/db';
+import { z } from 'zod';
+
+const schema = z.object({
+ courseId: z.number(),
+ userId: z.string(),
+});
+
+const razorpay = new Razorpay({
+ key_id: process.env.RAZORPAY_KEY_ID!,
+ key_secret: process.env.RAZORPAY_SECRET!,
+});
+
+export async function POST(req: NextRequest) {
+ const reqBody = await req.json();
+ const body = schema.safeParse(reqBody);
+
+ if (!body.success) {
+ return NextResponse.json(
+ {
+ error: 'Error parsing the body of the request',
+ },
+ {
+ status: 422,
+ },
+ );
+ }
+
+ const course = await db.course.findFirst({
+ where: {
+ id: body.data.courseId,
+ },
+ });
+
+ if (!course)
+ return NextResponse.json({ error: 'Course Not Found' }, { status: 404 });
+
+ const receipt = await db.receipt.create({
+ data: {
+ userId: body.data.userId,
+ courseId: body.data.courseId,
+ },
+ });
+
+ const payment_capture = 1;
+ const amount = course.price;
+ const options = {
+ amount: (amount * 100).toString(),
+ currency: 'INR',
+ receipt: receipt.id,
+ payment_capture,
+ // notes: {
+
+ // }
+ };
+
+ try {
+ const response = await razorpay.orders.create(options);
+
+ await db.receipt.update({
+ where: {
+ id: receipt.id,
+ },
+ data: {
+ razorpayOrderId: response.id,
+ },
+ });
+
+ return NextResponse.json({
+ id: response.id,
+ currency: response.currency,
+ amount: response.amount,
+ razorPayKey: process.env.RAZORPAY_KEY_ID,
+ });
+ } catch (err) {
+ console.log(err);
+ return NextResponse.json(err);
+ }
+}
diff --git a/src/app/api/razorpay/verify/route.ts b/src/app/api/razorpay/verify/route.ts
new file mode 100644
index 000000000..aad654f4b
--- /dev/null
+++ b/src/app/api/razorpay/verify/route.ts
@@ -0,0 +1,80 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { validatePaymentVerification } from 'razorpay/dist/utils/razorpay-utils';
+import { z } from 'zod';
+import db from '@/db';
+
+const razorPayZodSchema = z.object({
+ userId: z.string(),
+ courseId: z.number(),
+ razorpay_order_id: z.string(),
+ razorpay_payment_id: z.string(),
+ razorpay_signature: z.string(),
+});
+
+export async function POST(req: NextRequest) {
+ const jsonBody = await req.json();
+
+ const body = razorPayZodSchema.safeParse(jsonBody);
+
+ if (!body.success) {
+ return NextResponse.json(
+ {
+ error: 'Invalid Body',
+ },
+ { status: 422 },
+ );
+ }
+
+ const {
+ razorpay_order_id,
+ razorpay_payment_id,
+ razorpay_signature,
+ courseId,
+ userId,
+ } = body.data;
+
+ const isPaymentValid = validatePaymentVerification(
+ {
+ order_id: razorpay_order_id,
+ payment_id: razorpay_payment_id,
+ },
+ razorpay_signature,
+ process.env.RAZORPAY_SECRET!,
+ );
+
+ if (!isPaymentValid)
+ return NextResponse.json(
+ { error: 'Payment not verified' },
+ { status: 404 },
+ );
+
+ const receipt = await db.receipt.findFirst({
+ where: {
+ razorpayOrderId: razorpay_order_id,
+ },
+ });
+
+ if (!receipt)
+ return NextResponse.json({ error: 'Receipt not found' }, { status: 404 });
+
+ await db.purchase.create({
+ data: {
+ receiptId: receipt.id,
+ razorpay_order_id,
+ razorpay_payment_id,
+ razorpay_signature,
+ paymentVerified: isPaymentValid,
+ purchasedById: userId,
+ purchasedCourseId: courseId,
+ },
+ });
+
+ await db.userPurchases.create({
+ data: {
+ userId,
+ courseId,
+ },
+ });
+
+ return NextResponse.json({ message: 'Purchase Successful' }, { status: 200 });
+}
diff --git a/src/app/new-courses/[courseId]/page.tsx b/src/app/new-courses/[courseId]/page.tsx
new file mode 100644
index 000000000..7c08f972e
--- /dev/null
+++ b/src/app/new-courses/[courseId]/page.tsx
@@ -0,0 +1,52 @@
+import { RazorPayComponent } from '@/components/razorpay/Razorpay';
+import { Button } from '@/components/ui/button';
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+} from '@/components/ui/card';
+import Image from 'next/image';
+import db from '@/db';
+import { authOptions } from '@/lib/auth';
+import { getServerSession } from 'next-auth';
+import { redirect } from 'next/navigation';
+
+export default async function CourseDetails({
+ params,
+}: {
+ params: { courseId: string[] };
+}) {
+ const session = await getServerSession(authOptions);
+ if (!session?.user) {
+ redirect('/signin');
+ }
+ const course = await db.course.findFirst({
+ where: {
+ id: Number(params.courseId),
+ },
+ });
+ if (!course) return null;
+
+ return (
+
Join now
{' '} +