Files
Epicnabbo-Catalogus-Updated…/src/actions/bulk-users.ts
T
simoleo89andCursor 0e89d03940 Finish fine-grained ACL across remaining admin pages and actions.
Replace leftover requireStaff gates with module PERMS, drop hardcoded room rank thresholds, and expand contract tests so admin mutations cannot regress to dashboard-only checks.

Co-authored-by: Cursor <[email protected]>
2026-07-15 20:15:22 +02:00

184 lines
4.3 KiB
TypeScript

"use server";
import { requirePermission } from "@/lib/admin/guard";
import { PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import type { ActionResult } from "@/lib/safe-action-shared";
import { rcon } from "@/lib/services/rcon";
import { logStaffActivity } from "@/lib/services/staff-activity";
export async function bulkUnban({
userIds,
}: {
userIds: number[];
}): Promise<ActionResult<{ unbanned: number; total: number }>> {
const staff = await requirePermission(PERMS.USERS_EDIT);
const result = await prisma.ban.deleteMany({
where: { userId: { in: userIds } },
});
await logStaffActivity({
staffId: staff.id,
action: "bulk_unban",
description: `Unbanned ${result.count} user(s)`,
targetType: "user",
});
return {
ok: true as const,
data: { unbanned: result.count, total: userIds.length },
};
}
export async function bulkBan({
userIds,
reason,
duration,
}: {
userIds: number[];
reason: string;
duration: number;
}): Promise<ActionResult<{ banned: number }>> {
const staff = await requirePermission(PERMS.USERS_EDIT);
const now = Math.floor(Date.now() / 1000);
let banned = 0;
for (const userId of userIds) {
try {
await prisma.ban.create({
data: {
userId,
ip: "",
machineId: "",
userStaffId: staff.id,
timestamp: now,
banExpire: duration > 0 ? now + duration : 0,
banReason: reason,
type: "account",
},
});
banned++;
} catch {
// skip duplicates
}
}
await logStaffActivity({
staffId: staff.id,
action: "bulk_ban",
description: `Banned ${banned} user(s)`,
targetType: "user",
});
return { ok: true as const, data: { banned } };
}
export async function bulkGiveCurrency({
userIds,
amount,
type,
}: {
userIds: number[];
amount: number;
type: "credits" | "pixels" | "points";
}): Promise<
ActionResult<{
given: number;
total: number;
failedIds: Array<{ userId: number; reason: string }>;
}>
> {
const staff = await requirePermission(PERMS.USERS_EDIT);
let given = 0;
const failedIds: Array<{ userId: number; reason: string }> = [];
for (const userId of userIds) {
try {
if (type === "credits") {
await prisma.user.update({
where: { id: userId },
data: { credits: { increment: amount } },
});
await rcon.giveCredits(userId, amount);
} else if (type === "pixels") {
await prisma.usersCurrency.upsert({
where: { userId_type: { userId, type: 0 } },
update: { amount: { increment: amount } },
create: { userId, type: 0, amount },
});
await rcon.giveDuckets(userId, amount);
} else if (type === "points") {
await prisma.usersCurrency.upsert({
where: { userId_type: { userId, type: 101 } },
update: { amount: { increment: amount } },
create: { userId, type: 101, amount },
});
await rcon.givePointsGotw(userId, amount);
}
given++;
} catch {
failedIds.push({ userId, reason: "Database error" });
}
}
await logStaffActivity({
staffId: staff.id,
action: "bulk_give_currency",
description: `Gave ${amount} ${type} to ${given} user(s)`,
targetType: "user",
});
return {
ok: true as const,
data: { given, total: userIds.length, failedIds },
};
}
export async function bulkGiveBadge({
userIds,
badgeCode,
}: {
userIds: number[];
badgeCode: string;
}): Promise<
ActionResult<{
given: number;
total: number;
failedIds: Array<{ userId: number; reason: string }>;
}>
> {
const staff = await requirePermission(PERMS.USERS_EDIT);
let given = 0;
const failedIds: Array<{ userId: number; reason: string }> = [];
for (const userId of userIds) {
try {
const existing = await prisma.usersBadges.findFirst({
where: { userId, badgeCode },
select: { id: true },
});
if (!existing) {
const max = await prisma.usersBadges.aggregate({
where: { userId },
_max: { slotId: true },
});
const slotId = (max._max.slotId ?? 0) + 1;
await prisma.usersBadges.create({
data: { userId, slotId, badgeCode },
});
await rcon.giveBadge(userId, badgeCode);
}
given++;
} catch {
failedIds.push({ userId, reason: "Database error" });
}
}
await logStaffActivity({
staffId: staff.id,
action: "bulk_give_badge",
description: `Gave badge "${badgeCode}" to ${given} user(s)`,
targetType: "user",
});
return {
ok: true as const,
data: { given, total: userIds.length, failedIds },
};
}