fix(admin): P0 integrity — permanent bans, ACL sidebar, rank guards
Co-authored-by: Cursor <[email protected]>
This commit is contained in:
1 parent
11004626c7
commit
75cdfdebe4
14 files changed
+458
-68
No files matched your search
@@ -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 },
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user