277 lines
8.2 KiB
TypeScript
277 lines
8.2 KiB
TypeScript
"use server";
|
|
|
|
import { and, count, eq } from "drizzle-orm";
|
|
import { revalidatePath } from "next/cache";
|
|
import { z } from "zod";
|
|
import { contentMutationService } from "@/features/housekeeping/domains/content/services/mutations";
|
|
import {
|
|
db,
|
|
WebsiteEvent,
|
|
WebsiteEventRegistration,
|
|
WebsiteEventType,
|
|
} from "@/lib/db";
|
|
import { PERMS } from "@/lib/permissions";
|
|
import { adminAction, authAction } from "@/lib/safe-action";
|
|
import { ActionError, actionError, actionOk } from "@/lib/safe-action-shared";
|
|
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 result = await contentMutationService.execute(
|
|
{
|
|
correlationId: String(ctx.requestId),
|
|
expectedActorId: Number(ctx.session.user.id),
|
|
legacy: true,
|
|
},
|
|
"event-type.change",
|
|
{ action: "create", ...ctx.data },
|
|
);
|
|
if (!result.ok) throw new ActionError("Event type creation failed");
|
|
return actionOk({ id: Number(result.data.output?.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 result = await contentMutationService.execute(
|
|
{
|
|
correlationId: String(ctx.requestId),
|
|
expectedActorId: Number(ctx.session.user.id),
|
|
legacy: true,
|
|
},
|
|
"event-type.change",
|
|
{ action: "update", ...ctx.data },
|
|
);
|
|
if (!result.ok) throw new ActionError("Event type not found");
|
|
return actionOk({ id: ctx.data.id });
|
|
},
|
|
);
|
|
|
|
const deleteEventTypeInput = z.object({
|
|
id: z.coerce.number().int().positive(),
|
|
});
|
|
|
|
export const deleteEventType = adminAction(
|
|
{ permission: PERMS.EVENTS_EDIT, schema: deleteEventTypeInput },
|
|
async (ctx) => {
|
|
const result = await contentMutationService.execute(
|
|
{
|
|
correlationId: String(ctx.requestId),
|
|
expectedActorId: Number(ctx.session.user.id),
|
|
legacy: true,
|
|
},
|
|
"event-type.change",
|
|
{ action: "delete", ...ctx.data },
|
|
);
|
|
if (!result.ok) throw new ActionError("Event type not found");
|
|
return actionOk();
|
|
},
|
|
);
|
|
|
|
// ── Events ──────────────────────────────────────────────────────────
|
|
|
|
export const createEvent = adminAction(
|
|
{ permission: PERMS.EVENTS_EDIT, schema: createEventSchema },
|
|
async (ctx) => {
|
|
const result = await contentMutationService.execute(
|
|
{
|
|
correlationId: String(ctx.requestId),
|
|
expectedActorId: Number(ctx.session.user.id),
|
|
legacy: true,
|
|
},
|
|
"event.change",
|
|
{ action: "create", ...ctx.data },
|
|
);
|
|
if (!result.ok) throw new ActionError("Event creation failed");
|
|
return actionOk({ id: Number(result.data.output?.id) });
|
|
},
|
|
);
|
|
|
|
const updateEventInput = updateEventSchema.extend({
|
|
id: z.coerce.number().int().positive(),
|
|
});
|
|
|
|
export const updateEvent = adminAction(
|
|
{ permission: PERMS.EVENTS_EDIT, schema: updateEventInput },
|
|
async (ctx) => {
|
|
const result = await contentMutationService.execute(
|
|
{
|
|
correlationId: String(ctx.requestId),
|
|
expectedActorId: Number(ctx.session.user.id),
|
|
legacy: true,
|
|
},
|
|
"event.change",
|
|
{ action: "update", ...ctx.data },
|
|
);
|
|
if (!result.ok) throw new ActionError("Event not found");
|
|
return actionOk({ id: ctx.data.id });
|
|
},
|
|
);
|
|
|
|
const deleteEventInput = z.object({
|
|
id: z.coerce.number().int().positive(),
|
|
});
|
|
|
|
export const deleteEvent = adminAction(
|
|
{ permission: PERMS.EVENTS_EDIT, schema: deleteEventInput },
|
|
async (ctx) => {
|
|
const result = await contentMutationService.execute(
|
|
{
|
|
correlationId: String(ctx.requestId),
|
|
expectedActorId: Number(ctx.session.user.id),
|
|
legacy: true,
|
|
},
|
|
"event.change",
|
|
{ action: "delete", ...ctx.data },
|
|
);
|
|
if (!result.ok) throw new ActionError("Event not found");
|
|
return actionOk();
|
|
},
|
|
);
|
|
|
|
// ── Prizes ──────────────────────────────────────────────────────────
|
|
|
|
export const addEventPrize = adminAction(
|
|
{ permission: PERMS.EVENTS_EDIT, schema: eventPrizeSchema },
|
|
async (ctx) => {
|
|
const result = await contentMutationService.execute(
|
|
{
|
|
correlationId: String(ctx.requestId),
|
|
expectedActorId: Number(ctx.session.user.id),
|
|
legacy: true,
|
|
},
|
|
"event-prize.change",
|
|
{ action: "create", ...ctx.data },
|
|
);
|
|
if (!result.ok) throw new ActionError("Event prize creation failed");
|
|
return actionOk({ id: Number(result.data.output?.id) });
|
|
},
|
|
);
|
|
|
|
const deletePrizeInput = z.object({ id: z.coerce.number().int().positive() });
|
|
|
|
export const deleteEventPrize = adminAction(
|
|
{ permission: PERMS.EVENTS_EDIT, schema: deletePrizeInput },
|
|
async (ctx) => {
|
|
const result = await contentMutationService.execute(
|
|
{
|
|
correlationId: String(ctx.requestId),
|
|
expectedActorId: Number(ctx.session.user.id),
|
|
legacy: true,
|
|
},
|
|
"event-prize.change",
|
|
{ action: "delete", ...ctx.data },
|
|
);
|
|
if (!result.ok) throw new ActionError("Event prize deletion failed");
|
|
return actionOk();
|
|
},
|
|
);
|
|
|
|
// ── Winners ─────────────────────────────────────────────────────────
|
|
|
|
export const addEventWinner = adminAction(
|
|
{ permission: PERMS.EVENTS_EDIT, schema: eventWinnerSchema },
|
|
async (ctx) => {
|
|
const result = await contentMutationService.execute(
|
|
{
|
|
correlationId: String(ctx.requestId),
|
|
expectedActorId: Number(ctx.session.user.id),
|
|
legacy: true,
|
|
},
|
|
"event-winner.add",
|
|
ctx.data,
|
|
);
|
|
if (!result.ok) throw new ActionError("Event winner creation failed");
|
|
return actionOk({ id: Number(result.data.output?.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 db
|
|
.select({
|
|
id: WebsiteEvent.id,
|
|
status: WebsiteEvent.status,
|
|
endsAt: WebsiteEvent.endsAt,
|
|
maxPlayers: WebsiteEvent.maxPlayers,
|
|
minRank: WebsiteEventType.minRank,
|
|
})
|
|
.from(WebsiteEvent)
|
|
.innerJoin(WebsiteEventType, eq(WebsiteEvent.typeId, WebsiteEventType.id))
|
|
.where(eq(WebsiteEvent.id, ctx.data.eventId))
|
|
.limit(1);
|
|
|
|
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.minRank > 0) {
|
|
const rank = Number(ctx.session.user.rank ?? 0);
|
|
if (rank < event.minRank) {
|
|
return actionError("Your rank is too low to join this event");
|
|
}
|
|
}
|
|
if (event.maxPlayers != null) {
|
|
const [regCount] = await db
|
|
.select({ value: count() })
|
|
.from(WebsiteEventRegistration)
|
|
.where(eq(WebsiteEventRegistration.eventId, event.id));
|
|
if ((regCount?.value ?? 0) >= event.maxPlayers) {
|
|
return actionError("This event is full");
|
|
}
|
|
}
|
|
|
|
const [existing] = await db
|
|
.select({ id: WebsiteEventRegistration.id })
|
|
.from(WebsiteEventRegistration)
|
|
.where(
|
|
and(
|
|
eq(WebsiteEventRegistration.eventId, event.id),
|
|
eq(WebsiteEventRegistration.userId, userId),
|
|
),
|
|
)
|
|
.limit(1);
|
|
if (existing) return actionError("You are already registered");
|
|
|
|
await db.insert(WebsiteEventRegistration).values({
|
|
eventId: event.id,
|
|
userId,
|
|
});
|
|
|
|
revalidatePath("/events");
|
|
revalidatePath(`/events/${event.id}`);
|
|
return actionOk({ eventId: event.id });
|
|
},
|
|
);
|