fix(housekeeping): unify moderation target authority
This commit is contained in:
1 parent
9b91880e33
commit
8a894617e7
12 files changed
+873
-236
No files matched your search
@@ -1,7 +1,7 @@
|
||||
import "server-only";
|
||||
|
||||
import { eq } from "drizzle-orm";
|
||||
import { Ban, type Db, SupportTickets, User } from "@/lib/db";
|
||||
import { Ban, type Db, SupportTickets } from "@/lib/db";
|
||||
import type { AuditEntry, HousekeepingAuditWriter } from "@/lib/services/audit";
|
||||
import type {
|
||||
ModerationAction,
|
||||
@@ -62,6 +62,25 @@ export interface PeopleModerationMutationDependencies {
|
||||
required?: boolean,
|
||||
) => string;
|
||||
readonly requireRcon: (result: boolean) => Promise<void>;
|
||||
readonly loadTarget: (
|
||||
userId: number,
|
||||
context: PeopleModerationMutationContext,
|
||||
guardHierarchy: boolean,
|
||||
source?: Pick<Db, "select">,
|
||||
lock?: boolean,
|
||||
) => Promise<{
|
||||
readonly id: number;
|
||||
readonly username: string;
|
||||
readonly rank: number;
|
||||
readonly ipCurrent: string;
|
||||
readonly machineId: string;
|
||||
readonly online: string;
|
||||
}>;
|
||||
readonly runWithLockedTargetAuthority: (
|
||||
userId: number,
|
||||
context: PeopleModerationMutationContext,
|
||||
execute: () => Promise<void>,
|
||||
) => Promise<void>;
|
||||
readonly transport: ModerationTransport;
|
||||
readonly executeModerationAction: (
|
||||
transport: ModerationActionTransport,
|
||||
@@ -124,6 +143,8 @@ export function createPeopleModerationMutationExecutor(
|
||||
nonNegativeInteger,
|
||||
normalizedText,
|
||||
requireRcon,
|
||||
loadTarget,
|
||||
runWithLockedTargetAuthority,
|
||||
transport,
|
||||
executeModerationAction,
|
||||
logStaffActivity,
|
||||
@@ -194,6 +215,35 @@ export function createPeopleModerationMutationExecutor(
|
||||
before: null,
|
||||
after: { ...action },
|
||||
} satisfies PeopleModerationMutationSnapshot;
|
||||
if ("userId" in action) {
|
||||
await db.transaction(async (tx) => {
|
||||
await loadTarget(action.userId, context, true, tx, true);
|
||||
await writeAudit(
|
||||
auditEntry(
|
||||
context,
|
||||
"moderation.action",
|
||||
"User",
|
||||
action.userId,
|
||||
snapshot,
|
||||
"intent",
|
||||
),
|
||||
tx,
|
||||
);
|
||||
});
|
||||
return finalizeExternalWithAudit(
|
||||
context,
|
||||
"moderation.action",
|
||||
"User",
|
||||
action.userId,
|
||||
snapshot,
|
||||
() =>
|
||||
runWithLockedTargetAuthority(action.userId, context, () =>
|
||||
deliverModerationAction(action, context),
|
||||
),
|
||||
false,
|
||||
{ before: null, after: null },
|
||||
);
|
||||
}
|
||||
return runExternalWithAudit(
|
||||
context,
|
||||
"moderation.action",
|
||||
@@ -211,6 +261,10 @@ export function createPeopleModerationMutationExecutor(
|
||||
): Promise<PeopleModerationMutationSnapshot> {
|
||||
const data = record(input);
|
||||
const ticketId = positiveInteger(data.ticketId);
|
||||
const requestedAction = normalizedText(data.action, 32);
|
||||
if (!["kick", "mute", "alert"].includes(requestedAction)) {
|
||||
throw failure("VALIDATION", "errors.housekeeping.validation");
|
||||
}
|
||||
const action = moderationAction({
|
||||
...data,
|
||||
message: data.message ?? data.reason,
|
||||
@@ -223,13 +277,21 @@ export function createPeopleModerationMutationExecutor(
|
||||
id: SupportTickets.id,
|
||||
state: SupportTickets.state,
|
||||
modId: SupportTickets.modId,
|
||||
reportedId: SupportTickets.reportedId,
|
||||
})
|
||||
.from(SupportTickets)
|
||||
.where(eq(SupportTickets.id, ticketId))
|
||||
.limit(1);
|
||||
.for("update");
|
||||
if (!ticket) {
|
||||
throw failure("NOT_FOUND", "errors.housekeeping.notFound");
|
||||
}
|
||||
if (!("userId" in action) || action.userId !== ticket.reportedId) {
|
||||
throw failure("FORBIDDEN", "errors.housekeeping.forbidden");
|
||||
}
|
||||
if (ticket.state !== 0 && ticket.state !== 1) {
|
||||
throw failure("CONFLICT", "errors.housekeeping.conflict");
|
||||
}
|
||||
await loadTarget(ticket.reportedId, context, true, tx, true);
|
||||
snapshot = {
|
||||
before: {
|
||||
id: ticket.id,
|
||||
@@ -266,7 +328,14 @@ export function createPeopleModerationMutationExecutor(
|
||||
"support_tickets",
|
||||
ticketId,
|
||||
snapshot,
|
||||
() => deliverModerationAction(action, context),
|
||||
() => {
|
||||
if (!("userId" in action)) {
|
||||
throw failure("VALIDATION", "errors.housekeeping.validation");
|
||||
}
|
||||
return runWithLockedTargetAuthority(action.userId, context, () =>
|
||||
deliverModerationAction(action, context),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -293,16 +362,20 @@ export function createPeopleModerationMutationExecutor(
|
||||
let banId = 0;
|
||||
let snapshot!: PeopleModerationMutationSnapshot;
|
||||
await db.transaction(async (tx) => {
|
||||
const [user] = await tx
|
||||
.select({ username: User.username })
|
||||
.from(User)
|
||||
.where(eq(User.id, userId))
|
||||
.limit(1);
|
||||
username = user?.username ?? null;
|
||||
const user = await loadTarget(userId, context, true, tx, true);
|
||||
username = user.username;
|
||||
const needsIp = type === "ip" || type === "super";
|
||||
const needsMachine = type === "machine" || type === "super";
|
||||
if (
|
||||
(needsIp && user.ipCurrent.trim().length === 0) ||
|
||||
(needsMachine && user.machineId.trim().length === 0)
|
||||
) {
|
||||
throw failure("VALIDATION", "errors.housekeeping.validation");
|
||||
}
|
||||
const [result] = await tx.insert(Ban).values({
|
||||
userId,
|
||||
ip: "",
|
||||
machineId: "",
|
||||
ip: needsIp ? user.ipCurrent : "",
|
||||
machineId: needsMachine ? user.machineId : "",
|
||||
userStaffId: context.capability.actor.id,
|
||||
timestamp: now,
|
||||
banExpire,
|
||||
@@ -332,22 +405,23 @@ export function createPeopleModerationMutationExecutor(
|
||||
"Ban",
|
||||
banId,
|
||||
snapshot,
|
||||
async () => {
|
||||
let delivered = true;
|
||||
if (username !== null) {
|
||||
delivered = await transport.disconnectUser(userId, username);
|
||||
}
|
||||
await logStaffActivity({
|
||||
staffId: context.capability.actor.id,
|
||||
action: "user_ban",
|
||||
description: `Banned user #${userId} (${type}, ${
|
||||
hours > 0 ? `${hours}h` : "permanent"
|
||||
}): ${reason}`,
|
||||
targetType: "user",
|
||||
targetId: userId,
|
||||
});
|
||||
await requireRcon(delivered);
|
||||
},
|
||||
() =>
|
||||
runWithLockedTargetAuthority(userId, context, async () => {
|
||||
let delivered = true;
|
||||
if (username !== null) {
|
||||
delivered = await transport.disconnectUser(userId, username);
|
||||
}
|
||||
await logStaffActivity({
|
||||
staffId: context.capability.actor.id,
|
||||
action: "user_ban",
|
||||
description: `Banned user #${userId} (${type}, ${
|
||||
hours > 0 ? `${hours}h` : "permanent"
|
||||
}): ${reason}`,
|
||||
targetType: "user",
|
||||
targetId: userId,
|
||||
});
|
||||
await requireRcon(delivered);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user