From 817a962a1e1e583a12d6ddf27fb810b76d74926f Mon Sep 17 00:00:00 2001 From: vyshnav Date: Sat, 23 Nov 2024 11:42:36 +0530 Subject: [PATCH 01/34] add new sjec_user schema --- prisma/dev.db | Bin 102400 -> 102400 bytes prisma/dev.db-journal | Bin 29240 -> 29240 bytes .../20241123060552_sjec_user/migration.sql | 9 +++++++++ prisma/schema.prisma | 12 ++++++++++-- 4 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 prisma/migrations/20241123060552_sjec_user/migration.sql diff --git a/prisma/dev.db b/prisma/dev.db index cf9bda40b768bb08eb59b036d759f3c2285b1fb3..0cd05986bf9bcd7c4e1ad793b6255a139fd92765 100644 GIT binary patch delta 578 zcmZozz}B#UZGyDmDFy}xNgx&nVn!ggny6#Ucxq$95`G0f_OA^5g?tbA{CIcqO7nDc z|KZN%y2a(jxs6kiql^9PWjg7N#jC z#;Hb$Mkb~yW=4jV$*GAch9<_wM#knAX2yvYMy8gGlfxGjuo@W{nHU-x8H*X18JL-Gn(jOtR1%-cB<7=P#k0FQ(`1poj5 diff --git a/prisma/dev.db-journal b/prisma/dev.db-journal index b6bba527844afe892f76cd1fbbfad72db8048de9..b7f17f41f2cf02374fa84a002dff966972556b66 100644 GIT binary patch delta 395 zcmdn-gmK3c#tj@y%zW&>Cv!7NF?X^5-fYNpoxQ%1hmR|?k(a@ifq|9N%pld=GTAg) z*W4o2OxMK7GEFzpz%ohKFeSx2(cI89HO<)2)W|s1A}KK?+0-D#Jk`v=+|bx8IWaZa zEG@;z)WS5y#5mO`(a6L!#mvaiGC4Id#n8mq*vQ!2!pu0)!pPLp$iT?N(9p=(z|6qZ z)JP(}I4dCh;efGg8X-lBD^5?uuh)CmC439=~?pjSDRV6|Fa47g|De(U|zIm>0ISY%|(LZ}POBg9Q003_i BbX5QV delta 192 zcmdn-gmK3c#tj@y%)A^ECUY}MF;3WQD3r&+(a6Wk723$lFnJnR=4Mvz|7-#}nffak z7#PHX7z9{0iwOPT*HDz0JqySa<>hW=;ADNqz`v632VVuJ2`4KXKkG9dIqtWtOF?k6 zA=4D5%_me6nI_k3$xS|>#yMHpAd}79fKA+6b&`gJY+h=4d{AmyYEe;QPP`#94`}q} cFX~HKS%CIrY@VxI&cdQ7v7l|Ugpq;+04dTnzW@LL diff --git a/prisma/migrations/20241123060552_sjec_user/migration.sql b/prisma/migrations/20241123060552_sjec_user/migration.sql new file mode 100644 index 0000000..47ba217 --- /dev/null +++ b/prisma/migrations/20241123060552_sjec_user/migration.sql @@ -0,0 +1,9 @@ +-- CreateTable +CREATE TABLE "sjec_user" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "email" TEXT NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "sjec_user_email_key" ON "sjec_user"("email"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 911e675..1b76cfd 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -66,8 +66,8 @@ model Referral { isUsed Boolean @default(false) created_at DateTime @default(now()) - createdBy User @relation("createdByUser", fields: [createdById], references: [id], onDelete: Cascade) - usedBy User? @relation("usedByUser", fields: [usedById], references: [id]) + createdBy User @relation("createdByUser", fields: [createdById], references: [id], onDelete: Cascade) + usedBy User? @relation("usedByUser", fields: [usedById], references: [id]) forms Form[] } @@ -106,3 +106,11 @@ model Payment { formId String? @map("form_id") Form Form? @relation(fields: [formId], references: [id]) } + +model SjecUser { + id String @id @default(cuid()) + name String + email String @unique + + @@map("sjec_user") +} From 9f8a94afb880c8913eab8d8512ec9ac0b2625650 Mon Sep 17 00:00:00 2001 From: vyshnav Date: Sat, 23 Nov 2024 11:42:57 +0530 Subject: [PATCH 02/34] Add isAllowedToAccess function to check if user is allowed to access --- src/app/actions/is-allowed-to-access.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/app/actions/is-allowed-to-access.ts diff --git a/src/app/actions/is-allowed-to-access.ts b/src/app/actions/is-allowed-to-access.ts new file mode 100644 index 0000000..a9bd130 --- /dev/null +++ b/src/app/actions/is-allowed-to-access.ts @@ -0,0 +1,12 @@ +"use server"; + +import prisma from "@/server/db"; + +export const isAllowedToAccess = async (email: string): Promise => { + const user = await prisma.sjecUser.findFirst({ + where: { + email: email, + }, + }); + return user !== null; +}; From ffaab7e95347eeb041ac6bded79d99190170540d Mon Sep 17 00:00:00 2001 From: vyshnav Date: Sat, 23 Nov 2024 11:43:33 +0530 Subject: [PATCH 03/34] Refactor initial user data retrieval in Users page component --- src/app/admin/users/page.tsx | 44 +++++++++++++----------------------- 1 file changed, 16 insertions(+), 28 deletions(-) diff --git a/src/app/admin/users/page.tsx b/src/app/admin/users/page.tsx index 897dc52..64ba5a4 100644 --- a/src/app/admin/users/page.tsx +++ b/src/app/admin/users/page.tsx @@ -3,32 +3,20 @@ import prisma from "@/server/db"; import React from "react"; export default async function Users() { - let initialUserData = await prisma.user.findMany({ - select: { - id: true, - name: true, - email: true, - role: true, - image: true, - }, - take: 10, - }); - if (initialUserData === null) { - initialUserData = [ - { - id: "1", - name: "Test name", - email: "testEmail@gmail.com", - role: "PARTICIPANT", - image: "https://i.pravatar.cc/300?img=1", - }, - ]; - } - return ( - <> -
- -
- - ); + const initialUserData = await prisma.user.findMany({ + select: { + id: true, + name: true, + email: true, + role: true, + image: true, + }, + take: 10, + }); + + return ( +
+ +
+ ); } From 4fa4cee6f04d6ac85ee6bb71cee6867411271cfe Mon Sep 17 00:00:00 2001 From: vyshnav Date: Sat, 23 Nov 2024 11:43:51 +0530 Subject: [PATCH 04/34] Refactor create-order route to improve error handling and receipt generation --- src/app/api/create-order/route.ts | 60 ++++++++++++++++--------------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/src/app/api/create-order/route.ts b/src/app/api/create-order/route.ts index 28bef50..c314151 100644 --- a/src/app/api/create-order/route.ts +++ b/src/app/api/create-order/route.ts @@ -3,34 +3,36 @@ import { razorpay } from "@/lib/razorpay"; import { randomUUID } from "crypto"; export async function POST(request: NextRequest) { - const { amount } = await request.json(); - if (!amount || typeof amount !== "number" || amount <= 0) { - return NextResponse.json({ error: "Invalid amount" }, { status: 400 }); - } - try { - const order = await razorpay.orders.create({ - amount: amount * 100, - currency: "INR", - receipt: `receipt_${Date.now()}_${randomUUID()}`, - }); + const { amount } = await request.json(); + if (!amount || typeof amount !== "number" || amount <= 0) { + return NextResponse.json({ error: "Invalid amount" }, { status: 400 }); + } + try { + const shortUUID = randomUUID().split("-")[0]; // Shorten the UUID + const receipt = `rcpt_${Date.now()}_${shortUUID}`.slice(0, 40); // Ensure it doesn't exceed 40 chars + const order = await razorpay.orders.create({ + amount: amount * 100, + currency: "INR", + receipt, // Use the shortened receipt + }); - return NextResponse.json( - { - orderId: order.id, - }, - { - status: 200, - } - ); - } catch (error) { - console.error("Razorpay order creation error:", error); - return NextResponse.json( - { - error: "Error creating order ", - }, - { - status: 500, - } - ); - } + return NextResponse.json( + { + orderId: order.id, + }, + { + status: 200, + } + ); + } catch (error) { + console.error("Razorpay order creation error:", error); + return NextResponse.json( + { + error: "Error creating order ", + }, + { + status: 500, + } + ); + } } From 712a4c51ba6cd0a1ba14293e352ecd0c8f17f2e6 Mon Sep 17 00:00:00 2001 From: vyshnav Date: Sat, 23 Nov 2024 11:44:05 +0530 Subject: [PATCH 05/34] Refactor GET route in verify-order to improve authentication and authorization --- src/app/api/verify-order/[id]/route.ts | 47 ++++++++++++++------------ 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/src/app/api/verify-order/[id]/route.ts b/src/app/api/verify-order/[id]/route.ts index 569214e..298e0aa 100644 --- a/src/app/api/verify-order/[id]/route.ts +++ b/src/app/api/verify-order/[id]/route.ts @@ -1,27 +1,32 @@ import { authOptions } from "@/lib/auth-options"; import { razorpay } from "@/lib/razorpay"; -import { getServerSession } from "next-auth"; -import { getToken } from "next-auth/jwt"; +import { getServerSession } from "next-auth/next"; import { NextRequest, NextResponse } from "next/server"; -export async function GET( - request: NextRequest, - context: { params: { id: string } }, -) { - // const session = await getToken({ req: request, secret: process.env.NEXTAUTH_SECRET }); - // if (!session) { - // return NextResponse.json({ message: "Unauthorized", isOk: false }, { status: 401 }); - // } - // if (session.role !== "ADMIN") { - // return NextResponse.json({ message: "Forbidden", isOk: false }, { status: 403 }); - // } +export async function GET(request: NextRequest, context: { params: { id: string } }) { + // Check authentication + const session = await getServerSession(authOptions); + if (!session) { + return NextResponse.json({ message: "Unauthorized" }, { status: 401 }); + } - const { id } = context.params; - try { - const paymentData = await razorpay.payments.fetch(id); - return NextResponse.json(paymentData, { status: 200 }); - } catch (error) { - console.error("Error fetching payment data:", error); - return NextResponse.json({ error: "Payment not found" }, { status: 404 }); - } + // Check authorization (assuming 'role' is part of the session) + if (session.user?.role !== "ADMIN") { + return NextResponse.json({ message: "Forbidden" }, { status: 403 }); + } + + const { id } = context.params; + + try { + const paymentData = await razorpay.payments.fetch(id); + return NextResponse.json(paymentData); + } catch (error) { + console.error("Error fetching payment data:", error); + + if (error instanceof Error) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } + + return NextResponse.json({ error: "An unexpected error occurred" }, { status: 500 }); + } } From cd05bcaf61b42374024f107089e9ca5b739c833b Mon Sep 17 00:00:00 2001 From: vyshnav Date: Sat, 23 Nov 2024 11:44:43 +0530 Subject: [PATCH 06/34] Refactor user-list component to fix duplication issue --- src/components/admin/user-list.tsx | 269 +++++++++++++---------------- 1 file changed, 120 insertions(+), 149 deletions(-) diff --git a/src/components/admin/user-list.tsx b/src/components/admin/user-list.tsx index a69a18b..e921fd4 100644 --- a/src/components/admin/user-list.tsx +++ b/src/components/admin/user-list.tsx @@ -1,15 +1,8 @@ -/* eslint-disable react-hooks/exhaustive-deps */ "use client"; import React, { useState, useEffect, useRef, useCallback } from "react"; import axios from "axios"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "../ui/card"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"; import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar"; import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; import { Button } from "../ui/button"; @@ -19,164 +12,142 @@ import debounce from "lodash.debounce"; import ChangeRole from "./change-role"; export interface User { - id: string; - name: string | null; - email: string | null; - role: string; - image: string | null; + id: string; + name: string | null; + email: string | null; + role: string; + image: string | null; } interface UsersListProps { - initialUsers: User[]; - initialPage: number; + initialUsers: User[]; + initialPage: number; } -export const dynamic = "force-dynamic"; + const UsersList: React.FC = ({ initialUsers, initialPage }) => { - const [userList, setUserList] = useState(initialUsers); - const [currentPage, setCurrentPage] = useState(initialPage); - const [loading, setLoading] = useState(false); - const [hasMore, setHasMore] = useState(true); - const [searchQuery, setSearchQuery] = useState(""); // Search query state - const loader = useRef(null); + const [userList, setUserList] = useState(initialUsers); + const [currentPage, setCurrentPage] = useState(initialPage); + const [loading, setLoading] = useState(false); + const [hasMore, setHasMore] = useState(true); + const [searchQuery, setSearchQuery] = useState(""); + const loader = useRef(null); + + const fetchUsers = useCallback(async (page: number, query: string, isNewSearch: boolean) => { + if (loading) return; + setLoading(true); + try { + const response = await axios.get(`/api/users?page=${page}&search=${encodeURIComponent(query)}`, { + headers: { "Cache-Control": "no-cache" }, + }); + if (response.data.users.length > 0) { + setUserList((prevUsers) => + isNewSearch ? response.data.users : [...prevUsers, ...response.data.users] + ); + setCurrentPage(page); + setHasMore(response.data.users.length === 10); // Assuming 10 is the page size + } else { + setHasMore(false); + } + } catch (error) { + console.error("Error fetching users:", error); + } + setLoading(false); + }, []); + + const loadMoreUsers = useCallback(() => { + if (hasMore && !loading) { + fetchUsers(currentPage + 1, searchQuery, false); + } + }, [currentPage, hasMore, searchQuery, fetchUsers, loading]); + + const debouncedSearch = useCallback( + debounce((query: string) => { + setCurrentPage(1); + setHasMore(true); + fetchUsers(1, query, true); + }, 500), + [fetchUsers] + ); - const fetchUsers = async (page: number, query: string) => { - if (loading) return; - setLoading(true); - try { - const response = await axios.get( - `/api/users?page=${page}&search=${encodeURIComponent(query)}`, - { - headers: { "Cache-Control": "no-cache" }, - }, - ); - if (response.data.users.length > 0) { - setUserList((prevUsers) => [...prevUsers, ...response.data.users]); - setCurrentPage(page); - } else { - setHasMore(false); - } - } catch (error) { - console.error("Error fetching users:", error); - } - setLoading(false); - }; - const loadMoreUsers = useCallback(() => { - if (hasMore) { - fetchUsers(currentPage + 1, searchQuery); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentPage, hasMore, searchQuery]); + const handleSearchChange = (e: React.ChangeEvent) => { + const query = e.target.value; + setSearchQuery(query); + debouncedSearch(query); + }; - const debouncedFetchUsers = useCallback( - debounce(async (query: string) => { - setCurrentPage(1); // Reset page number - setHasMore(true); // Reset hasMore - try { - const response = await axios.get( - `/api/users?page=1&search=${encodeURIComponent(query)}`, + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting && hasMore) { + loadMoreUsers(); + } + }, + { threshold: 1.0 } ); - setUserList(response.data.users); - } catch (error) { - console.error("Error fetching users:", error); - } - }, 500), - [], - ); - const handleSearchChange = (e: React.ChangeEvent) => { - const query = e.target.value; - setSearchQuery(query); - debouncedFetchUsers(query); // Use debounced fetch function - }; - // Observe scroll and load more users when scrolled to the bottom - useEffect(() => { - if (loader.current) { - const observer = new IntersectionObserver( - (entries) => { - if (entries[0].isIntersecting && hasMore) { - loadMoreUsers(); - } - }, - { threshold: 1.0 }, - ); - observer.observe(loader.current); - return () => observer.disconnect(); - } - }, [loader.current, hasMore, loadMoreUsers]); + if (loader.current) { + observer.observe(loader.current); + } - return ( - <> -
-
- + return () => observer.disconnect(); + }, [hasMore, loadMoreUsers]); + + return ( +
-
- - -
+
+ + +
- Users - - Manage user roles and permissions. - + Users + Manage user roles and permissions. -
- {userList.map((user) => ( -
-
- - - - {user.name ? user.name[0] : "N/A"} - - -
-

- {user.name || "Unknown"} -

-

- {user.email || "No email"} -

-
-
- - - - - - - - -
- ))} -
- {hasMore && ( -
- {loading ? "Loading..." : "Load more"} +
+ {userList.map((user) => ( +
+
+ + + {user.name ? user.name[0] : "N/A"} + +
+

{user.name || "Unknown"}

+

+ {user.email || "No email"} +

+
+
+ + + + + + + + +
+ ))}
- )} + {hasMore && ( +
+ {loading ? "Loading..." : "Load more"} +
+ )} - -
-
- - ); + + ); }; export default UsersList; From 9cf65ae12a138c895350a9fa5ed95ebeb0b56240 Mon Sep 17 00:00:00 2001 From: vyshnav Date: Sat, 23 Nov 2024 11:45:13 +0530 Subject: [PATCH 07/34] add signIn callback --- src/lib/auth-options.ts | 151 +++++++++++++++++++++++----------------- 1 file changed, 86 insertions(+), 65 deletions(-) diff --git a/src/lib/auth-options.ts b/src/lib/auth-options.ts index dbb7bd2..6f3749e 100644 --- a/src/lib/auth-options.ts +++ b/src/lib/auth-options.ts @@ -1,8 +1,4 @@ -import { - DefaultSession, - type NextAuthOptions, - type Session as NextAuthSession, -} from "next-auth"; +import { DefaultSession, Profile, type NextAuthOptions, type Session as NextAuthSession } from "next-auth"; import NextAuth from "next-auth/next"; import GoogleProvider from "next-auth/providers/google"; import { PrismaAdapter } from "@next-auth/prisma-adapter"; @@ -10,77 +6,102 @@ import prisma from "@/server/db"; import { UserRoleType } from "@/types"; import { JWT } from "next-auth/jwt"; import { getUserById } from "@/app/actions/get-user-by-id"; +import { isAllowedToAccess } from "@/app/actions/is-allowed-to-access"; declare module "next-auth" { - interface Session { - user: { - id: string; - role: UserRoleType; - } & DefaultSession["user"]; - } + interface Session { + user: { + id: string; + role: UserRoleType; + } & DefaultSession["user"]; + } } declare module "next-auth/jwt" { - interface JWT { - role: UserRoleType; - } + interface JWT { + role: UserRoleType; + } } export const authOptions: NextAuthOptions = { - adapter: PrismaAdapter(prisma), - providers: [ - GoogleProvider({ - clientId: process.env.GOOGLE_ID ?? "", - clientSecret: process.env.GOOGLE_SECRET ?? "", - }), - ], - callbacks: { - async jwt({ token, user }: { token: JWT; user: any }): Promise { - //add user role to token - if (user) { - return { - ...token, - id: user.id, - role: user.role, - }; - } - if (token.id) { - const updatedUser = await getUserById(token.id); - return { - ...token, - role: updatedUser?.role, - }; - } - return token; - }, - async session({ - session, - token, - }: { - session: NextAuthSession; - token: JWT; - }): Promise { - //add role to session - return { - ...session, - user: { - ...session.user, - id: token.id, - role: token.role as UserRoleType, + adapter: PrismaAdapter(prisma), + providers: [ + GoogleProvider({ + clientId: process.env.GOOGLE_ID ?? "", + clientSecret: process.env.GOOGLE_SECRET ?? "", + }), + ], + callbacks: { + async signIn({ profile }: { profile?: Profile }) { + if (!profile?.email) return false; + + // Check if the email ends with "@sjec.ac.in" + if (!profile.email.endsWith("@sjec.ac.in")) { + return true; // Allow non-sjec emails + } + + // Extract the prefix of the email before '@' + const emailPrefix = profile.email.split("@")[0]; + + // Define BE and MBA patterns + const isBEEmail = /^[2][1-4]\d{3}\..+$/; // Matches 21XXXX, 22XXXX, 23XXXX, 24XXXX for BE + const isMBAEmail = /^[2][3-4]ba\d{3}\..+$/; // Matches 23baXXX, 24baXXX for MBA + + // Check BE email validity + if (isBEEmail.test(emailPrefix)) { + return true; // Allow all BE students with valid years + } + + // Check MBA email validity + if (isMBAEmail.test(emailPrefix)) { + return true; // Allow MBA students with valid years + } + + // If the email is sjec but does not fit the BE/MBA patterns, check the database + const isAllowed = await isAllowedToAccess(profile.email); + return isAllowed; + }, + + async jwt({ token, user }: { token: JWT; user: any }): Promise { + //add user role to token + if (user) { + return { + ...token, + id: user.id, + role: user.role, + }; + } + if (token.id) { + const updatedUser = await getUserById(token.id); + return { + ...token, + role: updatedUser?.role, + }; + } + return token; + }, + async session({ session, token }: { session: NextAuthSession; token: JWT }): Promise { + //add role to session + return { + ...session, + user: { + ...session.user, + id: token.id, + role: token.role as UserRoleType, + }, + }; }, - }; }, - }, - session: { - strategy: "jwt", - }, - pages: { - signIn: "/auth/signin", - signOut: "/auth/signout", - }, - secret: process.env.NEXTAUTH_SECRET, - // debug: process.env.NODE_ENV === "development", + session: { + strategy: "jwt", + }, + pages: { + signIn: "/auth/signin", + signOut: "/auth/signout", + }, + secret: process.env.NEXTAUTH_SECRET, + // debug: process.env.NODE_ENV === "development", }; export const handlers = NextAuth(authOptions); From e2762b0da8f6a3d8682f4863f1831bb9524f4c66 Mon Sep 17 00:00:00 2001 From: vyshnav Date: Sat, 23 Nov 2024 13:52:53 +0530 Subject: [PATCH 08/34] remove console.logs --- src/app/actions/invalidate-coupon.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/actions/invalidate-coupon.ts b/src/app/actions/invalidate-coupon.ts index ebfcb9c..86afa6b 100644 --- a/src/app/actions/invalidate-coupon.ts +++ b/src/app/actions/invalidate-coupon.ts @@ -16,7 +16,7 @@ export async function invalidateCouponCode( usedById: string; }; }> { - console.log("coupon code = " + couponCode, "session = " + session.user.id); + if (!couponCode) { return { success: false, message: "Coupon code is required" }; From c2c16ec89bcde99d5a4ff345cc780c966cbd99bb Mon Sep 17 00:00:00 2001 From: vyshnav Date: Sat, 23 Nov 2024 13:53:14 +0530 Subject: [PATCH 09/34] remove console.logs --- src/app/admin/razorpay/[id]/page.tsx | 1 - src/app/api/submit-form/route.ts | 2 +- src/app/api/uploadthing/core.ts | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/app/admin/razorpay/[id]/page.tsx b/src/app/admin/razorpay/[id]/page.tsx index 345986c..2dcd4a0 100644 --- a/src/app/admin/razorpay/[id]/page.tsx +++ b/src/app/admin/razorpay/[id]/page.tsx @@ -99,7 +99,6 @@ function FetchRazorpayPaymentData({ params }: { params: { id: string } }) { throw new Error("Failed to fetch payment data"); } const data = await res.json(); - console.log("Payment data:", data); setPaymentData(data); } catch (error) { toast.error("Error fetching payment data"); diff --git a/src/app/api/submit-form/route.ts b/src/app/api/submit-form/route.ts index cefded6..d4a0deb 100644 --- a/src/app/api/submit-form/route.ts +++ b/src/app/api/submit-form/route.ts @@ -13,7 +13,7 @@ export async function POST(req: NextRequest, res: NextResponse) { try { const response = await req.json(); const body: TRegistrationForm = response; - console.log(body); + const validationResult = RegistrationFormSchema.safeParse(body); if (!validationResult.success) { diff --git a/src/app/api/uploadthing/core.ts b/src/app/api/uploadthing/core.ts index cb1c652..7ae4c99 100644 --- a/src/app/api/uploadthing/core.ts +++ b/src/app/api/uploadthing/core.ts @@ -13,7 +13,7 @@ const auth = async (req: Request) => { export const ourFileRouter = { imageUploader: f({ image: { maxFileSize: "4MB", maxFileCount: 2 } }) .middleware(async ({ req }) => { - console.log("Middleware for imageUploader", req.url); + const user = await auth(req); if (!user) throw new UploadThingError("Unauthorized"); From 7c77e18a808aec8970d8a4708da64090e08e86b0 Mon Sep 17 00:00:00 2001 From: vyshnav Date: Sat, 23 Nov 2024 13:53:40 +0530 Subject: [PATCH 10/34] fix file upload component --- src/components/common/registration-form.tsx | 1058 +++++++++---------- 1 file changed, 513 insertions(+), 545 deletions(-) diff --git a/src/components/common/registration-form.tsx b/src/components/common/registration-form.tsx index 4185092..68d564f 100644 --- a/src/components/common/registration-form.tsx +++ b/src/components/common/registration-form.tsx @@ -4,47 +4,25 @@ import { getPrice } from "@/app/actions/get-price"; import { invalidateCouponCode } from "@/app/actions/invalidate-coupon"; import { submitForm } from "@/app/actions/submit-form"; import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { - basePrice, - initialdiscount, - sjecFacultyPrice, - sjecStudentPrice, -} from "@/constants"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { basePrice, initialdiscount, sjecFacultyPrice, sjecStudentPrice } from "@/constants"; import { getSjecMemberType } from "@/lib/helper"; import { FormDataInterface } from "@/types"; import getErrorMessage from "@/utils/getErrorMessage"; import { useUploadThing } from "@/utils/uploadthing"; -import { - baseSchema, - studentFormSchema, - studentSchema, -} from "@/utils/zod-schemas"; +import { baseSchema, studentFormSchema, studentSchema } from "@/utils/zod-schemas"; import { zodResolver } from "@hookform/resolvers/zod"; import { signIn, signOut, useSession } from "next-auth/react"; import Script from "next/script"; @@ -60,548 +38,538 @@ import InfoButton from "../ui/info-button"; import { redirect } from "next/navigation"; declare global { - interface Window { - Razorpay: any; - } + interface Window { + Razorpay: any; + } } type FormSchema = z.infer; type UploadedFile = { - id: string; - files: File[]; + id: string; + files: File[]; }; export default function RegistrationForm() { - const [step, setStep] = useState(1); - const [uploadedFiles, setUploadedFiles] = useState([]); - const [isProcessing, setIsProcessing] = useState(false); - const [success, setSuccess] = useState(false); - const [memberType, setMemberType] = useState< - "student" | "faculty" | "external" - >("external"); - const [pricing, setPricing] = useState({ - basePrice: basePrice, - discountAmount: initialdiscount, - finalPrice: basePrice, - }); - - const { data: session } = useSession(); + const [step, setStep] = useState(1); + const [uploadedFiles, setUploadedFiles] = useState([]); + const [isProcessing, setIsProcessing] = useState(false); + const [success, setSuccess] = useState(false); + const [memberType, setMemberType] = useState<"student" | "faculty" | "external">("external"); + const [pricing, setPricing] = useState({ + basePrice: basePrice, + discountAmount: initialdiscount, + finalPrice: basePrice, + }); - if (!session) { - redirect("/auth/signin/?callbackUrl=/register"); - } + const { data: session } = useSession(); - useEffect(() => { - setMemberType(getSjecMemberType(session?.user.email!)); - setPricing((prevPricing) => ({ - ...prevPricing, - finalPrice: - memberType === "student" - ? sjecStudentPrice - : memberType === "faculty" - ? sjecFacultyPrice - : basePrice, - })); - console.log("Member Type: ", memberType); - }, [session?.user.email, memberType]); + if (!session) { + redirect("/auth/signin/?callbackUrl=/register"); + } - const form = useForm({ - resolver: zodResolver(baseSchema), - defaultValues: { - designation: getSjecMemberType(session?.user.email!), - name: session?.user.name!, - email: session?.user.email!, - phone: "", - entityName: "", - couponCode: "", - foodPreference: "veg", - }, - }); + useEffect(() => { + setMemberType(getSjecMemberType(session?.user.email!)); + setPricing((prevPricing) => ({ + ...prevPricing, + finalPrice: + memberType === "student" + ? sjecStudentPrice + : memberType === "faculty" + ? sjecFacultyPrice + : basePrice, + })); + + }, [session?.user.email, memberType]); - const { startUpload, routeConfig } = useUploadThing("imageUploader", { - onClientUploadComplete: (res) => { - if (res && res.length == 2) { - form.setValue("idCard", res[0].url); - form.setValue("photo", res[1].url); - } - if (res && res.length == 1) { - form.setValue("photo", res[0].url); - } - console.log("Images uploaded successfully!"); - }, - onUploadError: () => { - toast.error("error occurred while uploading"); - }, - onUploadBegin: (file) => { - console.log("upload has begun for", file); - }, - }); + const form = useForm({ + resolver: zodResolver(baseSchema), + defaultValues: { + designation: getSjecMemberType(session?.user.email!), + name: session?.user.name!, + email: session?.user.email!, + phone: "", + entityName: "", + couponCode: "", + foodPreference: "veg", + }, + }); - const handleFileUpload = (id: "idCard" | "photo", files: File[]) => { - setUploadedFiles((prevFiles) => { - const existing = prevFiles.find((file) => file.id === id); - if (existing) { - return prevFiles.map((file) => - file.id === id ? { ...file, files } : file, - ); - } else { - return [...prevFiles, { id, files }]; - } + const { startUpload, routeConfig } = useUploadThing("imageUploader", { + onClientUploadComplete: (res) => { + if (res && res.length == 2) { + form.setValue("idCard", res[0].url); + form.setValue("photo", res[1].url); + } + if (res && res.length == 1) { + form.setValue("photo", res[0].url); + } + toast.success("✅ Images uploaded successfully!"); + }, + onUploadError: () => { + toast.error("error occurred while uploading"); + }, + onUploadBegin: (file) => { + toast.info(" upload has begun "); + }, }); - form.setValue(id, files.map((file) => file.name).join(", ")); - }; + const handleFileUpload = (id: "idCard" | "photo", files: File[]) => { + setUploadedFiles((prevFiles) => { + const existing = prevFiles.find((file) => file.id === id); + if (existing) { + return prevFiles.map((file) => (file.id === id ? { ...file, files } : file)); + } else { + return [...prevFiles, { id, files }]; + } + }); + + form.setValue(id, files.map((file) => file.name).join(", ")); + }; - const handlePayment = async () => { - setIsProcessing(true); - const couponCode = form.getValues("couponCode"); - try { - const response = await fetch("/api/create-order", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ amount: pricing.finalPrice }), - }); - const data = await response.json(); + const handlePayment = async () => { + setIsProcessing(true); + const couponCode = form.getValues("couponCode"); + try { + const response = await fetch("/api/create-order", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ amount: pricing.finalPrice }), + }); + const data = await response.json(); - const options = { - key: process.env.NEXT_PUBLIC_RAZORPAY_KEY_ID, - amount: pricing.finalPrice * 100, - currency: "INR", - name: "TEDxSJEC", - description: "Registration Fee", - order_id: data.orderId, - handler: async (response: any) => { - const resp = await fetch("/api/verify-order", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - orderId: response.razorpay_order_id, - razorpayPaymentId: response.razorpay_payment_id, - razorpaySignature: response.razorpay_signature, - amount: pricing.finalPrice, - }), - }); + const options = { + key: process.env.NEXT_PUBLIC_RAZORPAY_KEY_ID, + amount: pricing.finalPrice * 100, + currency: "INR", + name: "TEDxSJEC", + description: "Registration Fee", + order_id: data.orderId, + handler: async (response: any) => { + const resp = await fetch("/api/verify-order", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + orderId: response.razorpay_order_id, + razorpayPaymentId: response.razorpay_payment_id, + razorpaySignature: response.razorpay_signature, + amount: pricing.finalPrice, + }), + }); - const data = await resp.json(); - if (data.isOk) { - try { - if (uploadedFiles.length > 0) { - const allFiles = uploadedFiles.flatMap((file) => file.files); - if (allFiles.length > 0) { - await startUpload(allFiles); - } - } - if (couponCode) { - const result = await invalidateCouponCode(couponCode, session!); - if (!result.success) { - toast.error( - result.message || - "An error occurred while invalidating the coupon", - ); + const data = await resp.json(); + if (data.isOk) { + try { + if (uploadedFiles.length > 0) { + const allFiles = uploadedFiles.flatMap((file) => file.files); + if (allFiles.length > 0) { + await startUpload(allFiles); + } + } + if (couponCode) { + const result = await invalidateCouponCode(couponCode, session!); + if (!result.success) { + toast.error( + result.message || "An error occurred while invalidating the coupon" + ); + } + } + const formResponse = form.getValues(); + await submitForm(formResponse as FormDataInterface, pricing.finalPrice); + setIsProcessing(false); + setSuccess(true); + } catch (error) { + setIsProcessing(false); + toast.error(`Payment failed: ${getErrorMessage(data.error)}`); + } + } else { + setIsProcessing(false); + toast.error(`Payment failed: ${getErrorMessage(data.error)}`); + } + }, + notes: { + customerName: form.getValues("name"), + customerEmail: session?.user?.email, + customerContact: form.getValues("phone"), + }, + prefill: { + name: form.getValues("name"), + email: session?.user?.email, + contact: form.getValues("phone"), + }, + theme: { + color: "#3399cc", + }, + modal: { + ondismiss: () => { + setIsProcessing(false); + }, + }, + }; + const rzp1 = new window.Razorpay(options); + rzp1.open(); + } catch (error) { + toast.error(`Payment error: ${getErrorMessage(error)}`); + setIsProcessing(false); + } + }; + + const onSubmit = async (values: FormSchema) => { + await handlePayment(); + }; + + const verifyCoupon = async () => { + const couponCode = form.getValues("couponCode"); + try { + const result = await getPrice(couponCode); + if (!result.success) { + toast.error(result.message || "An error occurred while applying the coupon"); + return; + } + const { basePrice, discountAmount, finalPrice } = result; + setPricing({ + basePrice: basePrice ?? pricing.basePrice, + discountAmount: discountAmount ?? pricing.discountAmount, + finalPrice: finalPrice ?? pricing.finalPrice, + }); + toast.success("Coupon applied successfully"); + } catch (e) { + console.error(e); + toast.error(getErrorMessage(e)); + } + }; + + const handleNext = async () => { + let isValid = false; + if (step === 1) { + isValid = await form.trigger(["designation", "foodPreference"]); + } else if (step === 2) { + const designation = form.getValues("designation"); + if (designation === "student") { + form.clearErrors(); + const validationResult = await studentFormSchema.safeParseAsync(form.getValues()); + isValid = validationResult.success; + if (!isValid) { + if (validationResult.error) { + validationResult.error.issues.forEach((issue) => { + form.setError(issue.path[0] as keyof FormSchema, { + type: "manual", + message: issue.message, + }); + }); + } } - } - const formResponse = form.getValues(); - await submitForm( - formResponse as FormDataInterface, - pricing.finalPrice, - ); - setIsProcessing(false); - setSuccess(true); - } catch (error) { - setIsProcessing(false); - toast.error(`Payment failed: ${getErrorMessage(data.error)}`); + } else { + isValid = await form.trigger(["name", "email", "phone", "photo"]); } - } else { - setIsProcessing(false); - toast.error(`Payment failed: ${getErrorMessage(data.error)}`); - } - }, - notes: { - customerName: form.getValues("name"), - customerEmail: session?.user?.email, - customerContact: form.getValues("phone"), - }, - prefill: { - name: form.getValues("name"), - email: session?.user?.email, - contact: form.getValues("phone"), - }, - theme: { - color: "#3399cc", - }, - modal: { - ondismiss: () => { - setIsProcessing(false); - }, - }, - }; - const rzp1 = new window.Razorpay(options); - rzp1.open(); - } catch (error) { - toast.error(`Payment error: ${getErrorMessage(error)}`); - setIsProcessing(false); - } - }; + } - const onSubmit = async (values: FormSchema) => { - await handlePayment(); - }; + if (isValid) { + setStep(step + 1); + } + }; - const verifyCoupon = async () => { - const couponCode = form.getValues("couponCode"); - try { - const result = await getPrice(couponCode); - if (!result.success) { - toast.error( - result.message || "An error occurred while applying the coupon", + if (isProcessing) { + return ( +
+ +
); - return; - } - const { basePrice, discountAmount, finalPrice } = result; - setPricing({ - basePrice: basePrice ?? pricing.basePrice, - discountAmount: discountAmount ?? pricing.discountAmount, - finalPrice: finalPrice ?? pricing.finalPrice, - }); - toast.success("Coupon applied successfully"); - } catch (e) { - console.error(e); - toast.error(getErrorMessage(e)); } - }; - const handleNext = async () => { - let isValid = false; - if (step === 1) { - isValid = await form.trigger(["designation", "foodPreference"]); - } else if (step === 2) { - const designation = form.getValues("designation"); - if (designation === "student") { - form.clearErrors(); - const validationResult = await studentFormSchema.safeParseAsync( - form.getValues(), + if (success) { + return ( +
+ +
); - isValid = validationResult.success; - if (!isValid) { - if (validationResult.error) { - validationResult.error.issues.forEach((issue) => { - form.setError(issue.path[0] as keyof FormSchema, { - type: "manual", - message: issue.message, - }); - }); - } - } - } else { - isValid = await form.trigger(["name", "email", "phone", "photo"]); - } - } - - if (isValid) { - setStep(step + 1); } - }; - if (isProcessing) { return ( -
- -
- ); - } - - if (success) { - return ( -
- -
- ); - } - - return ( - -