"use server"; import { revalidatePath } from "next/cache"; import { z } from "zod"; import { PERMS } from "@/lib/permissions"; import { prisma } from "@/lib/prisma"; import { adminAction, authAction } from "@/lib/safe-action"; import { ActionError, actionError, actionOk } from "@/lib/safe-action-shared"; import { logAudit } from "@/lib/services/audit"; import { createEventSchema, eventPrizeSchema, eventTypeSchema, eventWinnerSchema, registerForEventSchema, updateEventSchema, } from "@/lib/validators/event"; // ── Event Types ───────────────────────────────────────────────────── export const createEventType = adminAction( { permission: PERMS.EVENTS_EDIT, schema: eventTypeSchema }, async (ctx) => { const eventType = await prisma.websiteEventType.create({ data: ctx.data, }); logAudit({ userId: ctx.session.user.id, action: "event_type_create", target: "WebsiteEventType", targetId: eventType.id, after: { name: eventType.name }, }); return actionOk({ id: eventType.id }); }, ); const updateEventTypeInput = eventTypeSchema.partial().extend({ id: z.coerce.number().int().positive(), }); export const updateEventType = adminAction( { permission: PERMS.EVENTS_EDIT, schema: updateEventTypeInput }, async (ctx) => { const { id, ...data } = ctx.data; const existing = await prisma.websiteEventType.findUnique({ where: { id }, }); if (!existing) throw new ActionError("Event type not found"); await prisma.websiteEventType.update({ where: { id }, data }); logAudit({ userId: ctx.session.user.id, action: "event_type_update", target: "WebsiteEventType", targetId: id, before: { name: existing.name }, after: data, }); return actionOk({ id }); }, ); const deleteEventTypeInput = z.object({ id: z.coerce.number().int().positive(), }); export const deleteEventType = adminAction( { permission: PERMS.EVENTS_EDIT, schema: deleteEventTypeInput }, async (ctx) => { const existing = await prisma.websiteEventType.findUnique({ where: { id: ctx.data.id }, }); if (!existing) throw new ActionError("Event type not found"); await prisma.websiteEventType.delete({ where: { id: ctx.data.id } }); logAudit({ userId: ctx.session.user.id, action: "event_type_delete", target: "WebsiteEventType", targetId: ctx.data.id, before: { name: existing.name }, }); return actionOk(); }, ); // ── Events ────────────────────────────────────────────────────────── export const createEvent = adminAction( { permission: PERMS.EVENTS_EDIT, schema: createEventSchema }, async (ctx) => { const event = await prisma.websiteEvent.create({ data: { ...ctx.data, hostUserId: Number(ctx.session.user.id), }, }); logAudit({ userId: ctx.session.user.id, action: "event_create", target: "WebsiteEvent", targetId: event.id, after: { title: event.title }, }); return actionOk({ id: event.id }); }, ); const updateEventInput = updateEventSchema.extend({ id: z.coerce.number().int().positive(), }); export const updateEvent = adminAction( { permission: PERMS.EVENTS_EDIT, schema: updateEventInput }, async (ctx) => { const { id, ...data } = ctx.data; const existing = await prisma.websiteEvent.findUnique({ where: { id } }); if (!existing) throw new ActionError("Event not found"); await prisma.websiteEvent.update({ where: { id }, data }); logAudit({ userId: ctx.session.user.id, action: "event_update", target: "WebsiteEvent", targetId: id, before: { title: existing.title, status: existing.status }, after: data, }); return actionOk({ id }); }, ); const deleteEventInput = z.object({ id: z.coerce.number().int().positive(), }); export const deleteEvent = adminAction( { permission: PERMS.EVENTS_EDIT, schema: deleteEventInput }, async (ctx) => { const existing = await prisma.websiteEvent.findUnique({ where: { id: ctx.data.id }, }); if (!existing) throw new ActionError("Event not found"); await prisma.websiteEvent.delete({ where: { id: ctx.data.id } }); logAudit({ userId: ctx.session.user.id, action: "event_delete", target: "WebsiteEvent", targetId: ctx.data.id, before: { title: existing.title }, }); return actionOk(); }, ); // ── Prizes ────────────────────────────────────────────────────────── export const addEventPrize = adminAction( { permission: PERMS.EVENTS_EDIT, schema: eventPrizeSchema }, async (ctx) => { const prize = await prisma.websiteEventPrize.create({ data: ctx.data }); return actionOk({ id: prize.id }); }, ); const deletePrizeInput = z.object({ id: z.coerce.number().int().positive() }); export const deleteEventPrize = adminAction( { permission: PERMS.EVENTS_EDIT, schema: deletePrizeInput }, async (ctx) => { await prisma.websiteEventPrize.delete({ where: { id: ctx.data.id } }); return actionOk(); }, ); // ── Winners ───────────────────────────────────────────────────────── export const addEventWinner = adminAction( { permission: PERMS.EVENTS_EDIT, schema: eventWinnerSchema }, async (ctx) => { const winner = await prisma.websiteEventWinner.create({ data: ctx.data }); logAudit({ userId: ctx.session.user.id, action: "event_winner_add", target: "WebsiteEventWinner", targetId: winner.id, after: { eventId: ctx.data.eventId, userId: ctx.data.userId, position: ctx.data.position, }, }); return actionOk({ id: winner.id }); }, ); // ── Public site: register ─────────────────────────────────────────── export const registerForEvent = authAction( { schema: registerForEventSchema, rateLimitKey: "event-register", rateLimitMax: 10, rateLimitWindowMs: 60_000, }, async (ctx) => { const userId = Number(ctx.session.user.id); if (!Number.isInteger(userId) || userId <= 0) { return actionError("Unauthorized"); } const event = await prisma.websiteEvent.findUnique({ where: { id: ctx.data.eventId }, include: { type: true, _count: { select: { registrations: true } }, }, }); if (!event) return actionError("Event not found"); if (event.status !== "published") { return actionError("This event is not open for registration"); } if (event.endsAt && event.endsAt.getTime() < Date.now()) { return actionError("This event has already ended"); } if (event.type.minRank > 0) { const rank = Number(ctx.session.user.rank ?? 0); if (rank < event.type.minRank) { return actionError("Your rank is too low to join this event"); } } if ( event.maxPlayers != null && event._count.registrations >= event.maxPlayers ) { return actionError("This event is full"); } const existing = await prisma.websiteEventRegistration.findUnique({ where: { eventId_userId: { eventId: event.id, userId }, }, }); if (existing) return actionError("You are already registered"); await prisma.websiteEventRegistration.create({ data: { eventId: event.id, userId }, }); revalidatePath("/events"); revalidatePath(`/events/${event.id}`); return actionOk({ eventId: event.id }); }, );