From 46bec9520c9edf5481c7bd870e28afadf60e716b Mon Sep 17 00:00:00 2001 From: anishshobithps Date: Tue, 7 May 2024 10:32:23 +0530 Subject: [PATCH] feat: update roles on dashboard, fix auth --- next.config.mjs | 23 ++-- package-lock.json | 40 +------ package.json | 2 +- src/app/(content)/page.tsx | 2 +- src/app/(content)/register/page.tsx | 2 +- src/app/admin/_components/columnsUsers.tsx | 101 +++++++++++++++--- src/app/api/admin/[id]/make-admin/route.ts | 34 ++++++ .../api/admin/[id]/make-coordinator/route.ts | 34 ++++++ .../api/admin/[id]/make-participant/route.ts | 34 ++++++ src/app/api/admin/payments/route.ts | 2 + src/app/api/coordinators/verify/[id]/route.ts | 34 ++++++ src/app/api/razorpay/route.ts | 4 +- src/app/api/razorpay/verify/route.ts | 71 +++--------- src/app/auth/signin/page.tsx | 23 ++++ src/app/coordinators/verify/[id]/page.tsx | 62 +++++++++++ src/app/layout.tsx | 5 +- src/auth.ts | 23 ++-- src/components/ui/textarea.tsx | 24 +++++ src/helper/RegistrationEmail.tsx | 6 +- 19 files changed, 390 insertions(+), 136 deletions(-) create mode 100644 src/app/api/admin/[id]/make-admin/route.ts create mode 100644 src/app/api/admin/[id]/make-coordinator/route.ts create mode 100644 src/app/api/admin/[id]/make-participant/route.ts create mode 100644 src/app/api/coordinators/verify/[id]/route.ts create mode 100644 src/app/auth/signin/page.tsx create mode 100644 src/app/coordinators/verify/[id]/page.tsx create mode 100644 src/components/ui/textarea.tsx diff --git a/next.config.mjs b/next.config.mjs index b304523..4342171 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,23 +1,18 @@ /** @type {import('next').NextConfig} */ const nextConfig = { + async redirects() { + return [ + { + source: "/api/verify/:id", + destination: "/coordinators/verify/:id", + permanent: true, + }, + ]; + }, images: { formats: ["image/avif", "image/webp"], remotePatterns: [ - // localhost - { - protocol: "http", - hostname: "localhost", - port: "3000", - pathname: "/**", - }, - // vercel - { - protocol: "https", - hostname: "tiara2024.vercel.app", - port: "", - pathname: "/**", - }, { protocol: "https", hostname: "assets.tiarasjec.in", diff --git a/package-lock.json b/package-lock.json index deb9719..89b050c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,7 +46,7 @@ "lucide-react": "^0.376.0", "maath": "^0.10.7", "next": "^14.2.3", - "next-auth": "^5.0.0-beta.16", + "next-auth": "^5.0.0-beta.17", "nodemailer": "^6.9.13", "preline": "^2.1.0", "razorpay": "^2.9.3", @@ -7231,11 +7231,11 @@ } }, "node_modules/next-auth": { - "version": "5.0.0-beta.16", - "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.16.tgz", - "integrity": "sha512-dX2snB+ezN23tFzSes3n3uosT9iBf0eILPYWH/R2fd9n3ZzdMQlRzq7JIOPeS1aLc84IuRlyuyXyx9XmmZB6og==", + "version": "5.0.0-beta.17", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.17.tgz", + "integrity": "sha512-XA/7JtAjOgDfAeotJPFUsFZGGItZwzZrxLt9Gc9fE7EchLk6zydZfuZ22Vvwixs3IilkN644D5IoD5tEOAFGCQ==", "dependencies": { - "@auth/core": "0.28.1" + "@auth/core": "0.30.0" }, "peerDependencies": { "@simplewebauthn/browser": "^9.0.1", @@ -7256,36 +7256,6 @@ } } }, - "node_modules/next-auth/node_modules/@auth/core": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.28.1.tgz", - "integrity": "sha512-gvp74mypYZADpTlfGRp6HE0G3pIHWvtJpy+KZ+8FvY0cmlIpHog+jdMOdd29dQtLtN25kF2YbfHsesCFuGUQbg==", - "dependencies": { - "@panva/hkdf": "^1.1.1", - "@types/cookie": "0.6.0", - "cookie": "0.6.0", - "jose": "^5.1.3", - "oauth4webapi": "^2.4.0", - "preact": "10.11.3", - "preact-render-to-string": "5.2.3" - }, - "peerDependencies": { - "@simplewebauthn/browser": "^9.0.1", - "@simplewebauthn/server": "^9.0.2", - "nodemailer": "^6.8.0" - }, - "peerDependenciesMeta": { - "@simplewebauthn/browser": { - "optional": true - }, - "@simplewebauthn/server": { - "optional": true - }, - "nodemailer": { - "optional": true - } - } - }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", diff --git a/package.json b/package.json index dd36eb9..cea83c9 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "lucide-react": "^0.376.0", "maath": "^0.10.7", "next": "^14.2.3", - "next-auth": "^5.0.0-beta.16", + "next-auth": "^5.0.0-beta.17", "nodemailer": "^6.9.13", "preline": "^2.1.0", "razorpay": "^2.9.3", diff --git a/src/app/(content)/page.tsx b/src/app/(content)/page.tsx index 09eaf3f..974aba7 100644 --- a/src/app/(content)/page.tsx +++ b/src/app/(content)/page.tsx @@ -1,4 +1,4 @@ -"use client";; +"use client"; import Lenis from "@/components/shared/lenis"; import { LabIntro } from "@/components/story/intro"; import { HeroParallax } from "@/components/widgets/Hero"; diff --git a/src/app/(content)/register/page.tsx b/src/app/(content)/register/page.tsx index ea286e2..ae75248 100644 --- a/src/app/(content)/register/page.tsx +++ b/src/app/(content)/register/page.tsx @@ -278,4 +278,4 @@ const Register: React.FC = () => { ); }; -export default Register; \ No newline at end of file +export default Register; diff --git a/src/app/admin/_components/columnsUsers.tsx b/src/app/admin/_components/columnsUsers.tsx index f1be784..b1bd50e 100644 --- a/src/app/admin/_components/columnsUsers.tsx +++ b/src/app/admin/_components/columnsUsers.tsx @@ -3,6 +3,40 @@ import { ColumnDef } from "@tanstack/react-table"; import { Payment, User } from "@prisma/client"; import { ArrowUpDown } from "lucide-react"; import { Button } from "@/components/ui/button"; +import { MoreHorizontal } from "lucide-react"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { toast } from "@/components/ui/use-toast"; +import { UserRole } from "@prisma/client"; + +async function updateRole({ + id, + role, + name, +}: { + id: string; + role: Lowercase; + name: string; +}) { + const res = await fetch(`/api/admin/${id}/make-${role}`, { + method: "POST", + }); + if (res.status === 200) { + toast({ + title: `${name} is now a ${role}`, + }); + } else { + toast({ + title: `Failed to make ${name} a ${role}`, + }); + } +} export const columns: ColumnDef[] = [ { @@ -71,22 +105,12 @@ export const columns: ColumnDef[] = [ variant="ghost" onClick={() => column.toggleSorting(column.getIsSorted() === "asc")} > - Amount + Role ); }, - accessorKey: "payment_amount", - cell: ({ row }) => { - return row.original.payment.map((payment, i) => ( -
- {new Intl.NumberFormat("en-IN", { - style: "currency", - currency: "INR", - }).format(payment.amount / 100)} -
- )); - }, + accessorKey: "role", }, { header: ({ column }) => { @@ -102,4 +126,57 @@ export const columns: ColumnDef[] = [ }, accessorKey: "contact", }, + { + id: "actions", + cell: ({ row }) => { + const user = row.original; + return ( + + + + + + Actions + + + updateRole({ + id: user.id, + role: "admin", + name: user.name!, + }) + } + > + Make Admin + + + updateRole({ + id: user.id, + role: "coordinator", + name: user.name!, + }) + } + > + Make Coordinator + + + updateRole({ + id: user.id, + role: "participant", + name: user.name!, + }) + } + > + Make User + + + + ); + }, + }, ]; diff --git a/src/app/api/admin/[id]/make-admin/route.ts b/src/app/api/admin/[id]/make-admin/route.ts new file mode 100644 index 0000000..8315afa --- /dev/null +++ b/src/app/api/admin/[id]/make-admin/route.ts @@ -0,0 +1,34 @@ +import { prisma } from "@/lib/prisma"; +import { auth } from "@/auth"; +import { NextRequest, NextResponse } from "next/server"; +import { UserRole } from "@prisma/client"; + +export async function POST(request: NextRequest, context: { params: { id: string } }) { + const session = await auth(); + if (!session) { + return NextResponse.json( + { message: "Unauthorized", isOk: false }, + { status: 401 } + ); + } + + if (session.user.role !== UserRole.ADMIN) { + return NextResponse.json( + { message: "Forbidden", isOk: false }, + { status: 403 } + ); + } + + const { id } = context.params; + + const user = await prisma.user.update({ + where: { + id: id, + }, + data: { + role: UserRole.ADMIN, + }, + }); + + return NextResponse.json({ user }, { status: 200 }); +} diff --git a/src/app/api/admin/[id]/make-coordinator/route.ts b/src/app/api/admin/[id]/make-coordinator/route.ts new file mode 100644 index 0000000..b13b358 --- /dev/null +++ b/src/app/api/admin/[id]/make-coordinator/route.ts @@ -0,0 +1,34 @@ +import { prisma } from "@/lib/prisma"; +import { auth } from "@/auth"; +import { NextRequest, NextResponse } from "next/server"; +import { UserRole } from "@prisma/client"; + +export async function POST(request: NextRequest, context: { params: { id: string } }) { + const session = await auth(); + if (!session) { + return NextResponse.json( + { message: "Unauthorized", isOk: false }, + { status: 401 } + ); + } + + if (session.user.role !== UserRole.ADMIN) { + return NextResponse.json( + { message: "Forbidden", isOk: false }, + { status: 403 } + ); + } + + const { id } = context.params; + + const user = await prisma.user.update({ + where: { + id: id, + }, + data: { + role: UserRole.COORDINATOR, + }, + }); + + return NextResponse.json({ user }, { status: 200 }); +} diff --git a/src/app/api/admin/[id]/make-participant/route.ts b/src/app/api/admin/[id]/make-participant/route.ts new file mode 100644 index 0000000..798481a --- /dev/null +++ b/src/app/api/admin/[id]/make-participant/route.ts @@ -0,0 +1,34 @@ +import { prisma } from "@/lib/prisma"; +import { auth } from "@/auth"; +import { NextRequest, NextResponse } from "next/server"; +import { UserRole } from "@prisma/client"; + +export async function POST(request: NextRequest, context: { params: { id: string } }) { + const session = await auth(); + if (!session) { + return NextResponse.json( + { message: "Unauthorized", isOk: false }, + { status: 401 } + ); + } + + if (session.user.role !== UserRole.ADMIN) { + return NextResponse.json( + { message: "Forbidden", isOk: false }, + { status: 403 } + ); + } + + const { id } = context.params; + + const user = await prisma.user.update({ + where: { + id: id, + }, + data: { + role: UserRole.PARTICIPANT, + }, + }); + + return NextResponse.json({ user }, { status: 200 }); +} diff --git a/src/app/api/admin/payments/route.ts b/src/app/api/admin/payments/route.ts index fb86084..5a54d80 100644 --- a/src/app/api/admin/payments/route.ts +++ b/src/app/api/admin/payments/route.ts @@ -25,6 +25,7 @@ export async function GET() { }); // for (const prismaPayment of payments) { + // console.log("ID: ", prismaPayment.razorpayPaymentId); // const payment = await razorpay.payments.fetch(prismaPayment.razorpayPaymentId); // await prisma.payment.update({ // where: { @@ -34,6 +35,7 @@ export async function GET() { // amount: parseFloat(payment.amount.toString()), // } // }) + // console.log("ID: Done", prismaPayment.razorpayPaymentId); // } return NextResponse.json(payments); diff --git a/src/app/api/coordinators/verify/[id]/route.ts b/src/app/api/coordinators/verify/[id]/route.ts new file mode 100644 index 0000000..f0c07bf --- /dev/null +++ b/src/app/api/coordinators/verify/[id]/route.ts @@ -0,0 +1,34 @@ +import { prisma } from "@/lib/prisma"; +import { auth } from "@/auth"; +import { NextRequest, NextResponse } from "next/server"; +import { UserRole } from "@prisma/client"; + +export async function GET( + request: NextRequest, + context: { params: { id: string } } +) { + const session = await auth(); + if (!session) { + return NextResponse.json( + { message: "Unauthorized", isOk: false }, + { status: 401 } + ); + } + + if (session.user.role !== UserRole.ADMIN) { + return NextResponse.json( + { message: "Forbidden", isOk: false }, + { status: 403 } + ); + } + + const { id } = context.params; + + const user = await prisma.user.findUnique({ + where: { + id: id, + }, + }); + + return NextResponse.json({ user }, { status: 200 }); +} diff --git a/src/app/api/razorpay/route.ts b/src/app/api/razorpay/route.ts index 8fcabed..34eedfa 100644 --- a/src/app/api/razorpay/route.ts +++ b/src/app/api/razorpay/route.ts @@ -22,7 +22,9 @@ export async function GET() { } try { - const paymentData = await razorpay.payments.all(); + const paymentData = await razorpay.payments.all({ + count: 100 + }); return NextResponse.json(paymentData, { status: 200 }); } catch (error) { console.error("Error fetching payment data:", error); diff --git a/src/app/api/razorpay/verify/route.ts b/src/app/api/razorpay/verify/route.ts index 4b68fe4..9fb6d24 100644 --- a/src/app/api/razorpay/verify/route.ts +++ b/src/app/api/razorpay/verify/route.ts @@ -5,6 +5,7 @@ import { PaymentStatus } from "@prisma/client"; import { auth } from "@/auth"; import { sendEmail } from "@/helper/mailer"; import { SentMessageInfo } from "nodemailer/lib/smtp-transport"; +import { razorpay } from "@/lib/razorpay"; const generatedSignature = ( razorpayOrderId: string, @@ -53,37 +54,12 @@ export async function POST(request: NextRequest) { data.orderCreationId, data.razorpayPaymentId ); - if (signature !== data.razorpaySignature) { - await prisma.payment.upsert({ - where: { - razorpayPaymentId: data.razorpayPaymentId, - user: { - email: session.user?.email, - }, - }, - update: { - status: PaymentStatus.FAILED, - orderCreationId: data.orderCreationId, - }, - create: { - amount: data.amount, - signature: data.razorpaySignature, - razorpayPaymentId: data.razorpayPaymentId, - orderCreationId: data.orderCreationId, - status: PaymentStatus.FAILED, - user: { - connect: { - email: session.user?.email!, - }, - }, - }, - }); - return NextResponse.json( - { message: "payment verification failed", isOk: false }, - { status: 400 } - ); - } else if (signature === data.razorpaySignature) { + const amount = parseFloat( + (await razorpay.payments.fetch(data.razorpayPaymentId)).amount.toString() + ); + + if (signature === data.razorpaySignature) { let email: SentMessageInfo | boolean = false; const user = await prisma.user.findUnique({ @@ -94,7 +70,7 @@ export async function POST(request: NextRequest) { try { email = await sendEmail({ - amount: data.amount, + amount, email: session.user?.email!, teamNames: data.teams.map((team) => team.name), contactNumber: data.phone, @@ -107,19 +83,9 @@ export async function POST(request: NextRequest) { } await prisma.$transaction(async (prisma) => { - await prisma.payment.upsert({ - where: { - razorpayPaymentId: data.razorpayPaymentId, - user: { - email: session.user?.email, - }, - }, - update: { - status: PaymentStatus.SUCCESS, - orderCreationId: data.orderCreationId, - }, - create: { - amount: data.amount, + await prisma.payment.create({ + data: { + amount, signature: data.razorpaySignature, razorpayPaymentId: data.razorpayPaymentId, orderCreationId: data.orderCreationId, @@ -140,13 +106,13 @@ export async function POST(request: NextRequest) { teams: true, }, }); - const mergedEvents = [...existingUser?.events!, ...data.events]; - await prisma.user.upsert({ + const mergedEvents = [...existingUser?.events!, ...data.events]; + await prisma.user.update({ where: { email: session.user?.email!, }, - update: { + data: { registrationEmailSent: !!email, contact: data.phone, college: data.college, @@ -157,17 +123,6 @@ export async function POST(request: NextRequest) { }, }, }, - create: { - registrationEmailSent: !!email, - contact: data.phone, - college: data.college, - events: data.events, - teams: { - createMany: { - data: data.teams, - }, - }, - }, include: { teams: true, }, diff --git a/src/app/auth/signin/page.tsx b/src/app/auth/signin/page.tsx new file mode 100644 index 0000000..eb7d407 --- /dev/null +++ b/src/app/auth/signin/page.tsx @@ -0,0 +1,23 @@ +"use client"; + +import { useSearchParams, useRouter } from "next/navigation"; +import { signIn, useSession } from "next-auth/react"; +import { useEffect } from "react"; + +export default function Signin() { + const router = useRouter(); + const { status } = useSession(); + const searchParams = useSearchParams(); + + const callbackUrl = searchParams.get("callbackUrl"); + + useEffect(() => { + if (status === "unauthenticated") { + signIn("google"); + } else if (status === "authenticated") { + router.push(callbackUrl || "/"); + } + }, [status, router, callbackUrl]); + + return
; +} diff --git a/src/app/coordinators/verify/[id]/page.tsx b/src/app/coordinators/verify/[id]/page.tsx new file mode 100644 index 0000000..ce2c510 --- /dev/null +++ b/src/app/coordinators/verify/[id]/page.tsx @@ -0,0 +1,62 @@ +"use client"; + +import { User } from "@prisma/client"; +import { useEffect, useState } from "react"; +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; + +export default function VerificationPage({ + params, +}: { + params: { id: string }; +}) { + const { id } = params; + const [data, setData] = useState(null); + useEffect(() => { + fetch(`/api/coordinators/verify/${id}`) + .then((res) => res.json()) + .then((data) => { + setData(data.user); + }); + }, [id]); + + if (!data) { + return
Loading...
; + } + + return ( + + + Verification of {data.name} + + + + + + + + + ); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index f0ae40a..60216f4 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -12,10 +12,7 @@ import Script from "next/script"; const inter = Inter({ subsets: ["latin"], variable: "--font-sans" }); export const metadata: Metadata = { - title: { - default: "Tiara 2024", - template: `%s - `, - }, + title: "Tiara 2024", description: "Tiara is a National-level Techno-Cultural fest, conducted for young minds aspiring to be extraordinary, that is open to all students of undergraduate level and above to come and showcase their talents and represent their respective institutions on the grand stage of Tiara.", icons: { diff --git a/src/auth.ts b/src/auth.ts index 48633b2..0a755f6 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -25,6 +25,20 @@ export const { handlers, auth, signIn, signOut } = NextAuth({ session: { strategy: "jwt", }, + pages: { + signIn: "/auth/signin", + }, + cookies: { + pkceCodeVerifier: { + name: "next-auth.pkce.code_verifier", + options: { + httpOnly: true, + sameSite: "none", + path: "/", + secure: true, + }, + }, + }, providers: [ Google({ profile(profile: GoogleProfile) { @@ -35,14 +49,7 @@ export const { handlers, auth, signIn, signOut } = NextAuth({ image: profile.picture, role: (profile.role as UserRole) ?? UserRole.PARTICIPANT, }; - }, - authorization: { - params: { - prompt: "consent", - access_type: "offline", - response_type: "code", - }, - }, + } }), ], callbacks: { diff --git a/src/components/ui/textarea.tsx b/src/components/ui/textarea.tsx new file mode 100644 index 0000000..d1258e4 --- /dev/null +++ b/src/components/ui/textarea.tsx @@ -0,0 +1,24 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +export interface TextareaProps + extends React.TextareaHTMLAttributes {} + +const Textarea = React.forwardRef( + ({ className, ...props }, ref) => { + return ( +