From 31110fc847dab24aa08e124bb6e5cdc6ae60c985 Mon Sep 17 00:00:00 2001 From: Nischal Shetty Date: Wed, 25 Sep 2024 23:35:29 +0530 Subject: [PATCH] Added Auth --- .husky/pre-commit | 2 +- src/actions/bounty/adminActions.ts | 124 +++++++++++------- src/actions/bounty/schema.ts | 2 +- src/actions/bounty/types.ts | 1 - src/actions/bounty/userActions.ts | 35 +++-- src/actions/payoutMethods/schema.ts | 8 +- src/app/Bounty/page.tsx | 21 +-- src/app/admin/bounty/AdminBountyPage.tsx | 25 ++-- src/components/PaymentDropdown.tsx | 2 +- .../bounty/BountySubmissionDialog.tsx | 32 ++--- .../admin-page/ConfirmedBountiesDialog.tsx | 5 +- tsconfig.json | 10 +- 12 files changed, 154 insertions(+), 113 deletions(-) diff --git a/.husky/pre-commit b/.husky/pre-commit index d663f7203..b138c2772 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,8 +1,8 @@ #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" -npm run format:fix npm run lint:fix +npm run format:fix git add . diff --git a/src/actions/bounty/adminActions.ts b/src/actions/bounty/adminActions.ts index 0d893171c..5d042826d 100644 --- a/src/actions/bounty/adminActions.ts +++ b/src/actions/bounty/adminActions.ts @@ -1,7 +1,8 @@ 'use server'; import prisma from '@/db'; -import { adminApprovalSchema } from './schema'; -import { AdminApprovalData } from './types'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth'; +import { ROLES } from '../types'; export type Bounty = { id: string; @@ -19,67 +20,90 @@ export type Bounty = { }; type BountyResponse = { - bounties: Bounty[]; - totalINRBounties: number; - totalSOLBounties: number; + bounties?: Bounty[]; + totalINRBounties?: number; + totalSOLBounties?: number; + error?: string; }; -export async function approveBounty(data: AdminApprovalData) { - const validatedData = adminApprovalSchema.parse(data); - - const updatedBounty = await prisma.bountySubmission.update({ - where: { id: validatedData.bountyId }, - data: { status: validatedData.status }, - }); +export async function getBounties(): Promise { + const session = await getServerSession(authOptions); - return updatedBounty; -} + if (!session || !session.user || session.user.role !== ROLES.ADMIN) { + return { error: 'Unauthorized or insufficient permissions' }; + } -export async function getBounties(): Promise { - const bounties = await prisma.bountySubmission.findMany({ - select: { - id: true, - prLink: true, - paymentMethod: true, - status: true, - createdAt: true, - updatedAt: true, - amount: true, - userId: true, - user: { - select: { - id: true, - name: true, + try { + const bounties = await prisma.bountySubmission.findMany({ + select: { + id: true, + prLink: true, + paymentMethod: true, + status: true, + createdAt: true, + updatedAt: true, + amount: true, + userId: true, + user: { + select: { + id: true, + name: true, + }, }, }, - }, - }); - let totalINRBounties = 0; - let totalSOLBounties = 0; + }); - bounties.forEach((bounty) => { - if (bounty.paymentMethod.includes('@')) { - totalINRBounties += bounty.amount || 0; - } else { - totalSOLBounties += bounty.amount || 0; - } - }); + let totalINRBounties = 0; + let totalSOLBounties = 0; - return { bounties, totalINRBounties, totalSOLBounties }; + bounties.forEach((bounty) => { + if (bounty.paymentMethod.includes('@')) { + totalINRBounties += bounty.amount || 0; + } else { + totalSOLBounties += bounty.amount || 0; + } + }); + + return { bounties, totalINRBounties, totalSOLBounties }; + } catch (e) { + return { error: 'An error occurred while approving the bounty.' }; + } } export async function deleteBounty(bountyId: string) { - const deleteBounty = await prisma.bountySubmission.delete({ - where: { id: bountyId }, - }); - return { success: !!deleteBounty }; + const session = await getServerSession(authOptions); + + if (!session || !session.user || session.user.role !== ROLES.ADMIN) { + return { error: 'Unauthorized or insufficient permissions' }; + } + try { + const deleteBounty = await prisma.bountySubmission.delete({ + where: { id: bountyId }, + }); + return { success: !!deleteBounty }; + } catch (e) { + return { error: 'An error occurred while approving the bounty.' }; + } } export async function confirmBounty(bountyId: string, amount: number) { - const updatedBounty = await prisma.bountySubmission.update({ - where: { id: bountyId }, - data: { status: 'confirmed', amount }, - }); + const session = await getServerSession(authOptions); + + if (!session || !session.user || session.user.role !== ROLES.ADMIN) { + return { error: 'Unauthorized or insufficient permissions' }; + } - return { success: !!updatedBounty }; + try { + const updatedBounty = await prisma.bountySubmission.update({ + where: { id: bountyId }, + data: { status: 'confirmed', amount }, + }); + + if (updatedBounty) { + return { success: true }; + } + return { error: 'Failed to update bounty.' }; + } catch (e) { + return { error: 'An error occurred while approving the bounty.' }; + } } diff --git a/src/actions/bounty/schema.ts b/src/actions/bounty/schema.ts index b6d6b6ba1..1869441e0 100644 --- a/src/actions/bounty/schema.ts +++ b/src/actions/bounty/schema.ts @@ -6,6 +6,6 @@ export const bountySubmissionSchema = z.object({ }); export const adminApprovalSchema = z.object({ - bountyId: z.string().uuid({ message: 'Invalid Bounty ID' }), + bountyId: z.string(), status: z.enum(['approved', 'rejected']), }); diff --git a/src/actions/bounty/types.ts b/src/actions/bounty/types.ts index 5f0705462..bb272b4e1 100644 --- a/src/actions/bounty/types.ts +++ b/src/actions/bounty/types.ts @@ -1,7 +1,6 @@ export interface BountySubmissionData { prLink: string; paymentMethod: string; - userId: string; } export interface AdminApprovalData { diff --git a/src/actions/bounty/userActions.ts b/src/actions/bounty/userActions.ts index 056c8dcc4..fef36ad85 100644 --- a/src/actions/bounty/userActions.ts +++ b/src/actions/bounty/userActions.ts @@ -4,28 +4,34 @@ import { bountySubmissionSchema } from './schema'; import { BountySubmissionData } from './types'; import { getServerSession } from 'next-auth'; import { authOptions } from '@/lib/auth'; +import { createSafeAction } from '@/lib/create-safe-action'; +import { Prisma } from '@prisma/client'; -export async function submitBounty(data: BountySubmissionData) { +async function submitBountyHandler(data: BountySubmissionData) { try { - const validatedData = bountySubmissionSchema.parse(data); const session = await getServerSession(authOptions); - if (!session?.user.id) { - throw new Error('User not authenticated'); + if (!session || !session.user) { + return { error: 'User not authenticated' }; } const bountySubmission = await prisma.bountySubmission.create({ data: { - prLink: validatedData.prLink, - paymentMethod: validatedData.paymentMethod, + prLink: data.prLink, + paymentMethod: data.paymentMethod, userId: session.user.id, }, }); - - return bountySubmission; - } catch (error) { - console.error('Error submitting bounty:', error); - throw error; + return { data: bountySubmission }; + } catch (error: any) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + if (error.code === 'P2002') { + return { + error: 'PR already submitted. Try a different one.', + }; + } + } + return { error: 'Failed to submit bounty!' }; } } @@ -33,7 +39,7 @@ export async function getUserBounties() { try { const session = await getServerSession(authOptions); - if (!session?.user.id) { + if (!session || !session.user) { throw new Error('User not authenticated'); } @@ -48,3 +54,8 @@ export async function getUserBounties() { throw error; } } + +export const submitBounty = createSafeAction( + bountySubmissionSchema, + submitBountyHandler, +); diff --git a/src/actions/payoutMethods/schema.ts b/src/actions/payoutMethods/schema.ts index 461a934aa..0bb9d7c36 100644 --- a/src/actions/payoutMethods/schema.ts +++ b/src/actions/payoutMethods/schema.ts @@ -3,13 +3,13 @@ import { z } from 'zod'; export const payoutMethodSchema = z.object({ upiId: z .string() - .refine((value) => (/^[0-9A-Za-z._-]{2,256}@[A-Za-z]{2,64}$/).test(value), { + .refine((value) => /^[0-9A-Za-z._-]{2,256}@[A-Za-z]{2,64}$/.test(value), { message: 'Enter a valid UPI address', }) .optional(), solanaAddress: z .string() - .refine((value) => (/^[A-Za-z0-9]{44}$/).test(value), { + .refine((value) => /^[A-Za-z0-9]{44}$/.test(value), { message: 'Enter a valid Solana address', }) .optional(), @@ -18,13 +18,13 @@ export const payoutMethodSchema = z.object({ export const upiIdInsertSchema = z.object({ upiId: z .string() - .refine((value) => (/^[0-9A_Za-z._-]{2,256}@[A_Za-z]{2,64}$/).test(value), { + .refine((value) => /^[0-9A_Za-z._-]{2,256}@[A_Za-z]{2,64}$/.test(value), { message: 'Invalid UPI address', }), }); export const solanaAddressInsertSchema = z.object({ - solanaAddress: z.string().refine((value) => (/^[A-Za-z0-9]{44}$/).test(value), { + solanaAddress: z.string().refine((value) => /^[A-Za-z0-9]{44}$/.test(value), { message: 'Invalid Solana address', }), }); diff --git a/src/app/Bounty/page.tsx b/src/app/Bounty/page.tsx index ff2fcebab..45f8dce31 100644 --- a/src/app/Bounty/page.tsx +++ b/src/app/Bounty/page.tsx @@ -28,6 +28,7 @@ export default function Page() { }; const fetchUserBounties = async () => { + setIsLoading(true); try { const userBounties = await getUserBounties(); @@ -40,6 +41,8 @@ export default function Page() { setBounties(sortedBounties); } catch (error) { toast.error('Failed to fetch bounties'); + } finally { + setIsLoading(false); } }; @@ -48,11 +51,6 @@ export default function Page() { fetchUserBounties(); }, []); - useEffect(() => { - fetchPayoutMethods(); - fetchUserBounties(); - }, [isBountyDialogOpen]); - const handleBountyDialogOpen = () => { if (upiAddresses?.length || solanaAddresses?.length) { setIsBountyDialogOpen(true); @@ -61,14 +59,19 @@ export default function Page() { } }; - const handleBountyDialogClose = () => setIsBountyDialogOpen(false); + const handleBountyDialogClose = () => { + setIsBountyDialogOpen(false); + fetchUserBounties(); + }; return ( <>
-
-

Your Bounties

+
+

+ Your Bounties +

{bounty.prLink} diff --git a/src/app/admin/bounty/AdminBountyPage.tsx b/src/app/admin/bounty/AdminBountyPage.tsx index 21d399fdd..aa75d800b 100644 --- a/src/app/admin/bounty/AdminBountyPage.tsx +++ b/src/app/admin/bounty/AdminBountyPage.tsx @@ -26,12 +26,14 @@ export const AdminBountyPage = () => { const fetchBounties = async () => { setIsLoading(true); const result = await getBounties(); - if (result) { - setBounties(result.bounties); - setINRBounties(result.totalINRBounties); - setSOLBounties(result.totalSOLBounties); - setIsLoading(false); + if (result.error) { + toast.error(result.error); + } else { + setBounties(result.bounties!); + setINRBounties(result.totalINRBounties!); + setSOLBounties(result.totalSOLBounties!); } + setIsLoading(false); }; useEffect(() => { @@ -60,9 +62,10 @@ export const AdminBountyPage = () => { setIsConfirmDialogOpen(true); }; - const handleConfirmBounty = async (amount: number) => { + const handleConfirmBountyDialog = async (amount: number) => { if (selectedBounty) { const result = await confirmBounty(selectedBounty.id, amount); + if (result.success) { toast.success('Bounty confirmed successfully.'); if (currency === 'INR') { @@ -74,7 +77,7 @@ export const AdminBountyPage = () => { setIsConfirmDialogOpen(false); fetchBounties(); } else { - toast.error('Failed to confirm bounty.'); + toast.error(result.error); } } }; @@ -90,7 +93,9 @@ export const AdminBountyPage = () => { <>
-

Admin Bounty Page

+

+ Admin Bounty Management +

Total Bounties Distributed: INR {INRBounties.toFixed(2)} | SOL{' '} {SOLBounties} @@ -119,7 +124,7 @@ export const AdminBountyPage = () => { href={bounty.prLink} target="_blank" rel="noopener noreferrer" - className="text-blue-600 hover:underline dark:text-blue-400" + className="break-all text-blue-600 hover:underline dark:text-blue-400" > {bounty.prLink} @@ -154,7 +159,7 @@ export const AdminBountyPage = () => { isOpen={isConfirmDialogOpen} setIsOpen={setIsConfirmDialogOpen} onClose={() => setIsConfirmDialogOpen(false)} - onConfirm={handleConfirmBounty} + onConfirm={handleConfirmBountyDialog} currency={currency} /> diff --git a/src/components/PaymentDropdown.tsx b/src/components/PaymentDropdown.tsx index 0abf5c56e..2e64d6161 100644 --- a/src/components/PaymentDropdown.tsx +++ b/src/components/PaymentDropdown.tsx @@ -63,7 +63,7 @@ export default function PaymentMethodsDropdown({ <>

))} 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"] }