Files
EpicNext-Cms/src/actions/admin-bans.ts
T
openhands e5ae51bff7
Local Build and Deploy / deploy (push) Successful in 59s
Add NFC normalization to all FormData inputs across 41 server actions
All user-supplied string values from FormData now go through
String.prototype.normalize('NFC') to prevent Unicode homoglyph
attacks and canonicalization bypasses. NFC is idempotent for
already-normalized strings, so this is a pure security improvement
with zero behavioral change for legitimate users.
2026-07-13 12:21:37 +02:00

71 lines
2.1 KiB
TypeScript

"use server";
import { revalidatePath } from "next/cache";
import { requireStaffRateLimited as requireStaff } from "@/lib/admin/guard";
import { prisma } from "@/lib/prisma";
import { rcon } from "@/lib/services/rcon";
import { logStaffActivity } from "@/lib/services/staff-activity";
import type { $Enums } from "@/generated/prisma/client";
type BanType = $Enums.bans_type;
const BAN_TYPES: ReadonlySet<string> = new Set(["account", "ip", "machine", "super"]);
const PERMANENT_SECONDS = 100 * 365 * 24 * 3600;
export async function createBan(formData: FormData): Promise<void> {
const staff = await requireStaff();
const userId = Number(formData.get("userId"));
const reason =
String(formData.get("reason") ?? "").normalize("NFC")
.trim()
.slice(0, 200) || "Banned";
const hours = Number(formData.get("hours"));
const type = String(formData.get("type"));
if (!(userId > 0) || !BAN_TYPES.has(type)) return;
const now = Math.floor(Date.now() / 1000);
const banExpire = hours > 0 ? now + Math.floor(hours) * 3600 : now + PERMANENT_SECONDS;
const user = await prisma.user.findUnique({
where: { id: userId },
select: { username: true },
});
await prisma.ban.create({
data: {
userId,
ip: "",
machineId: "",
userStaffId: staff.id,
timestamp: now,
banExpire,
banReason: reason,
type: type as BanType,
cfhTopic: -1,
},
});
if (user) await rcon.disconnectUser(userId, user.username);
await logStaffActivity({
staffId: staff.id,
action: "user_ban",
description: `Banned user #${userId} (${type}, ${hours > 0 ? `${hours}h` : "permanent"}): ${reason}`,
targetType: "user",
targetId: userId,
});
revalidatePath("/admin/bans");
}
export async function liftBan(formData: FormData): Promise<void> {
const staff = await requireStaff();
const id = Number(formData.get("id"));
if (id > 0) {
await prisma.ban.delete({ where: { id } });
await logStaffActivity({
staffId: staff.id,
action: "ban_lift",
description: `Lifted ban #${id}`,
});
}
revalidatePath("/admin/bans");
}