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