Replace raw db.execute tuple casts with queryRows/rowsFrom/execResult/ affectedRows helpers from lib/db, drop redundant mysql2 casts on typed query builders, and centralize per-test fakeForm into test/fake-form. Update db mocks in tests so helpers resolve against mocked execute.
209 lines
5.5 KiB
TypeScript
209 lines
5.5 KiB
TypeScript
"use server";
|
|
|
|
import { eq } from "drizzle-orm";
|
|
import { revalidatePath } from "next/cache";
|
|
import { z } from "zod";
|
|
import {
|
|
Ban,
|
|
db,
|
|
WebsiteHelpCenterTicketReplies,
|
|
WebsiteHelpCenterTickets,
|
|
} from "@/lib/db";
|
|
import { PERMS } from "@/lib/permissions";
|
|
import { adminAction } from "@/lib/safe-action";
|
|
import { ActionError, actionOk } from "@/lib/safe-action-shared";
|
|
import { logAudit } from "@/lib/services/audit";
|
|
|
|
const ticketIdField = z
|
|
.union([z.string(), z.number(), z.bigint()])
|
|
.transform((v) => BigInt(String(v)));
|
|
|
|
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("/admin/help-tickets");
|
|
revalidatePath(`/admin/help-tickets/${id}`);
|
|
revalidatePath("/mod/help-tickets");
|
|
revalidatePath(`/mod/help-tickets/${id}`);
|
|
revalidatePath("/help/tickets");
|
|
revalidatePath(`/help/tickets/${id}`);
|
|
}
|
|
|
|
export const liftBanFromHelpTicket = adminAction(
|
|
{
|
|
permission: PERMS.USERS_BAN,
|
|
schema: helpCenterTicketIdSchema,
|
|
},
|
|
async (ctx) => {
|
|
const ticketId = ctx.data.ticketId;
|
|
const [ticket] = await db
|
|
.select({
|
|
id: WebsiteHelpCenterTickets.id,
|
|
userId: WebsiteHelpCenterTickets.userId,
|
|
open: WebsiteHelpCenterTickets.open,
|
|
title: WebsiteHelpCenterTickets.title,
|
|
})
|
|
.from(WebsiteHelpCenterTickets)
|
|
.where(eq(WebsiteHelpCenterTickets.id, ticketId))
|
|
.limit(1);
|
|
if (!ticket) throw new ActionError("Ticket not found");
|
|
if (ticket.userId == null) {
|
|
throw new ActionError("Ticket has no requester to unban");
|
|
}
|
|
|
|
const result = await db.delete(Ban).where(eq(Ban.userId, ticket.userId));
|
|
const removed = Number(result[0]?.affectedRows ?? 0);
|
|
|
|
const now = new Date();
|
|
if (ticket.open) {
|
|
await db
|
|
.update(WebsiteHelpCenterTickets)
|
|
.set({ open: false, updatedAt: now })
|
|
.where(eq(WebsiteHelpCenterTickets.id, ticketId));
|
|
}
|
|
|
|
logAudit({
|
|
userId: ctx.session.user.id,
|
|
action: "unban_via_help_ticket",
|
|
target: "User",
|
|
targetId: ticket.userId,
|
|
after: {
|
|
ticketId: String(ticketId),
|
|
removedBans: removed,
|
|
title: ticket.title,
|
|
},
|
|
});
|
|
|
|
revalidateHelpCenterTicketPaths(ticketId);
|
|
revalidatePath("/admin/bans");
|
|
revalidatePath(`/admin/users/show/${ticket.userId}`);
|
|
return actionOk({ removed, userId: ticket.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 [ticket] = await db
|
|
.select({
|
|
id: WebsiteHelpCenterTickets.id,
|
|
open: WebsiteHelpCenterTickets.open,
|
|
})
|
|
.from(WebsiteHelpCenterTickets)
|
|
.where(eq(WebsiteHelpCenterTickets.id, ticketId))
|
|
.limit(1);
|
|
|
|
if (!ticket) throw new ActionError("Ticket not found");
|
|
|
|
const now = new Date();
|
|
const staffId = Number(ctx.session.user.id);
|
|
|
|
await db.transaction(async (tx) => {
|
|
await tx.insert(WebsiteHelpCenterTicketReplies).values({
|
|
ticketId,
|
|
userId: staffId,
|
|
content: ctx.data.content.trim(),
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
});
|
|
await tx
|
|
.update(WebsiteHelpCenterTickets)
|
|
.set({ updatedAt: now })
|
|
.where(eq(WebsiteHelpCenterTickets.id, ticketId));
|
|
});
|
|
|
|
logAudit({
|
|
userId: staffId,
|
|
action: "help_center_ticket_reply",
|
|
target: "WebsiteHelpCenterTickets",
|
|
targetId: Number(ticketId),
|
|
});
|
|
|
|
revalidateHelpCenterTicketPaths(ticketId);
|
|
return actionOk();
|
|
},
|
|
);
|
|
|
|
export const closeHelpCenterTicket = adminAction(
|
|
{ permission: HELP_TICKET_EDIT, schema: helpCenterTicketIdSchema },
|
|
async (ctx) => {
|
|
const ticketId = ctx.data.ticketId;
|
|
const [ticket] = await db
|
|
.select({
|
|
id: WebsiteHelpCenterTickets.id,
|
|
open: WebsiteHelpCenterTickets.open,
|
|
})
|
|
.from(WebsiteHelpCenterTickets)
|
|
.where(eq(WebsiteHelpCenterTickets.id, ticketId))
|
|
.limit(1);
|
|
|
|
if (!ticket) throw new ActionError("Ticket not found");
|
|
if (!ticket.open) throw new ActionError("Ticket is already closed");
|
|
|
|
const now = new Date();
|
|
await db
|
|
.update(WebsiteHelpCenterTickets)
|
|
.set({ open: false, updatedAt: now })
|
|
.where(eq(WebsiteHelpCenterTickets.id, ticketId));
|
|
|
|
logAudit({
|
|
userId: Number(ctx.session.user.id),
|
|
action: "help_center_ticket_close",
|
|
target: "WebsiteHelpCenterTickets",
|
|
targetId: Number(ticketId),
|
|
before: { open: true },
|
|
after: { open: false },
|
|
});
|
|
|
|
revalidateHelpCenterTicketPaths(ticketId);
|
|
return actionOk();
|
|
},
|
|
);
|
|
|
|
export const reopenHelpCenterTicket = adminAction(
|
|
{ permission: HELP_TICKET_EDIT, schema: helpCenterTicketIdSchema },
|
|
async (ctx) => {
|
|
const ticketId = ctx.data.ticketId;
|
|
const [ticket] = await db
|
|
.select({
|
|
id: WebsiteHelpCenterTickets.id,
|
|
open: WebsiteHelpCenterTickets.open,
|
|
})
|
|
.from(WebsiteHelpCenterTickets)
|
|
.where(eq(WebsiteHelpCenterTickets.id, ticketId))
|
|
.limit(1);
|
|
|
|
if (!ticket) throw new ActionError("Ticket not found");
|
|
if (ticket.open) throw new ActionError("Ticket is already open");
|
|
|
|
const now = new Date();
|
|
await db
|
|
.update(WebsiteHelpCenterTickets)
|
|
.set({ open: true, updatedAt: now })
|
|
.where(eq(WebsiteHelpCenterTickets.id, ticketId));
|
|
|
|
logAudit({
|
|
userId: Number(ctx.session.user.id),
|
|
action: "help_center_ticket_reopen",
|
|
target: "WebsiteHelpCenterTickets",
|
|
targetId: Number(ticketId),
|
|
before: { open: false },
|
|
after: { open: true },
|
|
});
|
|
|
|
revalidateHelpCenterTicketPaths(ticketId);
|
|
return actionOk();
|
|
},
|
|
);
|