feat: add admin operations suite
Local Build and Deploy / deploy (push) Successful in 50s

This commit is contained in:
Simo committed 2026-07-12 20:11:28 +02:00
1 parent 21a61068fb
commit 60278b9e71
38 files changed
+3452

No files matched your search

+206
View File
@@ -0,0 +1,206 @@
'use server'
import { z } from 'zod'
import { PERMS } from '@/lib/permissions'
import { prisma } from '@/lib/prisma'
import { adminAction } from '@/lib/safe-action'
import { ActionError, actionOk } from '@/lib/safe-action-shared'
import { logAudit } from '@/lib/services/audit'
import { rcon } from '@/lib/services/rcon'
// ── CFH Ticket Actions ──────────────────────────────────────────────
const cfhIdSchema = z.object({ ticketId: z.coerce.number().int().positive() })
export const assignCfhTicket = adminAction(
{ permission: PERMS.MODERATION_EDIT, schema: cfhIdSchema },
async (ctx) => {
const ticket = await prisma.supportTickets.findUnique({ where: { id: ctx.data.ticketId } })
if (!ticket) throw new ActionError('Ticket not found')
await prisma.supportTickets.update({
where: { id: ctx.data.ticketId },
data: { modId: ctx.session.user.id, state: 1 },
})
logAudit({
userId: ctx.session.user.id,
action: 'cfh_assign',
target: 'support_tickets',
targetId: ctx.data.ticketId,
})
return actionOk()
},
)
const cfhStateSchema = z.object({
ticketId: z.coerce.number().int().positive(),
state: z.coerce.number().int().min(0).max(3),
})
export const updateCfhState = adminAction(
{ permission: PERMS.MODERATION_EDIT, schema: cfhStateSchema },
async (ctx) => {
const ticket = await prisma.supportTickets.findUnique({ where: { id: ctx.data.ticketId } })
if (!ticket) throw new ActionError('Ticket not found')
await prisma.supportTickets.update({
where: { id: ctx.data.ticketId },
data: { state: ctx.data.state, modId: ctx.session.user.id },
})
logAudit({
userId: ctx.session.user.id,
action: 'cfh_state_change',
target: 'support_tickets',
targetId: ctx.data.ticketId,
before: { state: ticket.state },
after: { state: ctx.data.state },
})
return actionOk()
},
)
export const closeCfhTicket = adminAction(
{ permission: PERMS.MODERATION_EDIT, schema: cfhIdSchema },
async (ctx) => {
await prisma.supportTickets.update({
where: { id: ctx.data.ticketId },
data: { state: 2, modId: ctx.session.user.id },
})
logAudit({
userId: ctx.session.user.id,
action: 'cfh_close',
target: 'support_tickets',
targetId: ctx.data.ticketId,
})
return actionOk()
},
)
// ── Quick Mod Actions ────────────────────────────────────────────────
const userIdSchema = z.object({ userId: z.coerce.number().int().positive() })
export const quickKick = adminAction(
{ permission: PERMS.MODERATION_EDIT, schema: userIdSchema },
async (ctx) => {
await rcon.disconnectUser(ctx.data.userId)
logAudit({
userId: ctx.session.user.id,
action: 'mod_kick',
target: 'User',
targetId: ctx.data.userId,
})
return actionOk()
},
)
const muteSchema = z.object({
userId: z.coerce.number().int().positive(),
duration: z.coerce.number().int().min(0).max(525600), // max 1 year in minutes
})
export const quickMute = adminAction(
{ permission: PERMS.MODERATION_EDIT, schema: muteSchema },
async (ctx) => {
await rcon.muteUser(ctx.data.userId, ctx.data.duration)
logAudit({
userId: ctx.session.user.id,
action: 'mod_mute',
target: 'User',
targetId: ctx.data.userId,
after: { duration: ctx.data.duration },
})
return actionOk()
},
)
export const quickUnmute = adminAction(
{ permission: PERMS.MODERATION_EDIT, schema: userIdSchema },
async (ctx) => {
await rcon.unmuteUser(ctx.data.userId)
logAudit({
userId: ctx.session.user.id,
action: 'mod_unmute',
target: 'User',
targetId: ctx.data.userId,
})
return actionOk()
},
)
const alertSchema = z.object({
userId: z.coerce.number().int().positive(),
message: z.string().min(1).max(500),
})
export const quickAlert = adminAction(
{ permission: PERMS.MODERATION_EDIT, schema: alertSchema },
async (ctx) => {
await rcon.alertUser(ctx.data.userId, ctx.data.message)
logAudit({
userId: ctx.session.user.id,
action: 'mod_alert',
target: 'User',
targetId: ctx.data.userId,
after: { message: ctx.data.message },
})
return actionOk()
},
)
const roomIdSchema = z.object({ roomId: z.coerce.number().int().positive() })
export const quickRoomKick = adminAction(
{ permission: PERMS.MODERATION_EDIT, schema: roomIdSchema },
async (ctx) => {
await rcon.kickAll(ctx.data.roomId)
logAudit({
userId: ctx.session.user.id,
action: 'mod_room_kick',
target: 'Room',
targetId: ctx.data.roomId,
})
return actionOk()
},
)
const broadcastSchema = z.object({
message: z.string().min(1).max(500),
type: z.enum(['hotel', 'staff']),
})
export const broadcastAlert = adminAction(
{ permission: PERMS.MODERATION_EDIT, schema: broadcastSchema },
async (ctx) => {
if (ctx.data.type === 'hotel') {
await rcon.hotelAlert(ctx.data.message)
} else {
await rcon.staffAlert(ctx.data.message)
}
logAudit({
userId: ctx.session.user.id,
action: `mod_broadcast_${ctx.data.type}`,
target: 'broadcast',
after: { message: ctx.data.message },
})
return actionOk()
},
)