fix(housekeeping): address people moderation review
CI / check (pull_request) Failing after 9s
CI / release (pull_request) Skipped
CI / deploy (pull_request) Skipped

This commit is contained in:
Simo committed 2026-08-29 23:53:38 +02:00
1 parent 29fe22297b
commit 3fa1119f5a
20 files changed
+2000 -777

No files matched your search

+12 -1
View File
@@ -10,10 +10,21 @@ import { createCorrelationId } from "@/features/housekeeping/foundation/contract
import { PERMS } from "@/lib/permissions";
import { adminAction } from "@/lib/safe-action";
import { ActionError, actionOk } from "@/lib/safe-action-shared";
import { canonicalTicketId } from "@/lib/services/ticket-replies";
const ticketIdField = z
.union([z.string(), z.number(), z.bigint()])
.transform((v) => BigInt(String(v)));
.transform((value, context) => {
try {
return canonicalTicketId(value);
} catch {
context.addIssue({
code: "custom",
message: "Invalid ticket identifier",
});
return z.NEVER;
}
});
const replyHelpCenterTicketSchema = z.object({
ticketId: ticketIdField,
+29 -30
View File
@@ -29,6 +29,17 @@ async function execute(
);
}
async function executeLegacyModerationAction(
staff: { readonly id: number },
input: unknown,
) {
const result = await execute(staff, "moderation.action", input);
if (!result.ok) {
throw new Error("Could not execute moderation action");
}
return actionOk();
}
export const assignCfhTicket = adminAction(
{ permission: CFH_PERM, schema: cfhIdSchema },
async (ctx) => {
@@ -85,13 +96,11 @@ const userIdSchema = z.object({ userId: z.coerce.number().int().positive() });
export const quickKick = adminAction(
{ permission: MOD_ACTION_PERM, schema: userIdSchema },
async (ctx) => {
await execute(ctx.session.user, "moderation.action", {
(ctx) =>
executeLegacyModerationAction(ctx.session.user, {
action: "kick",
userId: ctx.data.userId,
});
return actionOk();
},
}),
);
const muteSchema = z.object({
@@ -101,24 +110,20 @@ const muteSchema = z.object({
export const quickMute = adminAction(
{ permission: MOD_ACTION_PERM, schema: muteSchema },
async (ctx) => {
await execute(ctx.session.user, "moderation.action", {
(ctx) =>
executeLegacyModerationAction(ctx.session.user, {
action: "mute",
...ctx.data,
});
return actionOk();
},
}),
);
export const quickUnmute = adminAction(
{ permission: MOD_ACTION_PERM, schema: userIdSchema },
async (ctx) => {
await execute(ctx.session.user, "moderation.action", {
(ctx) =>
executeLegacyModerationAction(ctx.session.user, {
action: "unmute",
userId: ctx.data.userId,
});
return actionOk();
},
}),
);
const alertSchema = z.object({
@@ -128,26 +133,22 @@ const alertSchema = z.object({
export const quickAlert = adminAction(
{ permission: MOD_ACTION_PERM, schema: alertSchema },
async (ctx) => {
await execute(ctx.session.user, "moderation.action", {
(ctx) =>
executeLegacyModerationAction(ctx.session.user, {
action: "alert",
...ctx.data,
});
return actionOk();
},
}),
);
const roomIdSchema = z.object({ roomId: z.coerce.number().int().positive() });
export const quickRoomKick = adminAction(
{ permission: MOD_ACTION_PERM, schema: roomIdSchema },
async (ctx) => {
await execute(ctx.session.user, "moderation.action", {
(ctx) =>
executeLegacyModerationAction(ctx.session.user, {
action: "room-kick",
roomId: ctx.data.roomId,
});
return actionOk();
},
}),
);
const broadcastSchema = z.object({
@@ -157,11 +158,9 @@ const broadcastSchema = z.object({
export const broadcastAlert = adminAction(
{ permission: MOD_ACTION_PERM, schema: broadcastSchema },
async (ctx) => {
await execute(ctx.session.user, "moderation.action", {
(ctx) =>
executeLegacyModerationAction(ctx.session.user, {
action: "broadcast",
...ctx.data,
});
return actionOk();
},
}),
);
@@ -8,11 +8,34 @@ const { execute, registrations, staff } = vi.hoisted(() => ({
}));
function wrapper(
options: { permission: string | readonly string[] },
options: {
permission: string | readonly string[];
schema?: {
safeParse(value: unknown):
| { success: true; data: unknown }
| { success: false; error: unknown };
};
},
handler: (context: { data: unknown; session: { user: typeof staff } }) => unknown,
) {
registrations.push(options);
return (data: unknown) => handler({ data, session: { user: staff } });
return async (data: unknown) => {
const parsed = options.schema?.safeParse(data);
if (parsed && !parsed.success) {
return { ok: false, error: "Validation failed" };
}
try {
return await handler({
data: parsed?.data ?? data,
session: { user: staff },
});
} catch (error) {
return {
ok: false,
error: error instanceof Error ? error.message : "Internal server error",
};
}
};
}
vi.mock("@/features/housekeeping/domains/people/services/mutations", () => ({
@@ -202,6 +225,64 @@ describe("legacy People support and moderation wrappers", () => {
);
});
it.each([
["kick", quickKick, { userId: 7 }],
["mute", quickMute, { userId: 7, duration: 60 }],
["unmute", quickUnmute, { userId: 7 }],
["alert", quickAlert, { userId: 7, message: "Stop" }],
["room kick", quickRoomKick, { roomId: 12 }],
["broadcast", broadcastAlert, { message: "Notice", type: "staff" }],
] as const)(
"maps a non-ok %s service result to the historical legacy failure boundary",
async (_label, action, input) => {
execute.mockResolvedValueOnce({
ok: false,
error: {
code: "DEPENDENCY_UNAVAILABLE",
messageKey: "errors.housekeeping.dependencyUnavailable",
},
correlationId: "quick-action-failure",
});
await expect(call(action, input)).resolves.toEqual({
ok: false,
error: "Could not execute moderation action",
});
},
);
it("maps a thrown moderation service failure instead of reporting success", async () => {
execute.mockRejectedValueOnce(new Error("RCON unavailable"));
await expect(call(quickKick, { userId: 7 })).resolves.toEqual({
ok: false,
error: "RCON unavailable",
});
});
it.each([
9_007_199_254_740_992,
"01",
"0",
0,
-1,
"18446744073709551616",
] as const)(
"rejects noncanonical help-ticket identifier %s at every legacy action schema",
async (ticketId) => {
for (const [action, input] of [
[replyHelpCenterTicket, { ticketId, content: "Handled" }],
[closeHelpCenterTicket, { ticketId }],
[reopenHelpCenterTicket, { ticketId }],
[liftBanFromHelpTicket, { ticketId }],
] as const) {
await expect(call(action, input)).resolves.toEqual({
ok: false,
error: "Validation failed",
});
}
expect(execute).not.toHaveBeenCalled();
},
);
it("keeps close-CFH missing rows as a successful legacy no-op", async () => {
execute.mockResolvedValueOnce({
ok: false,