fix(admin): P0 integrity — permanent bans, ACL sidebar, rank guards
Deploy / release (push) Skipped
Deploy / deploy (push) Successful in 1m36s

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
SimoandCursor committed 2026-07-22 19:01:59 +02:00
1 parent 11004626c7
commit 75cdfdebe4
14 files changed
+458 -68

No files matched your search

+2 -4
View File
@@ -15,8 +15,6 @@ const BAN_TYPES: ReadonlySet<string> = new Set([
"machine",
"super",
]);
const PERMANENT_SECONDS = 100 * 365 * 24 * 3600;
export async function createBan(formData: FormData): Promise<void> {
const staff = await requirePermissionRateLimited(PERMS.USERS_BAN);
const userId = Number(formData.get("userId"));
@@ -30,8 +28,8 @@ export async function createBan(formData: FormData): Promise<void> {
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;
// Emulator convention: banExpire 0 = permanent (not a far-future timestamp).
const banExpire = hours > 0 ? now + Math.floor(hours) * 3600 : 0;
const user = await prisma.user.findUnique({
where: { id: userId },
+32 -7
View File
@@ -3,6 +3,7 @@
import { revalidatePath } from "next/cache";
import { z } from "zod";
import { PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { adminAction } from "@/lib/safe-action";
import { ActionError, actionOk } from "@/lib/safe-action-shared";
import { rcon } from "@/lib/services/rcon";
@@ -70,9 +71,7 @@ export const disconnectUser = adminAction(
{ permission: PERMS.RCON_EXECUTE, schema: disconnectSchema },
async (ctx) => {
const username = ctx.data.username.normalize("NFC");
await requireRconOk(
await rcon.disconnectUser(ctx.data.userId, username),
);
await requireRconOk(await rcon.disconnectUser(ctx.data.userId, username));
revalidatePath(PATH);
return actionOk();
},
@@ -191,14 +190,42 @@ export const setMotto = adminAction(
const setRankSchema = z.object({
userId: z.coerce.number().int().positive(),
rank: z.coerce.number().int().min(0).max(10),
rank: z.coerce.number().int().min(1).max(9999),
});
/** Set a user's rank (rcon: setrank). */
export const setRank = adminAction(
{ permission: PERMS.RCON_EXECUTE, schema: setRankSchema },
async (ctx) => {
const staffRank = Number(ctx.session.user.rank);
const isSuper = ctx.permissions.isSuperAdmin;
const target = await prisma.user.findUnique({
where: { id: ctx.data.userId },
select: { rank: true },
});
if (!target) throw new ActionError("User not found");
const rankExists = await prisma.$queryRaw<{ id: number }[]>`
SELECT id FROM permission_ranks WHERE id = ${ctx.data.rank} LIMIT 1
`.catch(() => [] as { id: number }[]);
if (rankExists.length === 0) throw new ActionError("Rank does not exist");
if (!isSuper) {
if (target.rank >= staffRank) {
throw new ActionError(
"Cannot change rank of a user at or above your rank",
);
}
if (ctx.data.rank >= staffRank) {
throw new ActionError("Cannot set a rank equal to or above your own");
}
}
await requireRconOk(await rcon.setRank(ctx.data.userId, ctx.data.rank));
await prisma.user.update({
where: { id: ctx.data.userId },
data: { rank: ctx.data.rank },
});
revalidatePath(PATH);
return actionOk();
},
@@ -214,9 +241,7 @@ export const executeCommand = adminAction(
{ permission: PERMS.RCON_EXECUTE, schema: executeCommandSchema },
async (ctx) => {
const command = ctx.data.command.normalize("NFC");
await requireRconOk(
await rcon.executeCommand(ctx.data.userId, command),
);
await requireRconOk(await rcon.executeCommand(ctx.data.userId, command));
revalidatePath(PATH);
return actionOk();
},