"use server"; import { revalidatePath } from "next/cache"; import { z } from "zod"; import { createPeopleMutationInvocation, peopleMutationService, } from "@/features/housekeeping/domains/people/services/mutations"; import { createCorrelationId } from "@/features/housekeeping/foundation/contracts"; import { PERMS } from "@/lib/permissions"; import { adminAction } from "@/lib/safe-action"; import { ActionError, actionOk } from "@/lib/safe-action-shared"; import { canonicalTicketId } from "@/lib/services/ticket-replies"; const ticketIdField = z .union([z.string(), z.number(), z.bigint()]) .transform((value, context) => { try { return canonicalTicketId(value); } catch { context.addIssue({ code: "custom", message: "Invalid ticket identifier", }); return z.NEVER; } }); const replyHelpCenterTicketSchema = z.object({ ticketId: ticketIdField, content: z.string().min(1).max(5000), }); const helpCenterTicketIdSchema = z.object({ ticketId: ticketIdField, }); function revalidateHelpCenterTicketPaths(ticketId: bigint) { const id = String(ticketId); revalidatePath("/ase/people/support/help-tickets"); revalidatePath(`/ase/people/support/help-tickets/${id}`); revalidatePath("/help/tickets"); revalidatePath(`/help/tickets/${id}`); } async function execute( staff: { readonly id: number }, operation: "help-ticket.reply" | "help-ticket.status" | "help-ticket.unban", input: unknown, ) { return peopleMutationService.execute( createPeopleMutationInvocation(staff, createCorrelationId()), operation, input, ); } export const liftBanFromHelpTicket = adminAction( { permission: PERMS.USERS_BAN, schema: helpCenterTicketIdSchema, }, async (ctx) => { const ticketId = ctx.data.ticketId; const result = await execute(ctx.session.user, "help-ticket.unban", { ticketId: ticketId.toString(), }); if (!result.ok) { throw new ActionError( result.error.code === "CONFLICT" ? "Ticket has no requester to unban" : "Ticket not found", ); } revalidateHelpCenterTicketPaths(ticketId); revalidatePath("/ase/people/moderation/bans"); const removed = Number(result.data.output?.removed ?? 0); const userId = Number(result.data.output?.userId); revalidatePath(`/ase/people/users/${userId}`); return actionOk({ removed, userId }); }, ); const HELP_TICKET_EDIT = [PERMS.TICKETS_EDIT, PERMS.MOD_TICKETS_EDIT] as const; export const replyHelpCenterTicket = adminAction( { permission: HELP_TICKET_EDIT, schema: replyHelpCenterTicketSchema }, async (ctx) => { const ticketId = ctx.data.ticketId; const result = await execute(ctx.session.user, "help-ticket.reply", { ticketId: ticketId.toString(), content: ctx.data.content.trim(), }); if (!result.ok) throw new ActionError("Ticket not found"); revalidateHelpCenterTicketPaths(ticketId); return actionOk(); }, ); export const closeHelpCenterTicket = adminAction( { permission: HELP_TICKET_EDIT, schema: helpCenterTicketIdSchema }, async (ctx) => { const ticketId = ctx.data.ticketId; const result = await execute(ctx.session.user, "help-ticket.status", { ticketId: ticketId.toString(), status: "close", }); if (!result.ok) { throw new ActionError( result.error.code === "CONFLICT" ? "Ticket is already closed" : "Ticket not found", ); } revalidateHelpCenterTicketPaths(ticketId); return actionOk(); }, ); export const reopenHelpCenterTicket = adminAction( { permission: HELP_TICKET_EDIT, schema: helpCenterTicketIdSchema }, async (ctx) => { const ticketId = ctx.data.ticketId; const result = await execute(ctx.session.user, "help-ticket.status", { ticketId: ticketId.toString(), status: "reopen", }); if (!result.ok) { throw new ActionError( result.error.code === "CONFLICT" ? "Ticket is already open" : "Ticket not found", ); } revalidateHelpCenterTicketPaths(ticketId); return actionOk(); }, );