fix: restore complete admin feature dependencies

This commit is contained in:
Simo committed 2026-07-11 21:15:54 +02:00
1 parent 5b4228261a
commit 0cd753c735
115 files changed
+18565 -4930

No files matched your search

+230
View File
@@ -0,0 +1,230 @@
'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<string, unknown> = { 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<string, unknown> = { 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()
},
)