Files
Epicnabbo-Catalogus-Updated…/src/actions/admin-help-tickets.ts
T

143 lines
3.9 KiB
TypeScript

"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();
},
);