Skip to content

Commit

Permalink
feat: bounty submission and admin bounty management dashboard (#1326)
Browse files Browse the repository at this point in the history
  • Loading branch information
nischal-shetty2 authored Nov 17, 2024
1 parent 88bff90 commit 3207754
Show file tree
Hide file tree
Showing 16 changed files with 1,025 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
-- CreateTable
CREATE TABLE "BountySubmission" (
"id" TEXT NOT NULL,
"prLink" TEXT NOT NULL,
"paymentMethod" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'pending',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"amount" DOUBLE PRECISION NOT NULL DEFAULT 0,
"userId" TEXT NOT NULL,

CONSTRAINT "BountySubmission_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "BountySubmission_userId_prLink_key" ON "BountySubmission"("userId", "prLink");

-- AddForeignKey
ALTER TABLE "BountySubmission" ADD CONSTRAINT "BountySubmission_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
16 changes: 16 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ model User {
upiIds UpiId[] @relation("UserUpiIds")
solanaAddresses SolanaAddress[] @relation("UserSolanaAddresses")
githubUser GitHubLink? @relation("UserGithub")
bounties BountySubmission[]
}

model GitHubLink {
Expand Down Expand Up @@ -322,6 +323,21 @@ model Event {
updatedAt DateTime @updatedAt
}

model BountySubmission {
id String @id @default(uuid())
prLink String
paymentMethod String
status String @default("pending")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
amount Float @default(0)
userId String
user User @relation(fields: [userId], references: [id])
@@unique([userId, prLink])
}


enum VoteType {
UPVOTE
DOWNVOTE
Expand Down
109 changes: 109 additions & 0 deletions src/actions/bounty/adminActions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
'use server';
import prisma from '@/db';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { ROLES } from '../types';

export type Bounty = {
id: string;
prLink: string;
paymentMethod: string;
status: string;
createdAt: Date;
updatedAt: Date;
amount: number;
userId: string;
user: {
id: string;
name: string | null;
};
};

type BountyResponse = {
bounties?: Bounty[];
totalINRBounties?: number;
totalSOLBounties?: number;
error?: string;
};

export async function getBounties(): Promise<BountyResponse> {
const session = await getServerSession(authOptions);

if (!session || !session.user || session.user.role !== ROLES.ADMIN) {
return { error: 'Unauthorized or insufficient permissions' };
}

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;
}
});

return { bounties, totalINRBounties, totalSOLBounties };
} catch (e) {
return { error: 'An error occurred while approving the bounty.' };
}
}

export async function deleteBounty(bountyId: string) {
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 session = await getServerSession(authOptions);

if (!session || !session.user || session.user.role !== ROLES.ADMIN) {
return { error: 'Unauthorized or insufficient permissions' };
}

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.' };
}
}
2 changes: 2 additions & 0 deletions src/actions/bounty/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './userActions';
export * from './adminActions';
11 changes: 11 additions & 0 deletions src/actions/bounty/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { z } from 'zod';

export const bountySubmissionSchema = z.object({
prLink: z.string().url({ message: 'Invalid GitHub PR link' }),
paymentMethod: z.string(),
});

export const adminApprovalSchema = z.object({
bountyId: z.string(),
status: z.enum(['approved', 'rejected']),
});
9 changes: 9 additions & 0 deletions src/actions/bounty/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export interface BountySubmissionData {
prLink: string;
paymentMethod: string;
}

export interface AdminApprovalData {
bountyId: string;
status: 'approved' | 'rejected';
}
61 changes: 61 additions & 0 deletions src/actions/bounty/userActions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
'use server';
import prisma from '@/db';
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';

async function submitBountyHandler(data: BountySubmissionData) {
try {
const session = await getServerSession(authOptions);

if (!session || !session.user) {
return { error: 'User not authenticated' };
}

const bountySubmission = await prisma.bountySubmission.create({
data: {
prLink: data.prLink,
paymentMethod: data.paymentMethod,
userId: session.user.id,
},
});
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!' };
}
}

export async function getUserBounties() {
try {
const session = await getServerSession(authOptions);

if (!session || !session.user) {
throw new Error('User not authenticated');
}

const bounties = await prisma.bountySubmission.findMany({
where: { userId: session.user.id },
include: { user: true },
});

return bounties;
} catch (error) {
console.error('Error retrieving user bounties:', error);
throw error;
}
}

export const submitBounty = createSafeAction(
bountySubmissionSchema,
submitBountyHandler,
);
1 change: 0 additions & 1 deletion src/actions/payoutMethods/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import { createSafeAction } from '@/lib/create-safe-action';
const addUpiHandler = async (
data: InputTypeCreateUpi,
): Promise<ReturnTypeCreateUpi> => {
console.log(data);
const session = await getServerSession(authOptions);

if (!session || !session.user.id) {
Expand Down
Loading

0 comments on commit 3207754

Please sign in to comment.