"use server"; import config from "@payload-config"; import { getPayload } from "payload"; import { isPayloadUser } from "@/utils/access-control/isPayloadUser"; import { isSuperuser } from "@/utils/access-control/hasPermission"; import { emitGameEvent } from "@/utils/event-log/emit"; import { EventTypes } from "@/utils/event-log/eventTypes"; import { countGrantsByGrantor, findUserProfile, getGrantorAssignments, isRecipientEligible, } from "@/lib/awards/grant"; import { cleanupDuplicateAwards, type CleanupResult } from "@/lib/awards/index"; import type { Assignment, Profile } from "@/payload-types"; interface ActionResult { success: boolean; error?: string; data?: T; } async function authenticate() { const payloadConfig = await config; const payload = await getPayload({ config: payloadConfig }); const { headers } = await import("next/headers"); const hdrs = await headers(); const { user } = await payload.auth({ headers: hdrs, canSetHeaders: false, }); if (!isPayloadUser(user)) { throw new Error("Unauthorized"); } return { payload, user }; } export interface GrantResult { recipientUserId: number; success: boolean; error?: string; } export async function grantAward(input: { recipientUserIds: number[]; awardId: number; reason?: string; }): Promise> { try { const { payload, user } = await authenticate(); if (input.recipientUserIds.length === 0) { return { success: false, error: "No recipients selected." }; } let award; try { award = await payload.findByID({ collection: "awards", id: input.awardId, depth: 0, overrideAccess: true, }); } catch { return { success: false, error: "Award not found." }; } let grantorProfile: Profile | null = null; const superuser = await isSuperuser(payload, user); const isMedal = award.type === "medal"; if (isMedal && !superuser) { return { success: false, error: "Only superusers can grant medals." }; } let grantorAssignmentId: number | undefined; let scope: "same_command" | "whitelist" | "anyone" | null | undefined; let whitelist: (number | Assignment)[] | null | undefined; let remaining: number | null = null; if (!superuser && !isMedal) { const allowedTypes = award.grantConfig?.allowedGrantorAssignmentTypes ?? []; if (allowedTypes.length === 0) { return { success: false, error: "This award cannot be granted by leaders." }; } const grantorAssignments = await getGrantorAssignments( payload, user.id as number, allowedTypes, ); if (grantorAssignments.length === 0) { return { success: false, error: "You are not authorized to grant this award." }; } grantorAssignmentId = grantorAssignments[0].id; scope = award.grantConfig?.recipientScope ?? "same_command"; whitelist = award.grantConfig?.eligibleRecipientAssignments; const maxGrants = award.grantConfig?.maxGrantsPerGrantor ?? null; if (maxGrants) { grantorProfile = await findUserProfile(payload, user.id as number); if (!grantorProfile) { return { success: false, error: "You do not have a profile." }; } const issued = await countGrantsByGrantor(payload, award.id, grantorProfile.id); remaining = maxGrants - issued; if (remaining <= 0) { return { success: false, error: "You have reached the maximum number of grants for this award.", }; } } } if (!grantorProfile) { grantorProfile = await findUserProfile(payload, user.id as number); } if (!grantorProfile) { return { success: false, error: "You do not have a profile." }; } const results: GrantResult[] = []; let granted = 0; let processed = 0; for (const recipientUserId of input.recipientUserIds) { if (remaining !== null && processed >= remaining) break; if (grantorAssignmentId !== undefined) { const eligible = await isRecipientEligible( payload, recipientUserId, scope, grantorAssignmentId, whitelist, ); if (!eligible) { results.push({ recipientUserId, success: false, error: "Recipient is not eligible for this award.", }); processed += 1; continue; } } const recipientProfile = await findUserProfile(payload, recipientUserId); if (!recipientProfile) { results.push({ recipientUserId, success: false, error: "Recipient does not have a profile.", }); processed += 1; continue; } const progression = recipientProfile.progression; const currentAwards = progression?.awards ?? []; const alreadyHasAward = currentAwards.some((e) => { const id = typeof e.award === "number" ? e.award : e.award?.id; return id === award.id; }); if (alreadyHasAward) { results.push({ recipientUserId, success: false, error: "Recipient already has this award.", }); processed += 1; continue; } await payload.update({ collection: "profiles", id: recipientProfile.id, data: { progression: { ...progression, awards: [ ...currentAwards, { award: award.id, awardedBy: grantorProfile.id, awardedAt: new Date().toISOString(), awardReason: input.reason ?? null, }, ], }, }, overrideAccess: true, }); await emitGameEvent(payload, { type: EventTypes.AwardGrant, message: `Granted "${award.name}" to recipient profile #${recipientProfile.id}`, actor: user.id, data: { awardId: award.id, grantorProfileId: grantorProfile.id }, }); results.push({ recipientUserId, success: true }); granted += 1; processed += 1; } return { success: true, data: { results, granted, failed: results.length - granted }, }; } catch (e) { return { success: false, error: e instanceof Error ? e.message : "Unknown error", }; } } export async function cleanupDuplicateAwardsAction(): Promise< ActionResult > { try { const { payload, user } = await authenticate(); const superuser = await isSuperuser(payload, user); if (!superuser) { return { success: false, error: "Only superusers can run award cleanup." }; } const result = await cleanupDuplicateAwards(payload); return { success: true, data: result }; } catch (e) { return { success: false, error: e instanceof Error ? e.message : "Unknown error", }; } }