feat(housekeeping): deliver people account workflows

This commit is contained in:
Simo committed 2026-08-29 11:45:50 +02:00
1 parent 7920d4f46c
commit e1b31ff773
45 files changed
+4290 -1243

No files matched your search

@@ -0,0 +1,995 @@
import "server-only";
import crypto from "node:crypto";
import { and, eq, inArray, max, sql } from "drizzle-orm";
import type { ResultSetHeader } from "mysql2";
import { invalidateLoginCache } from "@/lib/auth";
import { hashPassword } from "@/lib/auth/password";
import {
Ban,
db,
GuildForumViews,
Guilds,
GuildsForumsComments,
GuildsForumsThreads,
GuildsMembers,
Items,
Rooms,
Sanctions,
User,
UsersBadges,
UsersCurrency,
UsersSettings,
WebsiteIpBlacklist,
WebsiteIpWhitelist,
WebsiteSetting,
WebsiteStaffApplications,
WebsiteTeams,
WebsiteWordfilter,
} from "@/lib/db";
import { PERMS } from "@/lib/permission-slugs";
import { logAudit } from "@/lib/services/audit";
import { reloadWordFilter } from "@/lib/services/moderation";
import { rcon } from "@/lib/services/rcon";
import { siteSettings } from "@/lib/services/site-settings";
import { notify } from "@/lib/services/webhook";
import { satisfiesCapability } from "../../../foundation/capability-context";
import {
anyCapability,
fail,
type HousekeepingCapabilityContext,
type HousekeepingErrorCode,
type HousekeepingResult,
ok,
} from "../../../foundation/contracts";
export type PeopleMutationOperation =
| "user.update"
| "user.ban"
| "user.unban"
| "user.alert"
| "user.disconnect"
| "user.mute"
| "user.unmute"
| "user.reset-password"
| "user.send-currency"
| "user.trade-lock"
| "users.bulk-ban"
| "users.bulk-unban"
| "users.bulk-currency"
| "users.bulk-badge"
| "guild.disband"
| "application.decide"
| "team.change"
| "ip.action"
| "vpn.configure"
| "word-filter.update";
export interface PeopleMutationSnapshot {
readonly before: Readonly<Record<string, unknown>> | null;
readonly after: Readonly<Record<string, unknown>> | null;
readonly output?: Readonly<Record<string, unknown>>;
}
export interface PeopleMutationContext {
readonly capability: HousekeepingCapabilityContext;
readonly correlationId: string;
}
export interface PeopleMutationAdapter {
execute(
operation: PeopleMutationOperation,
input: unknown,
context: PeopleMutationContext,
): Promise<PeopleMutationSnapshot>;
}
export interface PeopleMutationService {
execute(
context: PeopleMutationContext,
operation: PeopleMutationOperation,
input: unknown,
): Promise<HousekeepingResult<PeopleMutationSnapshot>>;
}
class PeopleMutationFailure extends Error {
constructor(
readonly code: HousekeepingErrorCode,
readonly messageKey: string,
readonly fieldErrors?: Readonly<Record<string, readonly string[]>>,
) {
super(messageKey);
this.name = "PeopleMutationFailure";
}
}
function operationCapability(operation: PeopleMutationOperation) {
if (operation === "user.ban" || operation === "user.unban") {
return anyCapability(PERMS.USERS_BAN);
}
if (operation === "user.reset-password") {
return anyCapability(PERMS.USERS_RESET_PASSWORD);
}
if (operation === "ip.action" || operation === "vpn.configure") {
return anyCapability(PERMS.SETTINGS_EDIT);
}
if (operation === "word-filter.update") {
return anyCapability(PERMS.WORDFILTER_EDIT);
}
return anyCapability(PERMS.USERS_EDIT);
}
export function createPeopleMutationService(
adapter: PeopleMutationAdapter,
): PeopleMutationService {
return {
async execute(context, operation, input) {
if (
!satisfiesCapability(context.capability, operationCapability(operation))
) {
return fail(
"FORBIDDEN",
"errors.housekeeping.forbidden",
context.correlationId,
);
}
try {
return ok(
await adapter.execute(operation, input, context),
context.correlationId,
);
} catch (error) {
if (error instanceof PeopleMutationFailure) {
return fail(
error.code,
error.messageKey,
context.correlationId,
error.fieldErrors,
);
}
return fail(
"DEPENDENCY_UNAVAILABLE",
"errors.housekeeping.dependencyUnavailable",
context.correlationId,
);
}
},
};
}
export function createLegacyPeopleMutationContext(
staff: {
readonly id: number;
readonly username: string;
readonly rank: number;
},
permission: string,
correlationId: string,
): PeopleMutationContext {
return {
correlationId,
capability: {
actor: { id: staff.id, username: staff.username, rank: staff.rank },
isSuperAdmin: false,
has: (slug) => slug === permission,
hasAny: (...slugs) => slugs.includes(permission),
hasAll: (...slugs) => slugs.every((slug) => slug === permission),
},
};
}
function record(input: unknown): Record<string, unknown> {
if (typeof input !== "object" || input === null || Array.isArray(input)) {
throw new PeopleMutationFailure(
"VALIDATION",
"errors.housekeeping.validation",
);
}
return input as Record<string, unknown>;
}
function positiveInteger(value: unknown): number {
const parsed = Number(value);
if (!Number.isSafeInteger(parsed) || parsed <= 0) {
throw new PeopleMutationFailure(
"VALIDATION",
"errors.housekeeping.validation",
);
}
return parsed;
}
function nonNegativeInteger(value: unknown): number {
const parsed = Number(value);
if (!Number.isSafeInteger(parsed) || parsed < 0) {
throw new PeopleMutationFailure(
"VALIDATION",
"errors.housekeeping.validation",
);
}
return parsed;
}
function normalizedText(value: unknown, max: number, required = true): string {
const text = String(value ?? "")
.normalize("NFC")
.trim()
.slice(0, max);
if (required && text.length === 0) {
throw new PeopleMutationFailure(
"VALIDATION",
"errors.housekeeping.validation",
);
}
return text;
}
function userIds(value: unknown): number[] {
if (!Array.isArray(value) || value.length === 0 || value.length > 100) {
throw new PeopleMutationFailure(
"VALIDATION",
"errors.housekeeping.validation",
);
}
return [...new Set(value.map(positiveInteger))];
}
async function requireRcon(result: boolean): Promise<void> {
if (!result) {
throw new PeopleMutationFailure(
"DEPENDENCY_UNAVAILABLE",
"errors.housekeeping.dependencyUnavailable",
);
}
}
type TargetUser = {
id: number;
username: string;
rank: number;
motto: string;
credits: number;
pixels: number;
online: string;
};
async function loadTarget(
userId: number,
context: PeopleMutationContext,
guardHierarchy: boolean,
): Promise<TargetUser> {
const [target] = await db
.select({
id: User.id,
username: User.username,
rank: User.rank,
motto: User.motto,
credits: User.credits,
pixels: User.pixels,
online: User.online,
})
.from(User)
.where(eq(User.id, userId))
.limit(1);
if (!target) {
throw new PeopleMutationFailure(
"NOT_FOUND",
"errors.housekeeping.notFound",
);
}
if (
guardHierarchy &&
target.rank >= context.capability.actor.rank &&
context.capability.actor.rank < 7
) {
throw new PeopleMutationFailure(
"FORBIDDEN",
"errors.housekeeping.forbidden",
);
}
return target;
}
function targetSnapshot(target: TargetUser) {
return {
id: target.id,
username: target.username,
rank: target.rank,
motto: target.motto,
credits: target.credits,
pixels: target.pixels,
online: target.online === "1",
};
}
async function auditMutation(
context: PeopleMutationContext,
operation: PeopleMutationOperation,
target: string,
targetId: number | undefined,
snapshot: PeopleMutationSnapshot,
): Promise<void> {
await logAudit({
userId: context.capability.actor.id,
action: `people.${operation}`,
target,
targetId,
before: snapshot.before ?? undefined,
after: snapshot.after ?? undefined,
correlationId: context.correlationId,
outcome: "success",
domain: "people",
});
}
async function executeUserMutation(
operation: Extract<PeopleMutationOperation, `user.${string}`>,
input: unknown,
context: PeopleMutationContext,
): Promise<PeopleMutationSnapshot> {
const data = record(input);
const userId = positiveInteger(data.userId);
const guardHierarchy = operation !== "user.alert";
const target = await loadTarget(userId, context, guardHierarchy);
const before = targetSnapshot(target);
let snapshot: PeopleMutationSnapshot;
if (operation === "user.update") {
const fields = record(data.fields);
const nextRank = fields.rank;
if (
nextRank !== undefined &&
positiveInteger(nextRank) >= context.capability.actor.rank &&
context.capability.actor.rank < 7
) {
throw new PeopleMutationFailure(
"FORBIDDEN",
"errors.housekeeping.forbidden",
);
}
const userPatch = Object.fromEntries(
["username", "mail", "rank", "motto", "credits", "pixels"].flatMap(
(key) => (fields[key] === undefined ? [] : [[key, fields[key]]]),
),
);
if (Object.keys(userPatch).length > 0) {
await db.update(User).set(userPatch).where(eq(User.id, userId));
}
for (const [key, type] of [
["duckets", 0],
["diamonds", 5],
] as const) {
if (fields[key] === undefined) continue;
const amount = nonNegativeInteger(fields[key]);
await db
.insert(UsersCurrency)
.values({ userId, type, amount })
.onDuplicateKeyUpdate({ set: { amount } });
}
invalidateLoginCache(target.username);
snapshot = { before, after: { ...before, ...fields } };
await notify({
action: "user_edit",
actor: context.capability.actor.username,
target: target.username,
targetId: userId,
});
} else if (operation === "user.ban") {
const reason = normalizedText(data.reason, 500);
const duration = nonNegativeInteger(data.duration);
const typeValue = normalizedText(data.type || "account", 32);
if (
typeValue !== "account" &&
typeValue !== "ip" &&
typeValue !== "machine"
) {
throw new PeopleMutationFailure(
"VALIDATION",
"errors.housekeeping.validation",
);
}
const type: "account" | "ip" | "machine" = typeValue;
const now = Math.floor(Date.now() / 1000);
const banExpire = duration > 0 ? now + duration * 3600 : 0;
await db.insert(Ban).values({
userId,
userStaffId: context.capability.actor.id,
timestamp: now,
banExpire,
banReason: reason,
type,
ip: normalizedText(data.ip, 255, false),
machineId: "",
});
await rcon.disconnectUser(userId);
snapshot = {
before: { ...before, banned: false },
after: { ...before, banned: true, banExpire, type, reason },
};
await notify({
action: "ban",
actor: context.capability.actor.username,
target: target.username,
details: reason,
});
} else if (operation === "user.unban") {
await db.delete(Ban).where(eq(Ban.userId, userId));
snapshot = {
before: { ...before, banned: true },
after: { ...before, banned: false },
};
await notify({
action: "unban",
actor: context.capability.actor.username,
target: target.username,
});
} else if (operation === "user.alert") {
const message = normalizedText(data.message, 500);
await requireRcon(await rcon.alertUser(userId, message));
snapshot = { before, after: { ...before, alertDelivered: true } };
} else if (operation === "user.disconnect") {
await requireRcon(await rcon.disconnectUser(userId));
snapshot = { before, after: { ...before, online: false } };
await notify({
action: "disconnect",
actor: context.capability.actor.username,
target: target.username,
});
} else if (operation === "user.mute") {
const duration = nonNegativeInteger(data.duration);
await requireRcon(await rcon.muteUser(userId, duration));
snapshot = { before, after: { ...before, muted: true, duration } };
} else if (operation === "user.unmute") {
await requireRcon(await rcon.unmuteUser(userId));
snapshot = { before, after: { ...before, muted: false } };
} else if (operation === "user.reset-password") {
const newPassword = crypto
.randomBytes(12)
.toString("base64url")
.slice(0, 16);
await db
.update(User)
.set({ password: await hashPassword(newPassword) })
.where(eq(User.id, userId));
invalidateLoginCache(target.username);
snapshot = {
before: { ...before, passwordReset: false },
after: { ...before, passwordReset: true },
output: { newPassword },
};
await notify({
action: "user_edit",
actor: context.capability.actor.username,
target: target.username,
details: "Password reset",
});
} else if (operation === "user.send-currency") {
const amount = positiveInteger(data.amount);
await requireRcon(await rcon.giveCredits(userId, amount));
snapshot = { before, after: { ...before, creditsSent: amount } };
} else {
const untilUnix = nonNegativeInteger(data.untilUnix);
const locked = untilUnix > 0;
await db.transaction(async (tx) => {
const [existing] = await tx
.select({ id: Sanctions.id })
.from(Sanctions)
.where(eq(Sanctions.habboId, userId))
.limit(1);
if (existing) {
await tx
.update(Sanctions)
.set({
tradeLockedUntil: untilUnix,
...(locked ? { reason: "Trade lock (CMS)" } : {}),
})
.where(eq(Sanctions.id, existing.id));
} else {
await tx.insert(Sanctions).values({
habboId: userId,
tradeLockedUntil: untilUnix,
reason: locked ? "Trade lock (CMS)" : "",
});
}
await tx
.update(UsersSettings)
.set({
canTrade: locked ? "0" : "1",
...(locked
? { tradelockAmount: sql`${UsersSettings.tradelockAmount} + 1` }
: {}),
})
.where(eq(UsersSettings.userId, userId));
});
await rcon.setTradeLock(userId, locked);
await rcon.alertUser(
userId,
locked
? "Trading has been disabled by staff."
: "Trading has been re-enabled by staff.",
);
if (target.online === "1") {
await rcon.disconnectUser(userId, target.username);
}
snapshot = {
before: { ...before, tradeLocked: !locked },
after: { ...before, tradeLocked: locked, untilUnix },
output: { userId, untilUnix },
};
}
await auditMutation(context, operation, "User", userId, snapshot);
return snapshot;
}
async function executeBulkMutation(
operation: Extract<PeopleMutationOperation, `users.${string}`>,
input: unknown,
context: PeopleMutationContext,
): Promise<PeopleMutationSnapshot> {
const data = record(input);
const ids = userIds(data.userIds);
const failures: Array<{ userId: number; reason: string }> = [];
let completed = 0;
let mutationReason: string | undefined;
if (operation === "users.bulk-unban") {
const result = await db.delete(Ban).where(inArray(Ban.userId, ids));
completed = Number(
(result as unknown as [{ affectedRows: number }])[0]?.affectedRows ?? 0,
);
} else if (operation === "users.bulk-ban") {
const reason = normalizedText(data.reason, 500);
mutationReason = reason;
const durationSeconds = nonNegativeInteger(data.duration);
const now = Math.floor(Date.now() / 1000);
for (const userId of ids) {
try {
await db.insert(Ban).values({
userId,
ip: "",
machineId: "",
userStaffId: context.capability.actor.id,
timestamp: now,
banExpire: durationSeconds > 0 ? now + durationSeconds : 0,
banReason: reason,
type: "account",
});
completed += 1;
} catch {
failures.push({ userId, reason: "Database error" });
}
}
} else if (operation === "users.bulk-currency") {
const amount = positiveInteger(data.amount);
const type = normalizedText(data.type, 16) as
| "credits"
| "pixels"
| "points";
for (const userId of ids) {
try {
if (type === "credits") {
await db
.update(User)
.set({ credits: sql`${User.credits} + ${amount}` })
.where(eq(User.id, userId));
await rcon.giveCredits(userId, amount);
} else {
const currencyType = type === "pixels" ? 0 : 101;
await db
.insert(UsersCurrency)
.values({ userId, type: currencyType, amount })
.onDuplicateKeyUpdate({
set: { amount: sql`${UsersCurrency.amount} + ${amount}` },
});
if (type === "pixels") await rcon.giveDuckets(userId, amount);
else await rcon.givePointsGotw(userId, amount);
}
completed += 1;
} catch {
failures.push({ userId, reason: "Database error" });
}
}
} else {
const badgeCode = normalizedText(data.badgeCode, 20);
for (const userId of ids) {
try {
const [existing] = await db
.select({ id: UsersBadges.id })
.from(UsersBadges)
.where(
and(
eq(UsersBadges.userId, userId),
eq(UsersBadges.badgeCode, badgeCode),
),
)
.limit(1);
if (!existing) {
const [aggregate] = await db
.select({ maxSlot: max(UsersBadges.slotId) })
.from(UsersBadges)
.where(eq(UsersBadges.userId, userId));
await db.insert(UsersBadges).values({
userId,
slotId: (aggregate?.maxSlot ?? 0) + 1,
badgeCode,
});
await rcon.giveBadge(userId, badgeCode);
}
completed += 1;
} catch {
failures.push({ userId, reason: "Database error" });
}
}
}
const snapshot = {
before: { userIds: ids },
after: {
completed,
total: ids.length,
failedIds: failures,
...(mutationReason ? { reason: mutationReason } : {}),
},
};
await auditMutation(context, operation, "User", undefined, snapshot);
return snapshot;
}
async function executeGuildDisband(
input: unknown,
context: PeopleMutationContext,
): Promise<PeopleMutationSnapshot> {
const guildId = positiveInteger(record(input).guildId);
const [guild] = await db
.select({ id: Guilds.id, name: Guilds.name, userId: Guilds.userId })
.from(Guilds)
.where(eq(Guilds.id, guildId))
.limit(1);
if (!guild) {
throw new PeopleMutationFailure(
"NOT_FOUND",
"errors.housekeeping.notFound",
);
}
await db.transaction(async (tx) => {
const threads = await tx
.select({ id: GuildsForumsThreads.id })
.from(GuildsForumsThreads)
.where(eq(GuildsForumsThreads.guildId, guildId));
const threadIds = threads.map((thread) => thread.id);
if (threadIds.length > 0) {
await tx
.delete(GuildsForumsComments)
.where(inArray(GuildsForumsComments.threadId, threadIds));
await tx
.delete(GuildsForumsThreads)
.where(eq(GuildsForumsThreads.guildId, guildId));
}
await tx
.delete(GuildForumViews)
.where(eq(GuildForumViews.guildId, guildId));
await tx.delete(GuildsMembers).where(eq(GuildsMembers.guildId, guildId));
await tx
.update(Rooms)
.set({ guildId: 0 })
.where(eq(Rooms.guildId, guildId));
await tx
.update(Items)
.set({ guildId: 0 })
.where(eq(Items.guildId, guildId));
await tx.delete(Guilds).where(eq(Guilds.id, guildId));
});
const snapshot = {
before: { id: guild.id, name: guild.name, userId: guild.userId },
after: null,
};
await auditMutation(context, "guild.disband", "Guild", guildId, snapshot);
return snapshot;
}
async function executeApplicationDecision(
input: unknown,
context: PeopleMutationContext,
): Promise<PeopleMutationSnapshot> {
const data = record(input);
const applicationId = positiveInteger(data.applicationId);
if (data.decision !== "dismiss") {
throw new PeopleMutationFailure(
"VALIDATION",
"errors.housekeeping.validation",
);
}
const [application] = await db
.select({
id: WebsiteStaffApplications.id,
userId: WebsiteStaffApplications.userId,
rankId: WebsiteStaffApplications.rankId,
content: WebsiteStaffApplications.content,
})
.from(WebsiteStaffApplications)
.where(eq(WebsiteStaffApplications.id, BigInt(applicationId)))
.limit(1);
if (!application) {
throw new PeopleMutationFailure(
"NOT_FOUND",
"errors.housekeeping.notFound",
);
}
await db
.delete(WebsiteStaffApplications)
.where(eq(WebsiteStaffApplications.id, BigInt(applicationId)));
const snapshot = {
before: {
id: Number(application.id),
userId: application.userId,
rankId: application.rankId,
content: application.content,
},
after: null,
};
await auditMutation(
context,
"application.decide",
"StaffApplication",
applicationId,
snapshot,
);
return snapshot;
}
async function executeTeamChange(
input: unknown,
context: PeopleMutationContext,
): Promise<PeopleMutationSnapshot> {
const data = record(input);
if (data.action === "delete") {
const teamId = positiveInteger(data.teamId);
const [team] = await db
.select({
id: WebsiteTeams.id,
rankName: WebsiteTeams.rankName,
badge: WebsiteTeams.badge,
jobDescription: WebsiteTeams.jobDescription,
hiddenRank: WebsiteTeams.hiddenRank,
})
.from(WebsiteTeams)
.where(eq(WebsiteTeams.id, BigInt(teamId)))
.limit(1);
if (!team) {
throw new PeopleMutationFailure(
"NOT_FOUND",
"errors.housekeeping.notFound",
);
}
await db.delete(WebsiteTeams).where(eq(WebsiteTeams.id, BigInt(teamId)));
const snapshot = {
before: { ...team, id: Number(team.id) },
after: null,
};
await auditMutation(context, "team.change", "Team", teamId, snapshot);
return snapshot;
}
const rankName = normalizedText(data.rankName, 255);
const badge = normalizedText(data.badge, 255, false);
const jobDescription = normalizedText(data.jobDescription, 255, false);
const staffColor = normalizedText(data.staffColor || "#327fa8", 255);
const hiddenRank = Boolean(data.hiddenRank);
const now = new Date();
const [result] = await db.insert(WebsiteTeams).values({
rankName,
badge: badge || null,
jobDescription: jobDescription || null,
staffColor,
hiddenRank,
createdAt: now,
updatedAt: now,
});
const teamId = Number(result.insertId);
const snapshot = {
before: null,
after: {
teamId,
rankName,
badge: badge || null,
jobDescription: jobDescription || null,
staffColor,
hiddenRank,
},
};
await auditMutation(context, "team.change", "Team", teamId, snapshot);
return snapshot;
}
async function executeIpAction(
input: unknown,
context: PeopleMutationContext,
): Promise<PeopleMutationSnapshot> {
const data = record(input);
const action = normalizedText(data.action, 32);
const blacklist = action.endsWith("blacklist");
const adding = action.startsWith("add-");
const table = blacklist ? WebsiteIpBlacklist : WebsiteIpWhitelist;
if (adding) {
const ipAddress = normalizedText(data.ipAddress, 255);
const asn = normalizedText(data.asn, 255, false) || null;
const [result] = blacklist
? await db
.insert(WebsiteIpBlacklist)
.values({ ipAddress, asn, blacklistAsn: asn !== null })
: await db
.insert(WebsiteIpWhitelist)
.values({ ipAddress, asn, whitelistAsn: asn !== null });
const id = Number(result.insertId);
const snapshot = { before: null, after: { id, action, ipAddress, asn } };
await auditMutation(context, "ip.action", "IpRule", id, snapshot);
return snapshot;
}
const id = positiveInteger(data.id);
await db.delete(table).where(eq(table.id, BigInt(id)));
const snapshot = { before: { id, action }, after: null };
await auditMutation(context, "ip.action", "IpRule", id, snapshot);
return snapshot;
}
async function writeVpnSetting(
key: string,
value: string,
comment: string,
): Promise<void> {
await db
.insert(WebsiteSetting)
.values({ key, value, comment })
.onDuplicateKeyUpdate({ set: { value } });
}
async function executeVpnConfiguration(
input: unknown,
context: PeopleMutationContext,
): Promise<PeopleMutationSnapshot> {
const data = record(input);
const enabled = Boolean(data.enabled);
const provider = normalizedText(data.provider, 32);
const apiKey = normalizedText(data.apiKey, 255, false);
const blockMessage = normalizedText(data.blockMessage, 255, false);
const [storedEnabled, storedProvider, storedApiKey, storedBlockMessage] =
await Promise.all([
siteSettings.get("vpn_block_enabled"),
siteSettings.get("vpn_provider"),
siteSettings.get("vpn_api_key"),
siteSettings.get("vpn_block_message"),
]);
const before = {
enabled: storedEnabled === "1",
provider: storedProvider ?? "none",
apiKeyConfigured: Boolean(storedApiKey),
blockMessage: storedBlockMessage ?? "",
};
await writeVpnSetting(
"vpn_block_enabled",
enabled ? "1" : "0",
"Block registrations from detected VPN/proxy IPs (0=no, 1=yes)",
);
await writeVpnSetting(
"vpn_provider",
provider,
"VPN/proxy detection provider (none/proxycheck/ipqualityscore)",
);
await writeVpnSetting(
"vpn_api_key",
apiKey,
"API key for the VPN/proxy detection provider",
);
await writeVpnSetting(
"vpn_block_message",
blockMessage,
"Message shown to users blocked for using a VPN/proxy",
);
await siteSettings.reload();
const snapshot = {
before,
after: {
enabled,
provider,
apiKeyConfigured: apiKey.length > 0,
blockMessage,
},
};
await auditMutation(
context,
"vpn.configure",
"VpnConfiguration",
undefined,
snapshot,
);
return snapshot;
}
async function executeWordFilterUpdate(
input: unknown,
context: PeopleMutationContext,
): Promise<PeopleMutationSnapshot> {
const data = record(input);
if (data.action === "add") {
const word = normalizedText(data.word, 255);
const [result] = (await db
.insert(WebsiteWordfilter)
.values({ word })) as unknown as [ResultSetHeader];
const id = Number(result.insertId);
reloadWordFilter();
await rcon.updateWordFilter();
const snapshot = { before: null, after: { id, word } };
await auditMutation(
context,
"word-filter.update",
"WordFilter",
id,
snapshot,
);
return snapshot;
}
const id = positiveInteger(data.id);
const [existing] = await db
.select({ id: WebsiteWordfilter.id, word: WebsiteWordfilter.word })
.from(WebsiteWordfilter)
.where(eq(WebsiteWordfilter.id, BigInt(id)))
.limit(1);
if (!existing) {
throw new PeopleMutationFailure(
"NOT_FOUND",
"errors.housekeeping.notFound",
);
}
await db
.delete(WebsiteWordfilter)
.where(eq(WebsiteWordfilter.id, BigInt(id)));
reloadWordFilter();
await rcon.updateWordFilter();
const snapshot = {
before: { id: Number(existing.id), word: existing.word },
after: null,
};
await auditMutation(
context,
"word-filter.update",
"WordFilter",
id,
snapshot,
);
return snapshot;
}
const productionPeopleMutationAdapter: PeopleMutationAdapter = {
async execute(operation, input, context) {
if (operation.startsWith("user.")) {
return executeUserMutation(
operation as Extract<PeopleMutationOperation, `user.${string}`>,
input,
context,
);
}
if (operation.startsWith("users.")) {
return executeBulkMutation(
operation as Extract<PeopleMutationOperation, `users.${string}`>,
input,
context,
);
}
if (operation === "guild.disband")
return executeGuildDisband(input, context);
if (operation === "application.decide") {
return executeApplicationDecision(input, context);
}
if (operation === "team.change") return executeTeamChange(input, context);
if (operation === "ip.action") return executeIpAction(input, context);
if (operation === "vpn.configure") {
return executeVpnConfiguration(input, context);
}
return executeWordFilterUpdate(input, context);
},
};
export const peopleMutationService = createPeopleMutationService(
productionPeopleMutationAdapter,
);