Local Build and Deploy / deploy (push) Successful in 59s
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.
71 lines
2.1 KiB
TypeScript
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");
|
|
}
|