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]>
This commit is contained in:
SimoandCursor committed 2026-07-15 20:15:22 +02:00
1 parent 3c8a8ff888
commit 0e89d03940
63 files changed
+420 -163

No files matched your search

+5 -4
View File
@@ -2,7 +2,8 @@
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { requireStaff } from "@/lib/admin/guard";
import { requirePermission } from "@/lib/admin/guard";
import { PERMS } from "@/lib/permissions";
import { formPositiveBigInt } from "@/lib/form-data";
import { prisma } from "@/lib/prisma";
import { logStaffActivity } from "@/lib/services/staff-activity";
@@ -11,7 +12,7 @@ import { logStaffActivity } from "@/lib/services/staff-activity";
// table; it only stores an image URL rendered in the site layout/widgets.
export async function createAd(formData: FormData): Promise<void> {
const staff = await requireStaff();
const staff = await requirePermission(PERMS.PAGES_EDIT);
const image = String(formData.get("image") ?? "")
.normalize("NFC")
.trim()
@@ -39,7 +40,7 @@ export async function createAd(formData: FormData): Promise<void> {
}
export async function updateAd(formData: FormData): Promise<void> {
const staff = await requireStaff();
const staff = await requirePermission(PERMS.PAGES_EDIT);
const raw = String(formData.get("id") ?? "").normalize("NFC");
if (!/^\d+$/.test(raw)) return;
const id = BigInt(raw);
@@ -70,7 +71,7 @@ export async function updateAd(formData: FormData): Promise<void> {
}
export async function deleteAd(formData: FormData): Promise<void> {
const staff = await requireStaff();
const staff = await requirePermission(PERMS.PAGES_EDIT);
const id = formPositiveBigInt(formData, "id");
if (!id) return;
+3 -2
View File
@@ -1,7 +1,8 @@
"use server";
import { revalidatePath } from "next/cache";
import { requireStaff } from "@/lib/admin/guard";
import { requirePermission } from "@/lib/admin/guard";
import { PERMS } from "@/lib/permissions";
import { rcon } from "@/lib/services/rcon";
/**
@@ -11,7 +12,7 @@ import { rcon } from "@/lib/services/rcon";
* `message` payload. Staff-gated; the message is trimmed/bounded before send.
*/
export async function sendHotelAlert(formData: FormData): Promise<void> {
await requireStaff();
await requirePermission(PERMS.NOTIFICATIONS_EDIT);
const message = String(formData.get("message") ?? "")
.normalize("NFC")
+3 -2
View File
@@ -1,12 +1,13 @@
"use server";
import { revalidatePath } from "next/cache";
import { requireStaff } from "@/lib/admin/guard";
import { requirePermission } from "@/lib/admin/guard";
import { PERMS } from "@/lib/permissions";
import { formPositiveBigInt } from "@/lib/form-data";
import { prisma } from "@/lib/prisma";
export async function dismissApplication(formData: FormData): Promise<void> {
await requireStaff();
await requirePermission(PERMS.USERS_EDIT);
const id = formPositiveBigInt(formData, "id");
if (!id) return;
+3 -2
View File
@@ -3,7 +3,8 @@
import { writeFile } from "node:fs/promises";
import path from "node:path";
import { redirect } from "next/navigation";
import { requireStaff } from "@/lib/admin/guard";
import { requirePermission } from "@/lib/admin/guard";
import { PERMS } from "@/lib/permissions";
import { logStaffActivity } from "@/lib/services/staff-activity";
// Writes a badge image to the configured emulator badge directory. The path is
@@ -20,7 +21,7 @@ function back(param: string, value: string): never {
}
export async function uploadBadge(formData: FormData): Promise<void> {
const staff = await requireStaff();
const staff = await requirePermission(PERMS.CATALOG_EDIT);
const dir = process.env.BADGE_UPLOAD_DIR;
if (!dir) {
+3 -2
View File
@@ -1,12 +1,13 @@
"use server";
import { revalidatePath } from "next/cache";
import { requireStaff } from "@/lib/admin/guard";
import { requirePermission } from "@/lib/admin/guard";
import { PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { rcon } from "@/lib/services/rcon";
export async function giveBadge(formData: FormData): Promise<void> {
await requireStaff();
await requirePermission(PERMS.CATALOG_EDIT);
const userId = Number(formData.get("userId"));
const code = String(formData.get("code") ?? "")
+5 -4
View File
@@ -1,12 +1,13 @@
"use server";
import { revalidatePath } from "next/cache";
import { requireStaff } from "@/lib/admin/guard";
import { requirePermission } from "@/lib/admin/guard";
import { PERMS } from "@/lib/permissions";
import { formPositiveBigInt } from "@/lib/form-data";
import { prisma } from "@/lib/prisma";
export async function createEmailTemplate(formData: FormData): Promise<void> {
await requireStaff();
await requirePermission(PERMS.PAGES_EDIT);
const name = String(formData.get("name") ?? "")
.normalize("NFC")
.trim()
@@ -35,7 +36,7 @@ export async function createEmailTemplate(formData: FormData): Promise<void> {
}
export async function updateEmailTemplate(formData: FormData): Promise<void> {
await requireStaff();
await requirePermission(PERMS.PAGES_EDIT);
const raw = String(formData.get("id") ?? "").normalize("NFC");
if (!raw) return;
let id: bigint;
@@ -68,7 +69,7 @@ export async function updateEmailTemplate(formData: FormData): Promise<void> {
}
export async function deleteEmailTemplate(formData: FormData): Promise<void> {
await requireStaff();
await requirePermission(PERMS.PAGES_EDIT);
const id = formPositiveBigInt(formData, "id");
if (!id) return;
await prisma.emailTemplates.delete({ where: { id } });
+5 -4
View File
@@ -2,7 +2,8 @@
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { requireStaff } from "@/lib/admin/guard";
import { requirePermission } from "@/lib/admin/guard";
import { PERMS } from "@/lib/permissions";
import { formPositiveBigInt } from "@/lib/form-data";
import { prisma } from "@/lib/prisma";
import { logStaffActivity } from "@/lib/services/staff-activity";
@@ -16,7 +17,7 @@ function parsePosition(value: FormDataEntryValue | null): number {
}
export async function createHelpQuestion(formData: FormData): Promise<void> {
const staff = await requireStaff();
const staff = await requirePermission(PERMS.PAGES_EDIT);
const name = String(formData.get("name") ?? "")
.normalize("NFC")
.trim()
@@ -82,7 +83,7 @@ export async function createHelpQuestion(formData: FormData): Promise<void> {
}
export async function updateHelpQuestion(formData: FormData): Promise<void> {
const staff = await requireStaff();
const staff = await requirePermission(PERMS.PAGES_EDIT);
const id = formPositiveBigInt(formData, "id");
if (!id) return;
@@ -149,7 +150,7 @@ export async function updateHelpQuestion(formData: FormData): Promise<void> {
}
export async function deleteHelpQuestion(formData: FormData): Promise<void> {
const staff = await requireStaff();
const staff = await requirePermission(PERMS.PAGES_EDIT);
const id = formPositiveBigInt(formData, "id");
if (!id) return;
+4 -3
View File
@@ -1,7 +1,8 @@
"use server";
import { revalidatePath } from "next/cache";
import { requireStaff } from "@/lib/admin/guard";
import { requirePermission } from "@/lib/admin/guard";
import { PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
/**
@@ -9,7 +10,7 @@ import { prisma } from "@/lib/prisma";
* string). Mirrors AtomCMS' housekeeping permission management.
*/
export async function upsertPermission(formData: FormData): Promise<void> {
await requireStaff();
await requirePermission(PERMS.SETTINGS_EDIT);
const permission = String(formData.get("permission") ?? "")
.normalize("NFC")
@@ -38,7 +39,7 @@ export async function upsertPermission(formData: FormData): Promise<void> {
}
export async function deletePermission(formData: FormData): Promise<void> {
await requireStaff();
await requirePermission(PERMS.SETTINGS_EDIT);
const raw = String(formData.get("id") ?? "").normalize("NFC");
if (!raw) return;
+6 -5
View File
@@ -1,7 +1,8 @@
"use server";
import { revalidatePath } from "next/cache";
import { requireStaff } from "@/lib/admin/guard";
import { requirePermission } from "@/lib/admin/guard";
import { PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
function parseIp(formData: FormData): string {
@@ -20,7 +21,7 @@ function parseAsn(formData: FormData): string | null {
}
export async function addWhitelist(formData: FormData): Promise<void> {
await requireStaff();
await requirePermission(PERMS.SETTINGS_EDIT);
const ipAddress = parseIp(formData);
if (!ipAddress) return;
const asn = parseAsn(formData);
@@ -31,7 +32,7 @@ export async function addWhitelist(formData: FormData): Promise<void> {
}
export async function deleteWhitelist(formData: FormData): Promise<void> {
await requireStaff();
await requirePermission(PERMS.SETTINGS_EDIT);
const raw = String(formData.get("id") ?? "")
.normalize("NFC")
.trim();
@@ -41,7 +42,7 @@ export async function deleteWhitelist(formData: FormData): Promise<void> {
}
export async function addBlacklist(formData: FormData): Promise<void> {
await requireStaff();
await requirePermission(PERMS.SETTINGS_EDIT);
const ipAddress = parseIp(formData);
if (!ipAddress) return;
const asn = parseAsn(formData);
@@ -52,7 +53,7 @@ export async function addBlacklist(formData: FormData): Promise<void> {
}
export async function deleteBlacklist(formData: FormData): Promise<void> {
await requireStaff();
await requirePermission(PERMS.SETTINGS_EDIT);
const raw = String(formData.get("id") ?? "")
.normalize("NFC")
.trim();
+3 -2
View File
@@ -1,7 +1,8 @@
"use server";
import { revalidatePath } from "next/cache";
import { requireStaff } from "@/lib/admin/guard";
import { requirePermission } from "@/lib/admin/guard";
import { PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { siteSettings } from "@/lib/services/site-settings";
@@ -35,7 +36,7 @@ async function upsertSetting(key: string, value: string): Promise<void> {
}
export async function saveMaintenance(formData: FormData): Promise<void> {
await requireStaff();
await requirePermission(PERMS.SETTINGS_EDIT);
// Checkbox: present only when ticked. Normalise to the '1'/'0' string the
// emulator/Laravel side expects.
+5 -4
View File
@@ -3,14 +3,15 @@
import { mkdir, writeFile } from "node:fs/promises";
import path from "node:path";
import { revalidatePath } from "next/cache";
import { requireStaff } from "@/lib/admin/guard";
import { requirePermission } from "@/lib/admin/guard";
import { PERMS } from "@/lib/permissions";
const MEDIA_DIR = "public/assets/images/media";
const MAX_SIZE = 5 * 1024 * 1024; // 5MB
const ALLOWED = ["image/png", "image/jpeg", "image/gif", "image/webp"];
export async function uploadMedia(formData: FormData): Promise<void> {
await requireStaff();
await requirePermission(PERMS.PAGES_EDIT);
const file = formData.get("file") as File | null;
if (!file || file.size === 0) return;
if (file.size > MAX_SIZE) return;
@@ -33,7 +34,7 @@ export async function uploadMedia(formData: FormData): Promise<void> {
}
export async function deleteMedia(name: string): Promise<void> {
await requireStaff();
await requirePermission(PERMS.PAGES_EDIT);
const { unlink } = await import("node:fs/promises");
const baseDir = path.resolve(process.cwd(), MEDIA_DIR);
const filePath = path.resolve(baseDir, name);
@@ -50,7 +51,7 @@ export async function deleteMedia(name: string): Promise<void> {
export async function uploadMediaAndReturn(
formData: FormData,
): Promise<string> {
await requireStaff();
await requirePermission(PERMS.PAGES_EDIT);
const file = formData.get("file") as File | null;
if (!file || file.size === 0) return "";
if (file.size > MAX_SIZE) return "";
+5 -4
View File
@@ -2,7 +2,8 @@
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { requireStaff } from "@/lib/admin/guard";
import { requirePermission } from "@/lib/admin/guard";
import { PERMS } from "@/lib/permissions";
import { formPositiveBigInt } from "@/lib/form-data";
import { prisma } from "@/lib/prisma";
import { logServerError } from "@/lib/server-log";
@@ -23,7 +24,7 @@ function parseMinRank(formData: FormData): number {
}
export async function createPermission(formData: FormData): Promise<void> {
const staff = await requireStaff();
const staff = await requirePermission(PERMS.PERMISSIONS_MANAGE);
const permission = String(formData.get("permission") ?? "")
.normalize("NFC")
.trim()
@@ -67,7 +68,7 @@ export async function createPermission(formData: FormData): Promise<void> {
}
export async function updatePermission(formData: FormData): Promise<void> {
const staff = await requireStaff();
const staff = await requirePermission(PERMS.PERMISSIONS_MANAGE);
const raw = String(formData.get("id") ?? "").normalize("NFC");
if (!raw) return;
const id = BigInt(raw);
@@ -106,7 +107,7 @@ export async function updatePermission(formData: FormData): Promise<void> {
}
export async function deletePermission(formData: FormData): Promise<void> {
const staff = await requireStaff();
const staff = await requirePermission(PERMS.PERMISSIONS_MANAGE);
const id = formPositiveBigInt(formData, "id");
if (!id) return;
+3 -2
View File
@@ -1,11 +1,12 @@
"use server";
import { revalidatePath } from "next/cache";
import { requireStaff } from "@/lib/admin/guard";
import { requirePermission } from "@/lib/admin/guard";
import { PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
export async function deletePhoto(formData: FormData): Promise<void> {
await requireStaff();
await requirePermission(PERMS.PAGES_EDIT);
const id = Number(formData.get("id"));
if (!(id > 0)) return;
+6 -5
View File
@@ -1,12 +1,13 @@
"use server";
import { revalidatePath } from "next/cache";
import { requireStaff } from "@/lib/admin/guard";
import { requirePermission } from "@/lib/admin/guard";
import { PERMS } from "@/lib/permissions";
import { formPositiveBigInt } from "@/lib/form-data";
import { prisma } from "@/lib/prisma";
export async function createCategory(formData: FormData): Promise<void> {
await requireStaff();
await requirePermission(PERMS.SHOP_EDIT);
const name = String(formData.get("name") ?? "")
.normalize("NFC")
.trim()
@@ -33,7 +34,7 @@ export async function createCategory(formData: FormData): Promise<void> {
}
export async function deleteCategory(formData: FormData): Promise<void> {
await requireStaff();
await requirePermission(PERMS.SHOP_EDIT);
const id = formPositiveBigInt(formData, "id");
if (!id) return;
@@ -48,7 +49,7 @@ export async function deleteCategory(formData: FormData): Promise<void> {
}
export async function createValue(formData: FormData): Promise<void> {
await requireStaff();
await requirePermission(PERMS.SHOP_EDIT);
const categoryId = formPositiveBigInt(formData, "categoryId");
if (!categoryId) return;
@@ -98,7 +99,7 @@ export async function createValue(formData: FormData): Promise<void> {
}
export async function deleteValue(formData: FormData): Promise<void> {
await requireStaff();
await requirePermission(PERMS.SHOP_EDIT);
const id = formPositiveBigInt(formData, "id");
if (!id) return;
+5 -4
View File
@@ -1,7 +1,8 @@
"use server";
import { revalidatePath } from "next/cache";
import { requireStaff } from "@/lib/admin/guard";
import { requirePermission } from "@/lib/admin/guard";
import { PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { logStaffActivity } from "@/lib/services/staff-activity";
@@ -31,7 +32,7 @@ function normaliseColor(raw: string): string {
// ── Tags CRUD (tags + taggables, AtomCMS article tags/categories) ──────────
export async function createTag(formData: FormData): Promise<void> {
const staff = await requireStaff();
const staff = await requirePermission(PERMS.PAGES_EDIT);
const name = str(formData.get("name")).trim().slice(0, 255);
if (!name) return;
@@ -56,7 +57,7 @@ export async function createTag(formData: FormData): Promise<void> {
}
export async function updateTag(formData: FormData): Promise<void> {
const staff = await requireStaff();
const staff = await requirePermission(PERMS.PAGES_EDIT);
const id = parseId(formData.get("id"));
if (id === null) return;
@@ -83,7 +84,7 @@ export async function updateTag(formData: FormData): Promise<void> {
}
export async function deleteTag(formData: FormData): Promise<void> {
const staff = await requireStaff();
const staff = await requirePermission(PERMS.PAGES_EDIT);
const id = parseId(formData.get("id"));
if (id === null) return;
+4 -3
View File
@@ -1,11 +1,12 @@
"use server";
import { revalidatePath } from "next/cache";
import { requireStaff } from "@/lib/admin/guard";
import { requirePermission } from "@/lib/admin/guard";
import { PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
export async function createTeam(formData: FormData): Promise<void> {
await requireStaff();
await requirePermission(PERMS.USERS_EDIT);
const rankName = String(formData.get("rankName") ?? "")
.normalize("NFC")
@@ -41,7 +42,7 @@ export async function createTeam(formData: FormData): Promise<void> {
}
export async function deleteTeam(formData: FormData): Promise<void> {
await requireStaff();
await requirePermission(PERMS.USERS_EDIT);
const id = BigInt(String(formData.get("id")));
await prisma.websiteTeams.delete({ where: { id } });
+7 -6
View File
@@ -1,7 +1,8 @@
"use server";
import { revalidatePath } from "next/cache";
import { requireStaff, requireStaffRateLimited } from "@/lib/admin/guard";
import { requirePermission, requirePermissionRateLimited } from "@/lib/admin/guard";
import { PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { rcon } from "@/lib/services/rcon";
import { type CurrencyName, sendCurrency } from "@/lib/services/send-currency";
@@ -15,7 +16,7 @@ const CURRENCIES: ReadonlySet<string> = new Set([
]);
export async function giveCurrency(formData: FormData): Promise<void> {
const staff = await requireStaffRateLimited();
const staff = await requirePermissionRateLimited(PERMS.USERS_EDIT);
const userId = Number(formData.get("userId"));
const type = String(formData.get("type"));
const amount = Number(formData.get("amount"));
@@ -38,7 +39,7 @@ export async function giveCurrency(formData: FormData): Promise<void> {
}
export async function setMotto(formData: FormData): Promise<void> {
await requireStaff();
await requirePermission(PERMS.USERS_EDIT);
const userId = Number(formData.get("userId"));
const motto = String(formData.get("motto") ?? "")
.normalize("NFC")
@@ -51,7 +52,7 @@ export async function setMotto(formData: FormData): Promise<void> {
}
export async function setRank(formData: FormData): Promise<void> {
const staff = await requireStaffRateLimited();
const staff = await requirePermissionRateLimited(PERMS.USERS_EDIT);
const userId = Number(formData.get("userId"));
const rank = Number(formData.get("rank"));
if (userId > 0 && rank > 0) {
@@ -69,7 +70,7 @@ export async function setRank(formData: FormData): Promise<void> {
}
export async function alertUser(formData: FormData): Promise<void> {
await requireStaff();
await requirePermission(PERMS.USERS_EDIT);
const userId = Number(formData.get("userId"));
const message = String(formData.get("message") ?? "")
.normalize("NFC")
@@ -78,7 +79,7 @@ export async function alertUser(formData: FormData): Promise<void> {
}
export async function disconnectUser(formData: FormData): Promise<void> {
await requireStaff();
await requirePermission(PERMS.USERS_EDIT);
const userId = Number(formData.get("userId"));
const username = String(formData.get("username") ?? "").normalize("NFC");
if (userId > 0) await rcon.disconnectUser(userId, username);
+4 -3
View File
@@ -1,13 +1,14 @@
"use server";
import { revalidatePath } from "next/cache";
import { requireStaff } from "@/lib/admin/guard";
import { requirePermission } from "@/lib/admin/guard";
import { PERMS } from "@/lib/permissions";
import { formPositiveBigInt } from "@/lib/form-data";
import { prisma } from "@/lib/prisma";
import { logServerError } from "@/lib/server-log";
export async function createVoucher(formData: FormData): Promise<void> {
await requireStaff();
await requirePermission(PERMS.SHOP_EDIT);
const code = String(formData.get("code") ?? "")
.normalize("NFC")
@@ -53,7 +54,7 @@ export async function createVoucher(formData: FormData): Promise<void> {
}
export async function deleteVoucher(formData: FormData): Promise<void> {
await requireStaff();
await requirePermission(PERMS.SHOP_EDIT);
const id = formPositiveBigInt(formData, "id");
if (!id) return;
+3 -2
View File
@@ -2,7 +2,8 @@
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { requireStaff } from "@/lib/admin/guard";
import { requirePermission } from "@/lib/admin/guard";
import { PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { siteSettings } from "@/lib/services/site-settings";
import { logStaffActivity } from "@/lib/services/staff-activity";
@@ -28,7 +29,7 @@ async function writeSetting(
}
export async function saveVpn(formData: FormData): Promise<void> {
const staff = await requireStaff();
const staff = await requirePermission(PERMS.SETTINGS_EDIT);
// Toggle: an unchecked checkbox submits nothing, so absence === disabled.
const enabled =
+6 -5
View File
@@ -1,7 +1,8 @@
"use server";
import { revalidatePath } from "next/cache";
import { requireStaff } from "@/lib/admin/guard";
import { requirePermission } from "@/lib/admin/guard";
import { PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { logStaffActivity } from "@/lib/services/staff-activity";
@@ -40,7 +41,7 @@ function revalidate(): void {
}
export async function createBox(formData: FormData): Promise<void> {
const staff = await requireStaff();
const staff = await requirePermission(PERMS.PAGES_EDIT);
const title = String(formData.get("title") ?? "")
.normalize("NFC")
@@ -82,7 +83,7 @@ export async function createBox(formData: FormData): Promise<void> {
}
export async function updateBox(formData: FormData): Promise<void> {
const staff = await requireStaff();
const staff = await requirePermission(PERMS.PAGES_EDIT);
const id = parseId(formData);
if (id == null) return;
@@ -125,7 +126,7 @@ export async function updateBox(formData: FormData): Promise<void> {
}
export async function deleteBox(formData: FormData): Promise<void> {
const staff = await requireStaff();
const staff = await requirePermission(PERMS.PAGES_EDIT);
const id = parseId(formData);
if (id == null) return;
@@ -147,7 +148,7 @@ export async function deleteBox(formData: FormData): Promise<void> {
}
export async function toggleBox(formData: FormData): Promise<void> {
const staff = await requireStaff();
const staff = await requirePermission(PERMS.PAGES_EDIT);
const id = parseId(formData);
if (id == null) return;
+4 -3
View File
@@ -1,11 +1,12 @@
"use server";
import { revalidatePath } from "next/cache";
import { requireStaff } from "@/lib/admin/guard";
import { requirePermission } from "@/lib/admin/guard";
import { PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
export async function getBadgeData({ code }: { code: string }) {
await requireStaff();
await requirePermission(PERMS.CATALOG_EDIT);
const badge = await prisma.websiteBadges.findUnique({
where: { badgeKey: code },
select: { badgeName: true, badgeDescription: true },
@@ -26,7 +27,7 @@ export async function updateBadge({
name: string;
desc: string;
}) {
await requireStaff();
await requirePermission(PERMS.CATALOG_EDIT);
await prisma.websiteBadges.upsert({
where: { badgeKey: code },
update: { badgeName: name, badgeDescription: desc, updatedAt: new Date() },
+6 -5
View File
@@ -1,6 +1,7 @@
"use server";
import { requireStaff } from "@/lib/admin/guard";
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";
@@ -11,7 +12,7 @@ export async function bulkUnban({
}: {
userIds: number[];
}): Promise<ActionResult<{ unbanned: number; total: number }>> {
const staff = await requireStaff();
const staff = await requirePermission(PERMS.USERS_EDIT);
const result = await prisma.ban.deleteMany({
where: { userId: { in: userIds } },
});
@@ -36,7 +37,7 @@ export async function bulkBan({
reason: string;
duration: number;
}): Promise<ActionResult<{ banned: number }>> {
const staff = await requireStaff();
const staff = await requirePermission(PERMS.USERS_EDIT);
const now = Math.floor(Date.now() / 1000);
let banned = 0;
@@ -84,7 +85,7 @@ export async function bulkGiveCurrency({
failedIds: Array<{ userId: number; reason: string }>;
}>
> {
const staff = await requireStaff();
const staff = await requirePermission(PERMS.USERS_EDIT);
let given = 0;
const failedIds: Array<{ userId: number; reason: string }> = [];
@@ -142,7 +143,7 @@ export async function bulkGiveBadge({
failedIds: Array<{ userId: number; reason: string }>;
}>
> {
const staff = await requireStaff();
const staff = await requirePermission(PERMS.USERS_EDIT);
let given = 0;
const failedIds: Array<{ userId: number; reason: string }> = [];
+8 -7
View File
@@ -1,7 +1,8 @@
"use server";
import { revalidatePath } from "next/cache";
import { requireStaff } from "@/lib/admin/guard";
import { requirePermission } from "@/lib/admin/guard";
import { PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { rcon } from "@/lib/services/rcon";
import { logStaffActivity } from "@/lib/services/staff-activity";
@@ -10,7 +11,7 @@ export async function updateBcPage({
id,
...fields
}: { id: number } & Record<string, unknown>) {
const staff = await requireStaff();
const staff = await requirePermission(PERMS.CATALOG_EDIT);
await prisma.catalogPagesBc.update({ where: { id }, data: fields as any });
await rcon.updateCatalog();
await logStaffActivity({
@@ -25,7 +26,7 @@ export async function updateBcPage({
}
export async function deleteBcItem({ id }: { id: number }) {
const staff = await requireStaff();
const staff = await requirePermission(PERMS.CATALOG_EDIT);
await prisma.catalogItemsBc.delete({ where: { id } });
await logStaffActivity({
staffId: staff.id,
@@ -47,7 +48,7 @@ export async function updateBcItem({
orderNumber?: number;
extradata?: string;
}) {
const staff = await requireStaff();
const staff = await requirePermission(PERMS.CATALOG_EDIT);
await prisma.catalogItemsBc.update({ where: { id }, data: data as any });
await logStaffActivity({
staffId: staff.id,
@@ -69,7 +70,7 @@ export async function createBcItem({
orderNumber: number;
extradata: string;
}) {
const staff = await requireStaff();
const staff = await requirePermission(PERMS.CATALOG_EDIT);
const created = await prisma.catalogItemsBc.create({
data: { pageId, ...data },
});
@@ -90,7 +91,7 @@ export async function toggleBcPage({
id: number;
field: "enabled" | "visible";
}) {
await requireStaff();
await requirePermission(PERMS.CATALOG_EDIT);
const page = await prisma.catalogPagesBc.findUnique({
where: { id },
select: { enabled: true, visible: true },
@@ -108,7 +109,7 @@ export async function createBcPage(input: {
parentId: number;
pageLayout: string;
}) {
const staff = await requireStaff();
const staff = await requirePermission(PERMS.CATALOG_EDIT);
const created = await prisma.catalogPagesBc.create({
data: {
caption: input.caption,
+8 -7
View File
@@ -1,7 +1,8 @@
"use server";
import { revalidatePath } from "next/cache";
import { requireStaff } from "@/lib/admin/guard";
import { requirePermission } from "@/lib/admin/guard";
import { PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { rcon } from "@/lib/services/rcon";
import { logStaffActivity } from "@/lib/services/staff-activity";
@@ -23,7 +24,7 @@ export async function createCatalogItem(data: {
haveOffer: "0" | "1";
clubOnly: "0" | "1";
}) {
const staff = await requireStaff();
const staff = await requirePermission(PERMS.CATALOG_EDIT);
const created = await prisma.catalogItems.create({ data });
await rcon.updateCatalog();
await logStaffActivity({
@@ -38,7 +39,7 @@ export async function createCatalogItem(data: {
}
export async function deleteCatalogItems({ ids }: { ids: number[] }) {
const staff = await requireStaff();
const staff = await requirePermission(PERMS.CATALOG_EDIT);
await prisma.catalogItems.deleteMany({ where: { id: { in: ids } } });
await rcon.updateCatalog();
await logStaffActivity({
@@ -58,7 +59,7 @@ export async function moveCatalogItems({
ids: number[];
targetPageId: number;
}) {
await requireStaff();
await requirePermission(PERMS.CATALOG_EDIT);
await prisma.catalogItems.updateMany({
where: { id: { in: ids } },
data: { pageId: targetPageId },
@@ -73,7 +74,7 @@ export async function reorderCatalogItems({
}: {
orders: Array<{ id: number; orderNumber: number }>;
}) {
await requireStaff();
await requirePermission(PERMS.CATALOG_EDIT);
for (const { id, orderNumber } of orders) {
await prisma.catalogItems.update({ where: { id }, data: { orderNumber } });
}
@@ -91,7 +92,7 @@ export async function updateCatalogItem({
catalogFields: Record<string, unknown>;
baseItem?: { id: number; fields: Record<string, unknown> };
}) {
const staff = await requireStaff();
const staff = await requirePermission(PERMS.CATALOG_EDIT);
await prisma.catalogItems.update({
where: { id },
data: catalogFields as any,
@@ -119,7 +120,7 @@ export async function translateCatalogItems({
}: {
items: Array<{ id: number; publicName: string; description: string }>;
}) {
await requireStaff();
await requirePermission(PERMS.CATALOG_EDIT);
let namesUpdated = 0;
let descriptionsUpdated = 0;
let furniDataUpdated = 0;
+8 -7
View File
@@ -1,7 +1,8 @@
"use server";
import { revalidatePath } from "next/cache";
import { requireStaff } from "@/lib/admin/guard";
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";
@@ -11,7 +12,7 @@ export async function updateCatalogPage({
id,
...fields
}: { id: number } & Record<string, unknown>): Promise<ActionResult> {
const staff = await requireStaff();
const staff = await requirePermission(PERMS.CATALOG_EDIT);
await prisma.catalogPages.update({ where: { id }, data: fields as any });
await rcon.updateCatalog();
await logStaffActivity({
@@ -26,7 +27,7 @@ export async function updateCatalogPage({
}
export async function deleteCatalogPage({ id }: { id: number }) {
const staff = await requireStaff();
const staff = await requirePermission(PERMS.CATALOG_EDIT);
await prisma.catalogPages.delete({ where: { id } });
await rcon.updateCatalog();
await logStaffActivity({
@@ -47,7 +48,7 @@ export async function toggleCatalogPage({
id: number;
action: "toggleEnabled" | "toggleVisible";
}) {
await requireStaff();
await requirePermission(PERMS.CATALOG_EDIT);
const page = await prisma.catalogPages.findUnique({
where: { id },
select: { enabled: true, visible: true },
@@ -74,7 +75,7 @@ export async function createCatalogPage(input: {
minRank?: number;
orderNum?: number;
}): Promise<ActionResult<{ id: number }>> {
const staff = await requireStaff();
const staff = await requirePermission(PERMS.CATALOG_EDIT);
const created = await prisma.catalogPages.create({
data: {
caption: input.caption,
@@ -110,7 +111,7 @@ export async function reorderTreePage(input: {
newParentId?: number;
newOrderNum: number;
}) {
await requireStaff();
await requirePermission(PERMS.CATALOG_EDIT);
await prisma.catalogPages.update({
where: { id: input.pageId },
data: {
@@ -129,7 +130,7 @@ export async function deleteTreePage(input: {
pageId: number;
mode: "reparent" | "cascade";
}) {
await requireStaff();
await requirePermission(PERMS.CATALOG_EDIT);
if (input.mode === "cascade") {
const children = await prisma.catalogPages.findMany({
where: { parentId: input.pageId },
+3 -2
View File
@@ -1,6 +1,7 @@
"use server";
import { requireStaff } from "@/lib/admin/guard";
import { requirePermission } from "@/lib/admin/guard";
import { PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
export async function importBadgeFromRemote({
@@ -12,7 +13,7 @@ export async function importBadgeFromRemote({
name: string;
description: string;
}) {
await requireStaff();
await requirePermission(PERMS.ASSETS_IMPORT);
try {
await prisma.websiteBadges.upsert({
where: { badgeKey: code },
+3 -2
View File
@@ -1,6 +1,7 @@
"use server";
import { requireStaff } from "@/lib/admin/guard";
import { requirePermission } from "@/lib/admin/guard";
import { PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
export interface MultiAccountCluster {
@@ -22,7 +23,7 @@ export async function detectMultiAccounts({
minAccounts: number;
limit: number;
}) {
await requireStaff();
await requirePermission(PERMS.USERS_VIEW);
const clusters: MultiAccountCluster[] = [];
const ipGroups = await prisma.user.groupBy({
+8 -7
View File
@@ -1,13 +1,14 @@
"use server";
import { revalidatePath } from "next/cache";
import { requireStaff } from "@/lib/admin/guard";
import { requirePermission } from "@/lib/admin/guard";
import { PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { rcon } from "@/lib/services/rcon";
import { logStaffActivity } from "@/lib/services/staff-activity";
export async function updateRoomItem(payload: Record<string, unknown>) {
const staff = await requireStaff();
const staff = await requirePermission(PERMS.ROOMS_EDIT);
const { roomId, itemId, ...data } = payload as {
roomId: number;
itemId: number;
@@ -31,7 +32,7 @@ export async function bulkDeleteRoomItems({
roomId: number;
itemIds: number[];
}) {
const staff = await requireStaff();
const staff = await requirePermission(PERMS.ROOMS_EDIT);
await prisma.items.deleteMany({ where: { id: { in: itemIds }, roomId } });
await logStaffActivity({
staffId: staff.id,
@@ -49,7 +50,7 @@ export async function deleteRoomItem({
roomId: number;
itemId: number;
}) {
const staff = await requireStaff();
const staff = await requirePermission(PERMS.ROOMS_EDIT);
await prisma.items.delete({ where: { id: itemId } });
await logStaffActivity({
staffId: staff.id,
@@ -68,7 +69,7 @@ export async function roomRconAction({
roomId: number;
action: string;
}) {
await requireStaff();
await requirePermission(PERMS.ROOMS_EDIT);
if (action === "reload") {
await rcon.send("reloadroom", { room_id: roomId });
} else if (action === "kick") {
@@ -81,7 +82,7 @@ export async function roomRconAction({
}
export async function deleteRoom({ id }: { id: number }) {
const staff = await requireStaff();
const staff = await requirePermission(PERMS.ROOMS_DELETE);
await prisma.rooms.delete({ where: { id } });
await logStaffActivity({
staffId: staff.id,
@@ -103,7 +104,7 @@ export async function updateRoom({
state?: string;
usersMax?: number;
}) {
const staff = await requireStaff();
const staff = await requirePermission(PERMS.ROOMS_EDIT);
await prisma.rooms.update({ where: { id }, data: data as any });
await logStaffActivity({
staffId: staff.id,
+4 -3
View File
@@ -1,11 +1,12 @@
"use server";
import { revalidatePath } from "next/cache";
import { requireStaff } from "@/lib/admin/guard";
import { requirePermission } from "@/lib/admin/guard";
import { PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
export async function deleteSoundtrack({ id }: { id: number }) {
await requireStaff();
await requirePermission(PERMS.CATALOG_EDIT);
await prisma.soundtracks.delete({ where: { id } });
revalidatePath("/admin/sounds");
}
@@ -23,7 +24,7 @@ export async function updateSoundtrack({
track: string;
length: number;
}) {
await requireStaff();
await requirePermission(PERMS.CATALOG_EDIT);
await prisma.soundtracks.update({
where: { id },
data: { name, author, track, length },
+7
View File
@@ -2,6 +2,8 @@ import { Award } from "lucide-react";
import { getTranslations } from "next-intl/server";
import { StatusCard } from "@/components/admin/dashboard";
import { prisma } from "@/lib/prisma";
import { redirect } from "next/navigation";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
export const dynamic = "force-dynamic";
@@ -16,6 +18,11 @@ type Achievement = {
};
export default async function AdminAchievements() {
const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.CATALOG_VIEW, session.user.rank)) {
redirect("/admin");
}
const t = await getTranslations("pages.admin.achievements");
let achievements: Achievement[];
+7 -1
View File
@@ -1,8 +1,9 @@
import { Megaphone } from "lucide-react";
import Link from "next/link";
import { notFound } from "next/navigation";
import { notFound, redirect } from "next/navigation";
import { deleteAd, updateAd } from "@/actions/admin-ads";
import { prisma } from "@/lib/prisma";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
export const dynamic = "force-dynamic";
@@ -11,6 +12,11 @@ export default async function EditAd({
}: {
params: Promise<{ id: string }>;
}) {
const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.PAGES_VIEW, session.user.rank)) {
redirect("/admin");
}
const { id } = await params;
let ad: Awaited<ReturnType<typeof prisma.websiteAds.findUnique>> = null;
try {
+8 -1
View File
@@ -1,8 +1,15 @@
import { Megaphone } from "lucide-react";
import Link from "next/link";
import { redirect } from "next/navigation";
import { createAd } from "@/actions/admin-ads";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
export default async function NewAd() {
const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.PAGES_EDIT, session.user.rank)) {
redirect("/admin");
}
export default function NewAd() {
return (
<main>
<div className="flex items-center gap-3 mb-6">
+7
View File
@@ -3,10 +3,17 @@ import Link from "next/link";
import { getTranslations } from "next-intl/server";
import { StatusCard } from "@/components/admin/dashboard";
import { prisma } from "@/lib/prisma";
import { redirect } from "next/navigation";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
export const dynamic = "force-dynamic";
export default async function AdminAds() {
const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.PAGES_VIEW, session.user.rank)) {
redirect("/admin");
}
const t = await getTranslations("pages.admin.ads");
let ads: Awaited<ReturnType<typeof prisma.websiteAds.findMany>>;
try {
+7
View File
@@ -3,6 +3,8 @@ import { getTranslations } from "next-intl/server";
import { sendHotelAlert } from "@/actions/admin-alerts";
import { StatusCard } from "@/components/admin/dashboard";
import { prisma } from "@/lib/prisma";
import { redirect } from "next/navigation";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
export const dynamic = "force-dynamic";
@@ -54,6 +56,11 @@ function Badge({ label, color }: { label: string; color: string }) {
}
export default async function AdminAlerts() {
const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.NOTIFICATIONS_VIEW, session.user.rank)) {
redirect("/admin");
}
const t = await getTranslations("pages.admin.alerts");
let alerts: Awaited<ReturnType<typeof prisma.alertLogs.findMany>>;
+7
View File
@@ -4,6 +4,8 @@ import { getTranslations } from "next-intl/server";
import { dismissApplication } from "@/actions/admin-applications";
import { StatusCard } from "@/components/admin/dashboard";
import { prisma } from "@/lib/prisma";
import { redirect } from "next/navigation";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
export const dynamic = "force-dynamic";
@@ -20,6 +22,11 @@ function formatDate(d: Date | null): string {
}
export default async function AdminApplications() {
const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.USERS_VIEW, session.user.rank)) {
redirect("/admin");
}
const t = await getTranslations("pages.admin.applications");
let applications: ApplicationRow[];
try {
+7
View File
@@ -3,6 +3,8 @@ import { getTranslations } from "next-intl/server";
import { uploadBadge } from "@/actions/admin-badge-upload";
import { giveBadge } from "@/actions/admin-badges";
import { prisma } from "@/lib/prisma";
import { redirect } from "next/navigation";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
export const dynamic = "force-dynamic";
@@ -13,6 +15,11 @@ export default async function AdminBadges({
}: {
searchParams: Promise<{ uploaded?: string; error?: string }>;
}) {
const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.CATALOG_VIEW, session.user.rank)) {
redirect("/admin");
}
const t = await getTranslations("pages.admin.badges");
const { uploaded, error } = await searchParams;
+7 -1
View File
@@ -1,7 +1,8 @@
import Link from "next/link";
import { notFound } from "next/navigation";
import { notFound, redirect } from "next/navigation";
import { getTranslations } from "next-intl/server";
import { prisma } from "@/lib/prisma";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
export const dynamic = "force-dynamic";
@@ -40,6 +41,11 @@ export default async function AdminCalendarCampaign({
}: {
params: Promise<{ id: string }>;
}) {
const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.SHOP_VIEW, session.user.rank)) {
redirect("/admin");
}
const t = await getTranslations("pages.admin.calendar");
const { id } = await params;
+7
View File
@@ -3,6 +3,8 @@ import Link from "next/link";
import { getTranslations } from "next-intl/server";
import { StatusCard } from "@/components/admin/dashboard";
import { prisma } from "@/lib/prisma";
import { redirect } from "next/navigation";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
export const dynamic = "force-dynamic";
@@ -22,6 +24,11 @@ function fmtDate(ts: number): string {
}
export default async function AdminCalendar() {
const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.SHOP_VIEW, session.user.rank)) {
redirect("/admin");
}
const t = await getTranslations("pages.admin.calendar");
let campaigns: Campaign[] = [];
+7
View File
@@ -5,10 +5,17 @@ import {
updateEmailTemplate,
} from "@/actions/admin-email-templates";
import { prisma } from "@/lib/prisma";
import { redirect } from "next/navigation";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
export const dynamic = "force-dynamic";
export default async function AdminEmailTemplates() {
const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.PAGES_VIEW, session.user.rank)) {
redirect("/admin");
}
const t = await getTranslations("pages.admin.emailTemplates");
let templates: Awaited<ReturnType<typeof prisma.emailTemplates.findMany>> =
+7
View File
@@ -2,10 +2,17 @@ import { Image } from "lucide-react";
import { getTranslations } from "next-intl/server";
import { prisma } from "@/lib/prisma";
import { FaviconForm } from "./favicon-form";
import { redirect } from "next/navigation";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
export const dynamic = "force-dynamic";
export default async function AdminFaviconPage() {
const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.SETTINGS_VIEW, session.user.rank)) {
redirect("/admin");
}
const t = await getTranslations("pages.admin.favicon");
let faviconUrl: string | null = null;
+7 -1
View File
@@ -1,8 +1,9 @@
import { HelpCircle } from "lucide-react";
import Link from "next/link";
import { notFound } from "next/navigation";
import { notFound, redirect } from "next/navigation";
import { deleteHelpQuestion, updateHelpQuestion } from "@/actions/admin-help";
import { prisma } from "@/lib/prisma";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
export const dynamic = "force-dynamic";
@@ -13,6 +14,11 @@ export default async function EditHelpQuestion({
params: Promise<{ id: string }>;
searchParams: Promise<{ error?: string }>;
}) {
const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.PAGES_VIEW, session.user.rank)) {
redirect("/admin");
}
const { id } = await params;
const { error } = await searchParams;
let entry: Awaited<
@@ -1,12 +1,19 @@
import { HelpCircle } from "lucide-react";
import Link from "next/link";
import { createHelpQuestion } from "@/actions/admin-help";
import { redirect } from "next/navigation";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
export default async function NewHelpQuestion({
searchParams,
}: {
searchParams: Promise<{ error?: string }>;
}) {
const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.PAGES_EDIT, session.user.rank)) {
redirect("/admin");
}
const { error } = await searchParams;
return (
+7
View File
@@ -3,10 +3,17 @@ import Link from "next/link";
import { getTranslations } from "next-intl/server";
import { StatusCard } from "@/components/admin/dashboard";
import { prisma } from "@/lib/prisma";
import { redirect } from "next/navigation";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
export const dynamic = "force-dynamic";
export default async function AdminHelpQuestions() {
const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.PAGES_VIEW, session.user.rank)) {
redirect("/admin");
}
const t = await getTranslations("pages.admin.helpCenter");
let entries: Awaited<
ReturnType<typeof prisma.websiteHelpCenterCategories.findMany>
+7
View File
@@ -5,6 +5,8 @@ import {
upsertPermission,
} from "@/actions/admin-housekeeping";
import { prisma } from "@/lib/prisma";
import { redirect } from "next/navigation";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
export const dynamic = "force-dynamic";
@@ -13,6 +15,11 @@ export default async function AdminHousekeeping({
}: {
searchParams: Promise<{ q?: string; mode?: "manage" | "import" }>;
}) {
const { session, permissions: acl } = await getAdminContext();
if (!canAccess(acl, PERMS.SETTINGS_VIEW, session.user.rank)) {
redirect("/admin");
}
const t = await getTranslations("pages.admin.housekeeping");
const sp = await searchParams;
+7
View File
@@ -8,6 +8,8 @@ import {
} from "@/actions/admin-ip";
import { StatusCard } from "@/components/admin/dashboard";
import { prisma } from "@/lib/prisma";
import { redirect } from "next/navigation";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
export const dynamic = "force-dynamic";
@@ -23,6 +25,11 @@ function formatDate(d: Date | null): string {
}
export default async function AdminIp() {
const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.SETTINGS_VIEW, session.user.rank)) {
redirect("/admin");
}
const t = await getTranslations("pages.admin.ip");
let whitelist: IpRow[] = [];
let blacklist: IpRow[] = [];
+7
View File
@@ -3,6 +3,8 @@ import { getTranslations } from "next-intl/server";
import { StatusCard } from "@/components/admin/dashboard";
import { buildStaffActivityWhere } from "@/lib/admin/log-filters";
import { prisma } from "@/lib/prisma";
import { redirect } from "next/navigation";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
export const dynamic = "force-dynamic";
@@ -23,6 +25,11 @@ export default async function AdminStaffActivities({
authorization?: string;
}>;
}) {
const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.LOGS_VIEW, session.user.rank)) {
redirect("/admin");
}
const t = await getTranslations("pages.admin.logs");
const sp = await searchParams;
+7
View File
@@ -3,6 +3,8 @@ import { getTranslations } from "next-intl/server";
import { saveMaintenance } from "@/actions/admin-maintenance";
import { StatusCard } from "@/components/admin/dashboard";
import { prisma } from "@/lib/prisma";
import { redirect } from "next/navigation";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
export const dynamic = "force-dynamic";
@@ -13,6 +15,11 @@ const KEYS = [
] as const;
export default async function AdminMaintenance() {
const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.SETTINGS_VIEW, session.user.rank)) {
redirect("/admin");
}
const t = await getTranslations("pages.admin.maintenance");
let rows: { key: string; value: string }[] = [];
+7
View File
@@ -2,8 +2,15 @@ import { Image } from "lucide-react";
import Link from "next/link";
import { getTranslations } from "next-intl/server";
import { AdminMediaGrid } from "@/components/admin/media-grid";
import { redirect } from "next/navigation";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
export default async function AdminMediaPage() {
const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.PAGES_VIEW, session.user.rank)) {
redirect("/admin");
}
const t = await getTranslations("pages.admin.media");
return (
+7
View File
@@ -2,6 +2,8 @@ import { Navigation } from "lucide-react";
import { getTranslations } from "next-intl/server";
import { StatusCard } from "@/components/admin/dashboard";
import { prisma } from "@/lib/prisma";
import { redirect } from "next/navigation";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
export const dynamic = "force-dynamic";
@@ -40,6 +42,11 @@ function YesNo({ on }: { on: boolean }) {
}
export default async function AdminNavigation() {
const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.PAGES_VIEW, session.user.rank)) {
redirect("/admin");
}
const t = await getTranslations("pages.admin.navigation");
let flatcats: Flatcat[] = [];
try {
+7
View File
@@ -3,10 +3,17 @@ import { getTranslations } from "next-intl/server";
import { deletePhoto } from "@/actions/admin-photos";
import { StatusCard } from "@/components/admin/dashboard";
import { prisma } from "@/lib/prisma";
import { redirect } from "next/navigation";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
export const dynamic = "force-dynamic";
export default async function AdminPhotos() {
const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.PAGES_VIEW, session.user.rank)) {
redirect("/admin");
}
const t = await getTranslations("pages.admin.photos");
let photos: Awaited<ReturnType<typeof prisma.cameraWeb.findMany>> = [];
try {
+7
View File
@@ -8,12 +8,19 @@ import {
} from "@/actions/admin-rare-values";
import { StatusCard } from "@/components/admin/dashboard";
import { prisma } from "@/lib/prisma";
import { redirect } from "next/navigation";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
export const dynamic = "force-dynamic";
const CURRENCY_TYPES = ["diamonds", "credits", "duckets", "points"];
export default async function AdminRareValues() {
const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.SHOP_VIEW, session.user.rank)) {
redirect("/admin");
}
const t = await getTranslations("pages.admin.rareValues");
let categories: Awaited<
+1 -1
View File
@@ -84,7 +84,7 @@ export default async function RoomFurniPage({
ownerUsername: room.owner?.username ?? room.ownerName,
}}
items={rows}
canEdit={session.user.rank >= 4}
canEdit={canAccess(permissions, PERMS.ROOMS_EDIT, session.user.rank)}
/>
</div>
);
+1 -4
View File
@@ -15,10 +15,7 @@ export default async function RoomEditPage({
}) {
const { id } = await params;
const { session, permissions } = await getAdminContext();
if (
!canAccess(permissions, PERMS.ROOMS_VIEW, session.user.rank) ||
session.user.rank < 4
) {
if (!canAccess(permissions, PERMS.ROOMS_EDIT, session.user.rank)) {
redirect("/admin");
}
+1 -3
View File
@@ -30,9 +30,7 @@ export default async function RoomsPage({
searchParams,
);
const canEdit =
canAccess(result.permissions, PERMS.ROOMS_VIEW, result.rank) &&
result.rank >= 4;
const canEdit = canAccess(result.permissions, PERMS.ROOMS_EDIT, result.rank);
return (
<div className="space-y-6">
+1 -1
View File
@@ -29,7 +29,7 @@ export default async function RoomShowPage({
redirect("/admin");
const room = await loadRoom(id);
const canEdit = session.user.rank >= 4;
const canEdit = canAccess(permissions, PERMS.ROOMS_EDIT, session.user.rank);
return (
<div className="space-y-6">
+7
View File
@@ -2,6 +2,8 @@ import { CreditCard } from "lucide-react";
import { getTranslations } from "next-intl/server";
import { StatusCard } from "@/components/admin/dashboard";
import { prisma } from "@/lib/prisma";
import { redirect } from "next/navigation";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
export const dynamic = "force-dynamic";
@@ -17,6 +19,11 @@ function fmt(ts: number): string {
}
export default async function AdminSubscriptions() {
const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.SHOP_VIEW, session.user.rank)) {
redirect("/admin");
}
const t = await getTranslations("pages.admin.subscriptions");
const now = Math.floor(Date.now() / 1000);
+7
View File
@@ -3,6 +3,8 @@ import { getTranslations } from "next-intl/server";
import { createTag, deleteTag, updateTag } from "@/actions/admin-tags";
import { StatusCard } from "@/components/admin/dashboard";
import { prisma } from "@/lib/prisma";
import { redirect } from "next/navigation";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
export const dynamic = "force-dynamic";
@@ -18,6 +20,11 @@ function formatDate(d: Date | null): string {
}
export default async function AdminTagsPage() {
const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.PAGES_VIEW, session.user.rank)) {
redirect("/admin");
}
const t = await getTranslations("pages.admin.tags");
let tags: Tag[] = [];
let usage = new Map<string, number>();
+7
View File
@@ -3,6 +3,8 @@ import { getTranslations } from "next-intl/server";
import { createTeam, deleteTeam } from "@/actions/admin-teams";
import { StatusCard } from "@/components/admin/dashboard";
import { prisma } from "@/lib/prisma";
import { redirect } from "next/navigation";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
export const dynamic = "force-dynamic";
@@ -16,6 +18,11 @@ type TeamRow = {
};
export default async function AdminTeams() {
const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.USERS_VIEW, session.user.rank)) {
redirect("/admin");
}
const t = await getTranslations("pages.admin.teams");
let teams: TeamRow[] = [];
try {
+6 -2
View File
@@ -2,8 +2,9 @@ import { ArrowLeftRight } from "lucide-react";
import Link from "next/link";
import { getTranslations } from "next-intl/server";
import { StatusCard } from "@/components/admin/dashboard";
import { requireStaff } from "@/lib/admin/guard";
import { prisma } from "@/lib/prisma";
import { redirect } from "next/navigation";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
export const dynamic = "force-dynamic";
@@ -47,7 +48,10 @@ export default async function AdminTransactions({
}: {
searchParams: Promise<{ page?: string }>;
}) {
await requireStaff();
const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.SHOP_VIEW, session.user.rank)) {
redirect("/admin");
}
const t = await getTranslations("pages.admin.transactions");
+7
View File
@@ -3,6 +3,8 @@ import { getTranslations } from "next-intl/server";
import { createVoucher, deleteVoucher } from "@/actions/admin-vouchers";
import { StatusCard } from "@/components/admin/dashboard";
import { prisma } from "@/lib/prisma";
import { redirect } from "next/navigation";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
export const dynamic = "force-dynamic";
@@ -22,6 +24,11 @@ function fmtDate(d: Date | null): string {
}
export default async function AdminVouchers() {
const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.SHOP_VIEW, session.user.rank)) {
redirect("/admin");
}
const t = await getTranslations("pages.admin.vouchers");
const now = Date.now();
+7
View File
@@ -4,6 +4,8 @@ import { getTranslations } from "next-intl/server";
import { saveVpn } from "@/actions/admin-vpn";
import { StatusCard } from "@/components/admin/dashboard";
import { siteSettings } from "@/lib/services/site-settings";
import { redirect } from "next/navigation";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
export const dynamic = "force-dynamic";
export const metadata = { title: "VPN" };
@@ -19,6 +21,11 @@ export default async function AdminVpn({
}: {
searchParams: Promise<{ saved?: string }>;
}) {
const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.SETTINGS_VIEW, session.user.rank)) {
redirect("/admin");
}
const sp = await searchParams;
const t = await getTranslations("pages.admin.vpn");
+7
View File
@@ -8,12 +8,19 @@ import {
} from "@/actions/admin-writeable-boxes";
import { StatusCard } from "@/components/admin/dashboard";
import { prisma } from "@/lib/prisma";
import { redirect } from "next/navigation";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
export const dynamic = "force-dynamic";
export const metadata = { title: "Writeable boxes" };
export default async function AdminWriteableBoxes() {
const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.PAGES_VIEW, session.user.rank)) {
redirect("/admin");
}
const t = await getTranslations("pages.admin.writeableBoxes");
const boxes = await prisma.websiteWriteableBoxes
.findMany({
+73 -37
View File
@@ -1,21 +1,14 @@
import { existsSync, readFileSync } from "node:fs";
import { existsSync, readFileSync, readdirSync } from "node:fs";
import { join, relative } from "node:path";
import { describe, expect, it } from "vitest";
const ROUTES: Array<[string, string]> = [
["moderation", "PERMS.MODERATION_VIEW"],
["moderation/actions", "PERMS.MODERATION_EDIT"],
["moderation/cfh", "PERMS.MODERATION_VIEW"],
["moderation/cfh/[id]", "PERMS.MODERATION_VIEW"],
["moderation/team", "PERMS.MODERATION_VIEW"],
["logs/audit", "PERMS.LOGS_VIEW"],
["logs/chat", "PERMS.LOGS_VIEW"],
["logs/commands", "PERMS.LOGS_VIEW"],
["logs/trades", "PERMS.LOGS_VIEW"],
["analytics", "PERMS.ANALYTICS_VIEW"],
["analytics/activity", "PERMS.ANALYTICS_VIEW"],
["analytics/economy", "PERMS.ANALYTICS_VIEW"],
["devops", "PERMS.DEVOPS_VIEW"],
["devops/errors", "PERMS.DEVOPS_VIEW"],
["online", "PERMS.USERS_VIEW"],
["commandocentrum", "PERMS.RCON_EXECUTE"],
["users/edit/[id]", "PERMS.USERS_EDIT"],
@@ -26,6 +19,43 @@ const ROUTES: Array<[string, string]> = [
["wordfilter", "PERMS.WORDFILTER_VIEW"],
["articles", "PERMS.NEWS_VIEW"],
["shop", "PERMS.SHOP_VIEW"],
["transactions", "PERMS.SHOP_VIEW"],
["vouchers", "PERMS.SHOP_VIEW"],
["vpn", "PERMS.SETTINGS_VIEW"],
["ip", "PERMS.SETTINGS_VIEW"],
["maintenance", "PERMS.SETTINGS_VIEW"],
["alerts", "PERMS.NOTIFICATIONS_VIEW"],
["tags", "PERMS.PAGES_VIEW"],
["ads", "PERMS.PAGES_VIEW"],
["media", "PERMS.PAGES_VIEW"],
["photos", "PERMS.PAGES_VIEW"],
["badges", "PERMS.CATALOG_VIEW"],
["teams", "PERMS.USERS_VIEW"],
["applications", "PERMS.USERS_VIEW"],
["logs", "PERMS.LOGS_VIEW"],
["catalog", "PERMS.CATALOG_VIEW"],
["rooms/edit/[id]", "PERMS.ROOMS_EDIT"],
];
const ACTION_GATES: Array<[string, string]> = [
["admin-settings.ts", "PERMS.SETTINGS_EDIT"],
["admin-theme.ts", "PERMS.SETTINGS_EDIT"],
["admin-emulator.ts", "PERMS.SETTINGS_EDIT"],
["admin-bans.ts", "PERMS.USERS_BAN"],
["admin-wordfilter.ts", "PERMS.WORDFILTER_EDIT"],
["admin-articles.ts", "PERMS.NEWS_EDIT"],
["admin-shop.ts", "PERMS.SHOP_EDIT"],
["admin-radio-autodj.ts", "PERMS.RADIO_EDIT"],
["catalog.ts", "PERMS.CATALOG_EDIT"],
["rooms.ts", "PERMS.ROOMS_EDIT"],
["admin-users.ts", "PERMS.USERS_EDIT"],
["admin-vpn.ts", "PERMS.SETTINGS_EDIT"],
["admin-ads.ts", "PERMS.PAGES_EDIT"],
["admin-vouchers.ts", "PERMS.SHOP_EDIT"],
["admin-alerts.ts", "PERMS.NOTIFICATIONS_EDIT"],
["admin-permissions.ts", "PERMS.PERMISSIONS_MANAGE"],
["commandocentrum.ts", "PERMS.RCON_EXECUTE"],
["translations.ts", "PERMS.SETTINGS_EDIT"],
];
describe("admin operations route contract", () => {
@@ -51,36 +81,42 @@ describe("admin operations route contract", () => {
expect(readFileSync(path, "utf8"), path).toContain(permission);
});
it("guards moderation mutations separately from page navigation", () => {
const source = readFileSync("src/actions/moderation.ts", "utf8");
expect(source).toContain("PERMS.MODERATION_EDIT");
expect(source).toContain("adminAction");
});
it("guards translation writes with SETTINGS_EDIT", () => {
const source = readFileSync("src/actions/translations.ts", "utf8");
expect(source).toContain("PERMS.SETTINGS_EDIT");
expect(source).not.toContain("rank < 7");
});
it("guards commandocentrum mutations with RCON_EXECUTE", () => {
const source = readFileSync("src/actions/commandocentrum.ts", "utf8");
expect(source).toContain("PERMS.RCON_EXECUTE");
expect(source).not.toContain("requireStaff()");
});
it.each([
["admin-settings.ts", "PERMS.SETTINGS_EDIT"],
["admin-theme.ts", "PERMS.SETTINGS_EDIT"],
["admin-emulator.ts", "PERMS.SETTINGS_EDIT"],
["admin-bans.ts", "PERMS.USERS_BAN"],
["admin-wordfilter.ts", "PERMS.WORDFILTER_EDIT"],
["admin-articles.ts", "PERMS.NEWS_EDIT"],
["admin-shop.ts", "PERMS.SHOP_EDIT"],
["admin-radio-autodj.ts", "PERMS.RADIO_EDIT"],
])("guards %s mutations with %s", (file, permission) => {
it.each(ACTION_GATES)("guards %s with %s", (file, permission) => {
const source = readFileSync(`src/actions/${file}`, "utf8");
expect(source).toContain(permission);
expect(source).not.toMatch(/await requireStaff\(\)/);
expect(source).not.toMatch(/await requireStaffRateLimited\(\)/);
});
it("has no requireStaff left in admin action modules", () => {
const dir = "src/actions";
const offenders: string[] = [];
for (const name of readdirSync(dir)) {
if (!name.endsWith(".ts") || name.endsWith(".test.ts")) continue;
const source = readFileSync(join(dir, name), "utf8");
if (/await requireStaff(RateLimited)?\(\)/.test(source)) {
offenders.push(name);
}
}
expect(offenders).toEqual([]);
});
it("has no requireStaff left in admin pages", () => {
const root = "src/app/admin";
const offenders: string[] = [];
function walk(dir: string) {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const full = join(dir, entry.name);
if (entry.isDirectory()) walk(full);
else if (entry.name === "page.tsx") {
const source = readFileSync(full, "utf8");
if (/await requireStaff\(/.test(source)) {
offenders.push(relative(process.cwd(), full).replaceAll("\\", "/"));
}
}
}
}
walk(root);
expect(offenders).toEqual([]);
});
});