diff --git a/src/app/api/event/[eventID]/participant/route.ts b/src/app/api/event/[eventID]/participant/route.ts new file mode 100644 index 0000000..91ce965 --- /dev/null +++ b/src/app/api/event/[eventID]/participant/route.ts @@ -0,0 +1,200 @@ +import { prisma } from '@/prisma'; +import { auth } from '@/auth'; +import { + returnZodTypeCheckedResponse, + userAuthenticated, +} from '@/lib/apiHelpers'; +import { + ErrorResponseSchema, + ZodErrorResponseSchema, +} from '@/app/api/validation'; +import { + inviteParticipantSchema, + ParticipantResponseSchema, + ParticipantsResponseSchema, +} from './validation'; + +export const GET = auth(async (req, { params }) => { + const authCheck = userAuthenticated(req); + if (!authCheck.continue) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, + authCheck.response, + authCheck.metadata, + ); + + const dbUser = await prisma.user.findUnique({ + where: { + id: authCheck.user.id, + }, + }); + + if (!dbUser) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, + { success: false, message: 'User not found' }, + { status: 404 }, + ); + + const eventID = (await params).eventID; + + const isParticipant = await prisma.meetingParticipant.findFirst({ + where: { + meeting_id: eventID, + user_id: dbUser.id, + }, + select: { + status: true, + }, + }); + + const isOrganizer = await prisma.meeting.findFirst({ + where: { + id: eventID, + organizer_id: dbUser.id, + }, + select: { + id: true, + }, + }); + + if (!isParticipant && !isOrganizer) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, + { + success: false, + message: 'User is not a participant or organizer of this event', + }, + { status: 403 }, + ); + + const participants = await prisma.meetingParticipant.findMany({ + where: { + meeting_id: eventID, + }, + select: { + user: { + select: { + id: true, + name: true, + first_name: true, + last_name: true, + image: true, + timezone: true, + }, + }, + status: true, + }, + }); + + return returnZodTypeCheckedResponse(ParticipantsResponseSchema, { + success: true, + participants, + }); +}); + +export const POST = auth(async (req, { params }) => { + const authCheck = userAuthenticated(req); + if (!authCheck.continue) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, + authCheck.response, + authCheck.metadata, + ); + + const dbUser = await prisma.user.findUnique({ + where: { + id: authCheck.user.id, + }, + }); + + if (!dbUser) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, + { success: false, message: 'User not found' }, + { status: 404 }, + ); + + const eventID = (await params).eventID; + + const isOrganizer = await prisma.meeting.findFirst({ + where: { + id: eventID, + organizer_id: dbUser.id, + }, + }); + + if (!isOrganizer) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, + { success: false, message: 'Only organizers can add participants' }, + { status: 403 }, + ); + + const dataRaw = await req.json(); + const data = await inviteParticipantSchema.safeParseAsync(dataRaw); + if (!data.success) { + return returnZodTypeCheckedResponse( + ZodErrorResponseSchema, + { + success: false, + message: 'Invalid request data', + errors: data.error.issues, + }, + { status: 400 }, + ); + } + const { user_id } = data.data; + + const participantExists = await prisma.meetingParticipant.findFirst({ + where: { + meeting_id: eventID, + user_id, + }, + }); + + if (participantExists) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, + { + success: false, + message: 'User is already a participant of this event', + }, + { status: 409 }, + ); + + const newParticipant = await prisma.meetingParticipant.create({ + data: { + meeting_id: eventID, + user_id, + }, + select: { + user: { + select: { + id: true, + name: true, + first_name: true, + last_name: true, + image: true, + timezone: true, + }, + }, + status: true, + }, + }); + + return returnZodTypeCheckedResponse(ParticipantResponseSchema, { + success: true, + participant: { + user: { + id: newParticipant.user.id, + name: newParticipant.user.name, + first_name: newParticipant.user.first_name, + last_name: newParticipant.user.last_name, + image: newParticipant.user.image, + timezone: newParticipant.user.timezone, + }, + status: newParticipant.status, + }, + }); +}); diff --git a/src/app/api/event/[eventID]/participant/swagger.ts b/src/app/api/event/[eventID]/participant/swagger.ts new file mode 100644 index 0000000..38dfd58 --- /dev/null +++ b/src/app/api/event/[eventID]/participant/swagger.ts @@ -0,0 +1,72 @@ +import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; +import zod from 'zod/v4'; +import { + ParticipantsResponseSchema, + ParticipantResponseSchema, + inviteParticipantSchema, +} from './validation'; +import { + invalidRequestDataResponse, + notAuthenticatedResponse, + serverReturnedDataValidationErrorResponse, + userNotFoundResponse, +} from '@/lib/defaultApiResponses'; +import { EventIdParamSchema } from '@/app/api/validation'; + +export default function registerSwaggerPaths(registry: OpenAPIRegistry) { + registry.registerPath({ + method: 'get', + path: '/api/event/{eventID}/participant', + request: { + params: zod.object({ + eventID: EventIdParamSchema, + }), + }, + responses: { + 200: { + description: 'List participants for the event', + content: { + 'application/json': { + schema: ParticipantsResponseSchema, + }, + }, + }, + ...notAuthenticatedResponse, + ...userNotFoundResponse, + ...serverReturnedDataValidationErrorResponse, + }, + tags: ['Event Participant'], + }); + + registry.registerPath({ + method: 'post', + path: '/api/event/{eventID}/participant', + request: { + params: zod.object({ + eventID: EventIdParamSchema, + }), + body: { + content: { + 'application/json': { + schema: inviteParticipantSchema, + }, + }, + }, + }, + responses: { + 200: { + description: 'Participant invited successfully', + content: { + 'application/json': { + schema: ParticipantResponseSchema, + }, + }, + }, + ...invalidRequestDataResponse, + ...notAuthenticatedResponse, + ...userNotFoundResponse, + ...serverReturnedDataValidationErrorResponse, + }, + tags: ['Event Participant'], + }); +} diff --git a/src/app/api/event/[eventID]/participant/validation.ts b/src/app/api/event/[eventID]/participant/validation.ts new file mode 100644 index 0000000..bacb9ac --- /dev/null +++ b/src/app/api/event/[eventID]/participant/validation.ts @@ -0,0 +1,50 @@ +import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi'; +import zod from 'zod/v4'; +import { + existingUserIdServerSchema, + PublicUserSchema, +} from '@/app/api/user/validation'; + +extendZodWithOpenApi(zod); + +export const participantStatusSchema = zod.enum([ + 'ACCEPTED', + 'DECLINED', + 'TENTATIVE', + 'PENDING', +]); + +export const inviteParticipantSchema = zod + .object({ + user_id: existingUserIdServerSchema, + }) + .openapi('InviteParticipant', { + description: 'Schema for inviting a participant to an event', + }); + +export const updateParticipantSchema = zod + .object({ + status: participantStatusSchema, + }) + .openapi('UpdateParticipant', { + description: 'Schema for updating participant status in an event', + }); + +export const ParticipantSchema = zod + .object({ + user: PublicUserSchema, + status: participantStatusSchema, + }) + .openapi('Participant', { + description: 'Participant information including user and status', + }); + +export const ParticipantResponseSchema = zod.object({ + success: zod.boolean(), + participant: ParticipantSchema, +}); + +export const ParticipantsResponseSchema = zod.object({ + success: zod.boolean(), + participants: zod.array(ParticipantSchema), +});