fix(housekeeping): unify moderation target authority

This commit is contained in:
Simo committed 2026-09-05 11:17:19 +02:00
1 parent 9b91880e33
commit 8a894617e7
12 files changed
+873 -236

No files matched your search

@@ -8,6 +8,7 @@ import { invalidateLoginCache } from "@/lib/auth";
import { hashPassword } from "@/lib/auth/password";
import {
Ban,
type Db,
db,
GuildForumViews,
Guilds,
@@ -150,6 +151,13 @@ class PeopleMutationFailure extends Error {
class ConfirmedExternalNoopFailure extends PeopleMutationFailure {}
class CompletedExternalEffectFailure extends Error {
constructor(readonly cause: unknown) {
super("External effect completed before the authority transaction failed");
this.name = "CompletedExternalEffectFailure";
}
}
function operationCapability(operation: PeopleMutationOperation) {
if (
operation === "user.ban" ||
@@ -374,6 +382,8 @@ type TargetUser = {
id: number;
username: string;
rank: number;
ipCurrent: string;
machineId: string;
motto: string;
credits: number;
pixels: number;
@@ -384,20 +394,24 @@ async function loadTarget(
userId: number,
context: PeopleMutationContext,
guardHierarchy: boolean,
source: Pick<Db, "select"> = db,
lock = false,
): Promise<TargetUser> {
const [target] = await db
const query = source
.select({
id: User.id,
username: User.username,
rank: User.rank,
ipCurrent: User.ipCurrent,
machineId: User.machineId,
motto: User.motto,
credits: User.credits,
pixels: User.pixels,
online: User.online,
})
.from(User)
.where(eq(User.id, userId))
.limit(1);
.where(eq(User.id, userId));
const [target] = lock ? await query.for("update") : await query.limit(1);
if (!target) {
throw new PeopleMutationFailure(
"NOT_FOUND",
@@ -417,6 +431,24 @@ async function loadTarget(
return target;
}
async function runWithLockedTargetAuthority(
userId: number,
context: PeopleMutationContext,
execute: () => Promise<void>,
): Promise<void> {
let externalCompleted = false;
try {
await db.transaction(async (tx) => {
await loadTarget(userId, context, true, tx, true);
await execute();
externalCompleted = true;
});
} catch (error) {
if (externalCompleted) throw new CompletedExternalEffectFailure(error);
throw error;
}
}
async function assertBulkTargetHierarchy(
userIds: readonly number[],
context: PeopleMutationContext,
@@ -489,6 +521,28 @@ async function finalizeExternalWithAudit(
try {
await execute();
} catch (error) {
if (error instanceof CompletedExternalEffectFailure) {
let audit: HousekeepingPartialCompletion["audit"] = "persisted";
try {
await logAudit(
canonicalAuditEntry(
context,
operation,
target,
targetId,
snapshot,
"partial",
),
);
} catch {
audit = "unavailable";
}
return withPartialCompletion(snapshot, {
status: "partial",
external: "completed",
audit,
});
}
const failureSnapshot = mutationCommitted
? snapshot
: error instanceof ConfirmedExternalNoopFailure
@@ -607,20 +661,41 @@ async function executeUserMutation(
before: null,
after: { userId, alertDelivered: true },
};
return runExternalWithAudit(
await db.transaction(async (tx) => {
await loadTarget(userId, context, true, tx, true);
await logAudit(
canonicalAuditEntry(
context,
operation,
"User",
userId,
snapshot,
"intent",
),
tx,
);
});
return finalizeExternalWithAudit(
context,
operation,
"User",
userId,
snapshot,
async () => requireRcon(await rcon.alertUser(userId, message)),
() =>
runWithLockedTargetAuthority(userId, context, async () => {
await requireRcon(await rcon.alertUser(userId, message));
}),
false,
{ before: null, after: { userId, alertDelivered: false } },
);
}
const guardHierarchy = operation !== "user.trade-lock";
const target = await loadTarget(userId, context, guardHierarchy);
const before = targetSnapshot(target);
let target!: TargetUser;
let before!: ReturnType<typeof targetSnapshot>;
if (operation !== "user.trade-lock") {
target = await loadTarget(userId, context, true);
before = targetSnapshot(target);
}
if (operation === "user.update") {
const fields = record(data.fields);
@@ -1093,6 +1168,8 @@ async function executeUserMutation(
const locked = untilUnix > 0;
let snapshot!: PeopleMutationSnapshot;
await db.transaction(async (tx) => {
target = await loadTarget(userId, context, true, tx, true);
before = targetSnapshot(target);
const [existing] = await tx
.select({
id: Sanctions.id,
@@ -1169,29 +1246,30 @@ async function executeUserMutation(
"User",
userId,
snapshot,
async () => {
await requireRcon(await rcon.setTradeLock(userId, locked));
await requireRcon(
await rcon.alertUser(
userId,
locked
? "Trading has been disabled by staff."
: "Trading has been re-enabled by staff.",
),
);
if (target.online === "1") {
await requireRcon(await rcon.disconnectUser(userId, target.username));
}
await logStaffActivity({
staffId: context.capability.actor.id,
action: locked ? "trade_lock" : "trade_unlock",
description: locked
? `Trade-locked ${target.username} (#${userId}) until ${untilUnix}`
: `Cleared trade lock for ${target.username} (#${userId})`,
targetType: "user",
targetId: userId,
});
},
() =>
runWithLockedTargetAuthority(userId, context, async () => {
await requireRcon(await rcon.setTradeLock(userId, locked));
await requireRcon(
await rcon.alertUser(
userId,
locked
? "Trading has been disabled by staff."
: "Trading has been re-enabled by staff.",
),
);
if (target.online === "1") {
await requireRcon(await rcon.disconnectUser(userId, target.username));
}
await logStaffActivity({
staffId: context.capability.actor.id,
action: locked ? "trade_lock" : "trade_unlock",
description: locked
? `Trade-locked ${target.username} (#${userId}) until ${untilUnix}`
: `Cleared trade lock for ${target.username} (#${userId})`,
targetType: "user",
targetId: userId,
});
}),
);
}
async function executeBulkMutation(
@@ -1956,6 +2034,8 @@ const moderationMutationExecutor = createPeopleModerationMutationExecutor({
nonNegativeInteger,
normalizedText,
requireRcon,
loadTarget,
runWithLockedTargetAuthority,
transport: rcon,
executeModerationAction,
logStaffActivity,