"use server"; import { PERMS } from "@/lib/permissions"; import { prisma } from "@/lib/prisma"; import { adminAction, authAction } from "@/lib/safe-action"; import { ActionError, actionOk } from "@/lib/safe-action-shared"; import { logAudit } from "@/lib/services/audit"; import { notify } from "@/lib/services/webhook"; import { assignTicketSchema, createTicketSchema, replyTicketSchema, updateTicketPrioritySchema, updateTicketStatusSchema, } from "@/lib/validators/ticket"; // ── User actions (authenticated, no admin perms needed) ────────────── export const createTicket = authAction({ schema: createTicketSchema }, async (ctx) => { const ticket = await prisma.websiteTicket.create({ data: { subject: ctx.data.subject, category: ctx.data.category, creatorId: ctx.session.user.id, }, }); // Create the first message await prisma.websiteTicketMessage.create({ data: { ticketId: ticket.id, userId: ctx.session.user.id, message: ctx.data.message, isStaff: 0, }, }); notify({ action: "ticket_create", actor: ctx.session.user.username, target: `#${ticket.id} - ${ticket.subject}`, details: `Category: ${ticket.category}`, }); return actionOk({ id: ticket.id }); }); export const userReplyTicket = authAction({ schema: replyTicketSchema }, async (ctx) => { const ticket = await prisma.websiteTicket.findUnique({ where: { id: ctx.data.ticketId }, }); if (!ticket) throw new ActionError("Ticket not found"); if (ticket.creatorId !== ctx.session.user.id) throw new ActionError("Unauthorized"); if (ticket.status === "closed") throw new ActionError("Ticket is closed"); await prisma.websiteTicketMessage.create({ data: { ticketId: ctx.data.ticketId, userId: ctx.session.user.id, message: ctx.data.message, isStaff: 0, }, }); // If ticket was in "waiting" (waiting for user), move back to open if (ticket.status === "waiting") { await prisma.websiteTicket.update({ where: { id: ctx.data.ticketId }, data: { status: "open" }, }); } return actionOk(); }); export const closeTicketByUser = authAction( { schema: replyTicketSchema.pick({ ticketId: true }) }, async (ctx) => { const ticket = await prisma.websiteTicket.findUnique({ where: { id: ctx.data.ticketId }, }); if (!ticket) throw new ActionError("Ticket not found"); if (ticket.creatorId !== ctx.session.user.id) throw new ActionError("Unauthorized"); if (ticket.status === "closed") throw new ActionError("Ticket is already closed"); await prisma.websiteTicket.update({ where: { id: ctx.data.ticketId }, data: { status: "closed", closedAt: new Date() }, }); return actionOk(); }, ); // ── Admin actions ──────────────────────────────────────────────────── export const adminReplyTicket = adminAction( { permission: PERMS.TICKETS_EDIT, schema: replyTicketSchema }, async (ctx) => { const ticket = await prisma.websiteTicket.findUnique({ where: { id: ctx.data.ticketId }, }); if (!ticket) throw new ActionError("Ticket not found"); await prisma.websiteTicketMessage.create({ data: { ticketId: ctx.data.ticketId, userId: ctx.session.user.id, message: ctx.data.message, isStaff: 1, }, }); // Auto-assign if not assigned yet const updates: Record = { status: "waiting" }; if (!ticket.assigneeId) { updates.assigneeId = ctx.session.user.id; } await prisma.websiteTicket.update({ where: { id: ctx.data.ticketId }, data: updates, }); logAudit({ userId: ctx.session.user.id, action: "ticket_reply", target: "WebsiteTicket", targetId: ctx.data.ticketId, }); return actionOk(); }, ); export const updateTicketStatus = adminAction( { permission: PERMS.TICKETS_EDIT, schema: updateTicketStatusSchema }, async (ctx) => { const ticket = await prisma.websiteTicket.findUnique({ where: { id: ctx.data.ticketId }, }); if (!ticket) throw new ActionError("Ticket not found"); const data: Record = { status: ctx.data.status }; if (ctx.data.status === "closed") { data.closedAt = new Date(); } if (ctx.data.status === "in_progress" && !ticket.assigneeId) { data.assigneeId = ctx.session.user.id; } await prisma.websiteTicket.update({ where: { id: ctx.data.ticketId }, data, }); logAudit({ userId: ctx.session.user.id, action: "ticket_status_change", target: "WebsiteTicket", targetId: ctx.data.ticketId, before: { status: ticket.status }, after: { status: ctx.data.status }, }); return actionOk(); }, ); export const assignTicket = adminAction( { permission: PERMS.TICKETS_EDIT, schema: assignTicketSchema }, async (ctx) => { const ticket = await prisma.websiteTicket.findUnique({ where: { id: ctx.data.ticketId }, }); if (!ticket) throw new ActionError("Ticket not found"); await prisma.websiteTicket.update({ where: { id: ctx.data.ticketId }, data: { assigneeId: ctx.data.assigneeId, status: ctx.data.assigneeId ? "in_progress" : "open", }, }); logAudit({ userId: ctx.session.user.id, action: "ticket_assign", target: "WebsiteTicket", targetId: ctx.data.ticketId, before: { assigneeId: ticket.assigneeId }, after: { assigneeId: ctx.data.assigneeId }, }); return actionOk(); }, ); export const updateTicketPriority = adminAction( { permission: PERMS.TICKETS_EDIT, schema: updateTicketPrioritySchema }, async (ctx) => { const ticket = await prisma.websiteTicket.findUnique({ where: { id: ctx.data.ticketId }, }); if (!ticket) throw new ActionError("Ticket not found"); await prisma.websiteTicket.update({ where: { id: ctx.data.ticketId }, data: { priority: ctx.data.priority }, }); logAudit({ userId: ctx.session.user.id, action: "ticket_priority_change", target: "WebsiteTicket", targetId: ctx.data.ticketId, before: { priority: ticket.priority }, after: { priority: ctx.data.priority }, }); return actionOk(); }, );