fix(housekeeping): coordinate all rank mutation paths
This commit is contained in:
1 parent
3d53321575
commit
866f38818b
14 files changed
+1070
-317
No files matched your search
@@ -34,6 +34,11 @@ import {
|
||||
executeModerationAction,
|
||||
reloadWordFilter,
|
||||
} from "@/lib/services/moderation";
|
||||
import {
|
||||
RankAssignmentFailure,
|
||||
type RankAssignmentTransaction,
|
||||
rankAssignmentCoordinator,
|
||||
} from "@/lib/services/rank-assignment";
|
||||
import { rcon } from "@/lib/services/rcon";
|
||||
import { siteSettings } from "@/lib/services/site-settings";
|
||||
import { logStaffActivity } from "@/lib/services/staff-activity";
|
||||
@@ -438,17 +443,6 @@ async function assertBulkTargetHierarchy(
|
||||
}
|
||||
}
|
||||
|
||||
async function assertConfiguredRankExists(rank: number): Promise<void> {
|
||||
const [rows] = (await db.execute(
|
||||
sql`SELECT id FROM permission_ranks WHERE id = ${rank} LIMIT 1`,
|
||||
)) as unknown as [unknown[], unknown];
|
||||
if (!Array.isArray(rows) || rows.length === 0) {
|
||||
throw new PeopleMutationFailure("VALIDATION", "errors.validation.invalid", {
|
||||
rank: ["errors.validation.invalid"],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function targetSnapshot(target: TargetUser) {
|
||||
return {
|
||||
id: target.id,
|
||||
@@ -642,22 +636,39 @@ async function executeUserMutation(
|
||||
"errors.housekeeping.forbidden",
|
||||
);
|
||||
}
|
||||
await assertConfiguredRankExists(rank);
|
||||
}
|
||||
const userPatch = Object.fromEntries(
|
||||
["username", "mail", "rank", "motto", "credits", "pixels"].flatMap(
|
||||
(key) => (fields[key] === undefined ? [] : [[key, fields[key]]]),
|
||||
["username", "mail", "motto", "credits", "pixels"].flatMap((key) =>
|
||||
fields[key] === undefined ? [] : [[key, fields[key]]],
|
||||
),
|
||||
);
|
||||
const safeFields = Object.fromEntries(
|
||||
Object.entries(fields).filter(([key]) => key !== "mail"),
|
||||
);
|
||||
if (fields.mail !== undefined) safeFields.mailChanged = true;
|
||||
const after: Record<string, unknown> = { ...before, ...safeFields };
|
||||
const rankSyncAudit = {
|
||||
userId: context.capability.actor.id,
|
||||
action: "system.external-sync",
|
||||
target: "rcon.set-rank",
|
||||
targetId: userId,
|
||||
correlationId: context.correlationId,
|
||||
domain: "system" as const,
|
||||
after: {
|
||||
kind: "set-rank",
|
||||
operation: "rcon.set-rank",
|
||||
userId,
|
||||
rank: nextRank,
|
||||
},
|
||||
};
|
||||
if (nextRank !== undefined) after.rankRecoveryId = context.correlationId;
|
||||
const snapshot: PeopleMutationSnapshot = {
|
||||
before,
|
||||
after: { ...before, ...safeFields },
|
||||
after,
|
||||
};
|
||||
await db.transaction(async (tx) => {
|
||||
const persist = async (tx: RankAssignmentTransaction) => {
|
||||
if (nextRank !== undefined)
|
||||
await logAudit({ ...rankSyncAudit, outcome: "intent" }, tx);
|
||||
if (Object.keys(userPatch).length > 0) {
|
||||
await tx.update(User).set(userPatch).where(eq(User.id, userId));
|
||||
}
|
||||
@@ -683,8 +694,49 @@ async function executeUserMutation(
|
||||
),
|
||||
tx,
|
||||
);
|
||||
});
|
||||
return finalizeExternalWithAudit(
|
||||
};
|
||||
if (nextRank !== undefined) {
|
||||
try {
|
||||
await rankAssignmentCoordinator.commit({
|
||||
userId,
|
||||
rank: positiveInteger(nextRank),
|
||||
authorize(currentRank) {
|
||||
before.rank = currentRank;
|
||||
if (
|
||||
!context.capability.isSuperAdmin &&
|
||||
currentRank >= context.capability.actor.rank
|
||||
) {
|
||||
throw new PeopleMutationFailure(
|
||||
"FORBIDDEN",
|
||||
"errors.housekeeping.forbidden",
|
||||
);
|
||||
}
|
||||
},
|
||||
mutate: persist,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof RankAssignmentFailure) {
|
||||
if (error.code === "RANK_NOT_FOUND") {
|
||||
throw new PeopleMutationFailure(
|
||||
"VALIDATION",
|
||||
"errors.validation.invalid",
|
||||
{
|
||||
rank: ["errors.validation.invalid"],
|
||||
},
|
||||
);
|
||||
}
|
||||
throw new PeopleMutationFailure(
|
||||
"NOT_FOUND",
|
||||
"errors.housekeeping.notFound",
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
await db.transaction(persist);
|
||||
}
|
||||
let rankAuditUnavailable = false;
|
||||
const completed = await finalizeExternalWithAudit(
|
||||
context,
|
||||
operation,
|
||||
"User",
|
||||
@@ -692,6 +744,33 @@ async function executeUserMutation(
|
||||
snapshot,
|
||||
async () => {
|
||||
invalidateLoginCache(target.username);
|
||||
if (nextRank !== undefined) {
|
||||
const delivery = await rankAssignmentCoordinator.synchronize(userId);
|
||||
after.rankSynchronization = delivery.status;
|
||||
if (delivery.status !== "superseded")
|
||||
after.synchronizedRank = delivery.rank;
|
||||
try {
|
||||
await logAudit({
|
||||
...rankSyncAudit,
|
||||
after: {
|
||||
...rankSyncAudit.after,
|
||||
...(delivery.status === "superseded"
|
||||
? { superseded: true }
|
||||
: { rank: delivery.rank }),
|
||||
},
|
||||
outcome: delivery.status === "pending" ? "partial" : "success",
|
||||
});
|
||||
} catch {
|
||||
rankAuditUnavailable = true;
|
||||
after.rankCompletionAudit = "pending";
|
||||
}
|
||||
if (delivery.status === "pending") {
|
||||
throw new PeopleMutationFailure(
|
||||
"DEPENDENCY_UNAVAILABLE",
|
||||
"errors.housekeeping.dependencyUnavailable",
|
||||
);
|
||||
}
|
||||
}
|
||||
void notify({
|
||||
action: "user_edit",
|
||||
actor: context.capability.actor.username,
|
||||
@@ -700,6 +779,14 @@ async function executeUserMutation(
|
||||
});
|
||||
},
|
||||
);
|
||||
if (rankAuditUnavailable) {
|
||||
return withPartialCompletion(completed, {
|
||||
status: "partial",
|
||||
external: completed.completion?.external ?? "completed",
|
||||
audit: "unavailable",
|
||||
});
|
||||
}
|
||||
return completed;
|
||||
}
|
||||
|
||||
if (operation === "user.ban") {
|
||||
|
||||
Reference in new issue
Block a user