feat(housekeeping): complete people moderation parity

This commit is contained in:
Simo committed 2026-08-29 22:38:50 +02:00
1 parent 3d385d1869
commit 29fe22297b
41 files changed
+3465 -580

No files matched your search

+57 -111
View File
@@ -1,13 +1,14 @@
"use server";
import { eq } from "drizzle-orm";
import { z } from "zod";
import { db, SupportTickets } from "@/lib/db";
import {
createPeopleMutationInvocation,
peopleMutationService,
} from "@/features/housekeeping/domains/people/services/mutations";
import { createCorrelationId } from "@/features/housekeeping/foundation/contracts";
import { actionOk, adminAction } from "@/lib/foundation/action";
import { NotFoundError } from "@/lib/foundation/errors";
import { PERMS } from "@/lib/permissions";
import { logAudit } from "@/lib/services/audit";
import { rcon } from "@/lib/services/rcon";
// ── CFH Ticket Actions ──────────────────────────────────────────────
@@ -16,28 +17,31 @@ const cfhIdSchema = z.object({ ticketId: z.coerce.number().int().positive() });
const CFH_PERM = [PERMS.MODERATION_EDIT, PERMS.MOD_CFH_EDIT] as const;
const MOD_ACTION_PERM = [PERMS.MODERATION_EDIT, PERMS.MOD_ACTIONS] as const;
async function execute(
staff: { readonly id: number },
operation: "cfh.resolve" | "moderation.action",
input: unknown,
) {
return peopleMutationService.execute(
createPeopleMutationInvocation(staff, createCorrelationId()),
operation,
input,
);
}
export const assignCfhTicket = adminAction(
{ permission: CFH_PERM, schema: cfhIdSchema },
async (ctx) => {
const [ticket] = await db
.select({ id: SupportTickets.id })
.from(SupportTickets)
.where(eq(SupportTickets.id, ctx.data.ticketId))
.limit(1);
if (!ticket) throw new NotFoundError("SupportTicket", ctx.data.ticketId);
await db
.update(SupportTickets)
.set({ modId: ctx.session.user.id, state: 1 })
.where(eq(SupportTickets.id, ctx.data.ticketId));
logAudit({
userId: ctx.session.user.id,
action: "cfh_assign",
target: "support_tickets",
targetId: ctx.data.ticketId,
const result = await execute(ctx.session.user, "cfh.resolve", {
ticketId: ctx.data.ticketId,
state: 1,
});
if (!result.ok) {
if (result.error.code === "NOT_FOUND") {
throw new NotFoundError("SupportTicket", ctx.data.ticketId);
}
throw new Error("Could not assign support ticket");
}
return actionOk();
},
);
@@ -50,30 +54,13 @@ const cfhStateSchema = z.object({
export const updateCfhState = adminAction(
{ permission: CFH_PERM, schema: cfhStateSchema },
async (ctx) => {
const [ticket] = await db
.select({
id: SupportTickets.id,
state: SupportTickets.state,
})
.from(SupportTickets)
.where(eq(SupportTickets.id, ctx.data.ticketId))
.limit(1);
if (!ticket) throw new NotFoundError("SupportTicket", ctx.data.ticketId);
await db
.update(SupportTickets)
.set({ state: ctx.data.state, modId: ctx.session.user.id })
.where(eq(SupportTickets.id, ctx.data.ticketId));
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 },
});
const result = await execute(ctx.session.user, "cfh.resolve", ctx.data);
if (!result.ok) {
if (result.error.code === "NOT_FOUND") {
throw new NotFoundError("SupportTicket", ctx.data.ticketId);
}
throw new Error("Could not update support ticket");
}
return actionOk();
},
);
@@ -81,18 +68,13 @@ export const updateCfhState = adminAction(
export const closeCfhTicket = adminAction(
{ permission: CFH_PERM, schema: cfhIdSchema },
async (ctx) => {
await db
.update(SupportTickets)
.set({ state: 2, modId: ctx.session.user.id })
.where(eq(SupportTickets.id, ctx.data.ticketId));
logAudit({
userId: ctx.session.user.id,
action: "cfh_close",
target: "support_tickets",
targetId: ctx.data.ticketId,
const result = await execute(ctx.session.user, "cfh.resolve", {
ticketId: ctx.data.ticketId,
state: 2,
});
if (!result.ok && result.error.code !== "NOT_FOUND") {
throw new Error("Could not close support ticket");
}
return actionOk();
},
);
@@ -104,15 +86,10 @@ const userIdSchema = z.object({ userId: z.coerce.number().int().positive() });
export const quickKick = adminAction(
{ permission: MOD_ACTION_PERM, 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,
await execute(ctx.session.user, "moderation.action", {
action: "kick",
userId: ctx.data.userId,
});
return actionOk();
},
);
@@ -125,16 +102,10 @@ const muteSchema = z.object({
export const quickMute = adminAction(
{ permission: MOD_ACTION_PERM, 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 },
await execute(ctx.session.user, "moderation.action", {
action: "mute",
...ctx.data,
});
return actionOk();
},
);
@@ -142,15 +113,10 @@ export const quickMute = adminAction(
export const quickUnmute = adminAction(
{ permission: MOD_ACTION_PERM, 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,
await execute(ctx.session.user, "moderation.action", {
action: "unmute",
userId: ctx.data.userId,
});
return actionOk();
},
);
@@ -163,16 +129,10 @@ const alertSchema = z.object({
export const quickAlert = adminAction(
{ permission: MOD_ACTION_PERM, 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 },
await execute(ctx.session.user, "moderation.action", {
action: "alert",
...ctx.data,
});
return actionOk();
},
);
@@ -182,15 +142,10 @@ const roomIdSchema = z.object({ roomId: z.coerce.number().int().positive() });
export const quickRoomKick = adminAction(
{ permission: MOD_ACTION_PERM, 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,
await execute(ctx.session.user, "moderation.action", {
action: "room-kick",
roomId: ctx.data.roomId,
});
return actionOk();
},
);
@@ -203,19 +158,10 @@ const broadcastSchema = z.object({
export const broadcastAlert = adminAction(
{ permission: MOD_ACTION_PERM, 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 },
await execute(ctx.session.user, "moderation.action", {
action: "broadcast",
...ctx.data,
});
return actionOk();
},
);