From 59c18e5ed6344665ce1b732250b94ab9fde8b08d Mon Sep 17 00:00:00 2001 From: Dhruvil Mehta <68022411+dhruvilmehta@users.noreply.github.com> Date: Mon, 20 May 2024 18:41:55 -0700 Subject: [PATCH] Added user registration and Course purchase system --- package.json | 1 + .../migration.sql | 54 +++++ prisma/schema.prisma | 132 +++++++----- src/app/api/auth/register/route.ts | 36 ++++ src/app/api/razorpay/route.ts | 83 ++++++++ src/app/api/razorpay/verify/route.ts | 80 ++++++++ src/app/new-courses/[courseId]/page.tsx | 66 ++++++ src/app/new-courses/page.tsx | 60 ++++++ src/app/receipts/page.tsx | 57 +++++ src/app/signup/page.tsx | 15 ++ src/components/Appbar.tsx | 11 + src/components/Signup.tsx | 194 ++++++++++++++++++ src/components/VideoPlayer2.tsx | 2 +- .../profile-menu/ProfileDropdown.tsx | 13 +- src/components/razorpay/Razorpay.tsx | 102 +++++++++ src/lib/auth.ts | 12 +- tsconfig.json | 10 +- 17 files changed, 866 insertions(+), 62 deletions(-) create mode 100644 prisma/migrations/20240521013022_add_user_course_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/receipts/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 d6542fb01..60bbe4312 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "nextjs-toploader": "^1.6.11", "node-fetch": "^3.3.2", "notion-client": "^6.16.0", + "razorpay": "^2.9.2", "pdf-lib": "^1.17.1", "react": "^18", "react-dom": "^18", diff --git a/prisma/migrations/20240521013022_add_user_course_purchase/migration.sql b/prisma/migrations/20240521013022_add_user_course_purchase/migration.sql new file mode 100644 index 000000000..629412c89 --- /dev/null +++ b/prisma/migrations/20240521013022_add_user_course_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 39874f139..e04408163 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -10,6 +10,7 @@ datasource db { model Course { id Int @id @default(autoincrement()) + price Int @default(1000) appxCourseId Int discordRoleId String title String @@ -19,8 +20,10 @@ model Course { slug String content CourseContent[] purchasedBy UserPurchases[] + purchases Purchase[] + receipts Receipt[] certificate Certificate[] - certIssued Boolean @default(false) + certIssued Boolean @default(false) } model UserPurchases { @@ -64,12 +67,12 @@ model CourseContent { } model Certificate { - id String @id @default(cuid()) - slug String @default("certId") - user User @relation(fields: [userId], references: [id]) - userId String - course Course @relation(fields: [courseId], references: [id]) - courseId Int + id String @id @default(cuid()) + slug String @default("certId") + user User @relation(fields: [userId], references: [id]) + userId String + course Course @relation(fields: [courseId], references: [id]) + courseId Int @@unique([userId, courseId]) } @@ -147,6 +150,33 @@ model User { questions Question[] answers Answer[] certificate Certificate[] + 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 { @@ -206,74 +236,76 @@ model Comment { votes Vote[] isPinned Boolean @default(false) } + model Question { - id Int @id @default(autoincrement()) - title String - content String - slug String @unique - createdAt DateTime @default(now()) - author User @relation(fields: [authorId], references: [id]) - authorId String - upvotes Int @default(0) - downvotes Int @default(0) + id Int @id @default(autoincrement()) + title String + content String + slug String @unique + createdAt DateTime @default(now()) + author User @relation(fields: [authorId], references: [id]) + authorId String + upvotes Int @default(0) + downvotes Int @default(0) totalanswers Int @default(0) - answers Answer[] - votes Vote[] - tags String[] - updatedAt DateTime @updatedAt + answers Answer[] + votes Vote[] + tags String[] + updatedAt DateTime @updatedAt @@index([authorId]) } model Answer { - id Int @id @default(autoincrement()) - content String - createdAt DateTime @default(now()) - question Question @relation(fields: [questionId], references: [id]) - questionId Int - author User @relation(fields: [authorId], references: [id]) - authorId String - votes Vote[] - upvotes Int @default(0) - downvotes Int @default(0) - totalanswers Int @default(0) - parentId Int? - responses Answer[] @relation("AnswerToAnswer") - parent Answer? @relation("AnswerToAnswer", fields: [parentId], references: [id]) - updatedAt DateTime @updatedAt + id Int @id @default(autoincrement()) + content String + createdAt DateTime @default(now()) + question Question @relation(fields: [questionId], references: [id]) + questionId Int + author User @relation(fields: [authorId], references: [id]) + authorId String + votes Vote[] + upvotes Int @default(0) + downvotes Int @default(0) + totalanswers Int @default(0) + parentId Int? + responses Answer[] @relation("AnswerToAnswer") + parent Answer? @relation("AnswerToAnswer", fields: [parentId], references: [id]) + updatedAt DateTime @updatedAt @@index([questionId]) @@index([authorId]) @@index([parentId]) } - model Vote { - id Int @id @default(autoincrement()) - questionId Int? - question Question? @relation(fields: [questionId], references: [id]) - answerId Int? - answer Answer? @relation(fields: [answerId], references: [id]) - commentId Int? - comment Comment? @relation(fields: [commentId], references: [id]) - userId String - user User @relation(fields: [userId], references: [id]) - voteType VoteType - createdAt DateTime @default(now()) + id Int @id @default(autoincrement()) + questionId Int? + question Question? @relation(fields: [questionId], references: [id]) + answerId Int? + answer Answer? @relation(fields: [answerId], references: [id]) + commentId Int? + comment Comment? @relation(fields: [commentId], references: [id]) + userId String + user User @relation(fields: [userId], references: [id]) + voteType VoteType + createdAt DateTime @default(now()) - @@unique([questionId, userId]) - @@unique([answerId, userId]) - @@unique([commentId, userId]) + @@unique([questionId, userId]) + @@unique([answerId, userId]) + @@unique([commentId, userId]) } enum VoteType { UPVOTE DOWNVOTE } + enum PostType { QUESTION ANSWER } + 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..e06dd515a --- /dev/null +++ b/src/app/api/razorpay/route.ts @@ -0,0 +1,83 @@ +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: { + userId: body.data.userId, + courseId: body.data.courseId, + receipt: receipt.id, + }, + }; + + 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..daf0e9bc1 --- /dev/null +++ b/src/app/new-courses/[courseId]/page.tsx @@ -0,0 +1,66 @@ +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
Course does not exist
; + + const ifPurchasedByUser = + (await db.userPurchases.count({ + where: { + userId: session.user.id, + courseId: course.id, + }, + })) > 0 + ? true + : false; + + return ( + + + + + + {course.title} + {course.description} + + + + {ifPurchasedByUser ? ( + + ) : ( + + )} + + + ); +} diff --git a/src/app/new-courses/page.tsx b/src/app/new-courses/page.tsx new file mode 100644 index 000000000..297cfb690 --- /dev/null +++ b/src/app/new-courses/page.tsx @@ -0,0 +1,60 @@ +import { Button } from '@/components/ui/button'; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from '@/components/ui/card'; +import db from '@/db'; +import { authOptions } from '@/lib/auth'; +import { getServerSession } from 'next-auth'; +import Image from 'next/image'; +import Link from 'next/link'; +import { redirect } from 'next/navigation'; + +export default async function PurchasePage() { + const session = await getServerSession(authOptions); + if (!session?.user) { + redirect('/signin'); + } + + const courses = await db.course.findMany({ + where: { + purchasedBy: { + none: { + userId: session.user.id, + }, + }, + }, + }); + + return ( + <> +
+ {courses.length === 0 ? ( +
No New Courses to Buy
+ ) : ( + courses.map((course) => ( + + + + + + {course.title} + {course.description} + + + + + + + + + )) + )} +
+ + ); +} diff --git a/src/app/receipts/page.tsx b/src/app/receipts/page.tsx new file mode 100644 index 000000000..1c2133f92 --- /dev/null +++ b/src/app/receipts/page.tsx @@ -0,0 +1,57 @@ +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from '@/components/ui/card'; +import db from '@/db'; +import { authOptions } from '@/lib/auth'; +import { getServerSession } from 'next-auth'; +import Image from 'next/image'; +import { redirect } from 'next/navigation'; + +export default async function Receipts() { + const session = await getServerSession(authOptions); + if (!session?.user) { + redirect('/signin'); + } + + const courses = await db.purchase.findMany({ + where: { + purchasedById: session.user.id, + }, + select: { + receiptId: true, + purchasedCourse: true, + }, + }); + + return ( + <> +
+ {courses.length === 0 ? ( +
No Receipts found
+ ) : ( + courses.map((course) => ( + + + + + + {course.purchasedCourse.title} + + {course.purchasedCourse.description} + + + + Receipt Id: {course.receiptId} + + + )) + )} +
+ + ); +} diff --git a/src/app/signup/page.tsx b/src/app/signup/page.tsx new file mode 100644 index 000000000..e39d09db6 --- /dev/null +++ b/src/app/signup/page.tsx @@ -0,0 +1,15 @@ +import { authOptions } from '@/lib/auth'; +import { getServerSession } from 'next-auth'; +import { redirect } from 'next/navigation'; +import React from 'react'; +import Signup from '@/components/Signup'; + +const SignupPage = async () => { + const session = await getServerSession(authOptions); + if (session?.user) { + redirect('/'); + } + return ; +}; + +export default SignupPage; diff --git a/src/components/Appbar.tsx b/src/components/Appbar.tsx index 8bd84452d..0705c455d 100644 --- a/src/components/Appbar.tsx +++ b/src/components/Appbar.tsx @@ -1,6 +1,7 @@ 'use client'; import Link from 'next/link'; +import { useRouter } from 'next/navigation'; import { AppbarAuth } from './AppbarAuth'; import { useSession } from 'next-auth/react'; import { useRecoilState } from 'recoil'; @@ -17,6 +18,8 @@ import ProfileDropdown from './profile-menu/ProfileDropdown'; import { ThemeToggler } from './ThemeToggler'; export const Appbar = () => { + const router = useRouter(); + // const session = useSession(); const { data: session, status: sessionStatus } = useSession(); const [sidebarOpen, setSidebarOpen] = useRecoilState(sidebarOpenAtom); const currentPath = usePathname(); @@ -43,6 +46,14 @@ export const Appbar = () => {
+ +
{/* Search Bar for smaller devices */} diff --git a/src/components/Signup.tsx b/src/components/Signup.tsx new file mode 100644 index 000000000..b81f75575 --- /dev/null +++ b/src/components/Signup.tsx @@ -0,0 +1,194 @@ +'use client'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Toaster } from '@/components/ui/sonner'; +import { signIn } from 'next-auth/react'; +import { useRouter } from 'next/navigation'; +import React, { useRef, useState } from 'react'; + +import { toast } from 'sonner'; +const Signup = () => { + const [isPasswordVisible, setIsPasswordVisible] = useState(false); + const [requiredError, setRequiredError] = useState({ + emailReq: false, + passReq: false, + }); + + function togglePasswordVisibility() { + setIsPasswordVisible((prevState: any) => !prevState); + } + const router = useRouter(); + const email = useRef(''); + const password = useRef(''); + const name = useRef(''); + + const handleSubmit = async (e?: React.FormEvent) => { + if (e) { + e.preventDefault(); + } + + if (!email.current || !password.current) { + setRequiredError({ + emailReq: email.current ? false : true, + passReq: password.current ? false : true, + }); + return; + } + + const register = await fetch('/api/auth/register', { + method: 'POST', + body: JSON.stringify({ + name: name.current, + email: email.current, + password: password.current, + }), + }); + const data = await register.json(); + + if (register.status !== 201) { + toast(data.error, { + action: { + label: 'Close', + onClick: () => console.log('Closed Toast'), + }, + }); + return; + } + + const res = await signIn('credentials', { + username: email.current, + password: password.current, + redirect: false, + }); + + if (!res?.error) { + router.push('/'); + } else { + toast('Error Signing in', { + action: { + label: 'Close', + onClick: () => console.log('Closed Toast'), + }, + }); + } + }; + return ( +
+ + + Signup + + +
+
+ + { + name.current = e.target.value; + }} + /> +
+
+ + { + setRequiredError((prevState) => ({ + ...prevState, + emailReq: false, + })); + email.current = e.target.value; + }} + /> + {requiredError.emailReq && ( + Email is required + )} +
+
+ +
+ { + setRequiredError((prevState) => ({ + ...prevState, + passReq: false, + })); + password.current = e.target.value; + }} + onKeyDown={async (e) => { + if (e.key === 'Enter') { + setIsPasswordVisible(false); + handleSubmit(); + } + }} + /> + +
+ {requiredError.passReq && ( + Password is required + )} +
+
+ +
+
+ +
+ ); +}; + +export default Signup; diff --git a/src/components/VideoPlayer2.tsx b/src/components/VideoPlayer2.tsx index 526d2dca1..d7db7298a 100644 --- a/src/components/VideoPlayer2.tsx +++ b/src/components/VideoPlayer2.tsx @@ -147,7 +147,7 @@ export const VideoPlayer: FunctionComponent = ({ player.currentTime(player.currentTime() - 5); event.stopPropagation(); break; - case 'ArrowUp': // Arrow up for increasing volume + case 'ArrowUp': // Arrow up for increasing volume event.preventDefault(); player.volume(player.volume() + 0.1); event.stopPropagation(); diff --git a/src/components/profile-menu/ProfileDropdown.tsx b/src/components/profile-menu/ProfileDropdown.tsx index 88cc5e178..6ce4ccc0e 100644 --- a/src/components/profile-menu/ProfileDropdown.tsx +++ b/src/components/profile-menu/ProfileDropdown.tsx @@ -1,7 +1,13 @@ 'use client'; import Link from 'next/link'; -import { BookmarkIcon, HistoryIcon, LogOutIcon, User2Icon } from 'lucide-react'; +import { + BookmarkIcon, + HistoryIcon, + LogOutIcon, + Receipt, + User2Icon, +} from 'lucide-react'; import { DropdownMenu, DropdownMenuContent, @@ -26,6 +32,11 @@ const ProfileDropdown = () => { icon: , label: 'Bookmarks', }, + { + href: '/receipts', + icon: , + label: 'Receipts', + }, ]; return ( diff --git a/src/components/razorpay/Razorpay.tsx b/src/components/razorpay/Razorpay.tsx new file mode 100644 index 000000000..95a98ad34 --- /dev/null +++ b/src/components/razorpay/Razorpay.tsx @@ -0,0 +1,102 @@ +'use client'; +import { Button } from '../ui/button'; +import { toast } from 'sonner'; +import { useRouter } from 'next/navigation'; + +const initializeRazorpay = () => { + return new Promise((resolve) => { + const script = document.createElement('script'); + script.src = 'https://checkout.razorpay.com/v1/checkout.js'; + + script.onload = () => { + resolve(true); + }; + script.onerror = () => { + resolve(false); + }; + + document.body.appendChild(script); + }); +}; + +type RazorPayInputType = { + courseId: number; + userId: string; +}; + +export const RazorPayComponent = ({ courseId, userId }: RazorPayInputType) => { + const router = useRouter(); + + const makePayment = async () => { + const res = await initializeRazorpay(); + + if (!res) { + alert('Razorpay SDK Failed to load'); + return; + } + + const data = await fetch('/api/razorpay', { + method: 'POST', + body: JSON.stringify({ + courseId, + userId, + }), + }).then((res) => res.json()); + + const options = { + key_id: data.razorPayKey, + name: '100xdevs', + currency: data.currency, + amount: data.amount, + order_id: data.id, + description: 'Thank you for purchasing course', + image: '/harkirat.png', + handler: async (response: any) => { + const verify = await fetch('/api/razorpay/verify', { + method: 'POST', + body: JSON.stringify({ ...response, userId, courseId }), + }); + const data = await verify.json(); + if (verify.status !== 200) { + toast(data.error, { + action: { + label: 'Close', + onClick: () => console.log('Closed Toast'), + }, + }); + } else { + toast(data.message, { + action: { + label: 'Close', + onClick: () => console.log('Closed Toast'), + }, + }); + router.push('/'); + } + }, + }; + + const paymentObject = new (window as any).Razorpay(options); + paymentObject.on('payment.failed', () => { + toast('Payment did not execute', { + action: { + label: 'Close', + onClick: () => console.log('Closed Toast'), + }, + }); + }); + paymentObject.open(); + }; + + return ( +
+ +
+ ); +}; diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 8d6ca3eb8..0a669a6d2 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -138,11 +138,13 @@ export const authOptions = { name: true, }, }); - if ( - userDb && - userDb.password && - (await bcrypt.compare(credentials.password, userDb.password)) - ) { + if (userDb && userDb.password) { + const valid = await bcrypt.compare( + credentials.password, + userDb.password, + ); + if (!valid) return null; + const jwt = await generateJWT({ id: userDb.id, }); diff --git a/tsconfig.json b/tsconfig.json index cec286f06..1f308869a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,14 +16,14 @@ "incremental": true, "plugins": [ { - "name": "next", - }, + "name": "next" + } ], "paths": { "@/*": ["./src/*"], - "@public/*": ["./public/*"], - }, + "@public/*": ["./public/*"] + } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"], + "exclude": ["node_modules"] }