diff --git a/src/actions/admin-ads.ts b/src/actions/admin-ads.ts index 0e380313..b369e93b 100644 --- a/src/actions/admin-ads.ts +++ b/src/actions/admin-ads.ts @@ -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 { - 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 { } export async function updateAd(formData: FormData): Promise { - 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 { } export async function deleteAd(formData: FormData): Promise { - const staff = await requireStaff(); + const staff = await requirePermission(PERMS.PAGES_EDIT); const id = formPositiveBigInt(formData, "id"); if (!id) return; diff --git a/src/actions/admin-alerts.ts b/src/actions/admin-alerts.ts index 787e3e40..f2eaa14e 100644 --- a/src/actions/admin-alerts.ts +++ b/src/actions/admin-alerts.ts @@ -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 { - await requireStaff(); + await requirePermission(PERMS.NOTIFICATIONS_EDIT); const message = String(formData.get("message") ?? "") .normalize("NFC") diff --git a/src/actions/admin-applications.ts b/src/actions/admin-applications.ts index ad560890..1088fe35 100644 --- a/src/actions/admin-applications.ts +++ b/src/actions/admin-applications.ts @@ -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 { - await requireStaff(); + await requirePermission(PERMS.USERS_EDIT); const id = formPositiveBigInt(formData, "id"); if (!id) return; diff --git a/src/actions/admin-badge-upload.ts b/src/actions/admin-badge-upload.ts index f8d48d02..e93f335f 100644 --- a/src/actions/admin-badge-upload.ts +++ b/src/actions/admin-badge-upload.ts @@ -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 { - const staff = await requireStaff(); + const staff = await requirePermission(PERMS.CATALOG_EDIT); const dir = process.env.BADGE_UPLOAD_DIR; if (!dir) { diff --git a/src/actions/admin-badges.ts b/src/actions/admin-badges.ts index 608d1b0e..1835b6c2 100644 --- a/src/actions/admin-badges.ts +++ b/src/actions/admin-badges.ts @@ -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 { - await requireStaff(); + await requirePermission(PERMS.CATALOG_EDIT); const userId = Number(formData.get("userId")); const code = String(formData.get("code") ?? "") diff --git a/src/actions/admin-email-templates.ts b/src/actions/admin-email-templates.ts index d73521f7..f1e98d46 100644 --- a/src/actions/admin-email-templates.ts +++ b/src/actions/admin-email-templates.ts @@ -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 { - 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 { } export async function updateEmailTemplate(formData: FormData): Promise { - 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 { } export async function deleteEmailTemplate(formData: FormData): Promise { - await requireStaff(); + await requirePermission(PERMS.PAGES_EDIT); const id = formPositiveBigInt(formData, "id"); if (!id) return; await prisma.emailTemplates.delete({ where: { id } }); diff --git a/src/actions/admin-help.ts b/src/actions/admin-help.ts index 1cf277a2..f703f79a 100644 --- a/src/actions/admin-help.ts +++ b/src/actions/admin-help.ts @@ -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 { - 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 { } export async function updateHelpQuestion(formData: FormData): Promise { - 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 { } export async function deleteHelpQuestion(formData: FormData): Promise { - const staff = await requireStaff(); + const staff = await requirePermission(PERMS.PAGES_EDIT); const id = formPositiveBigInt(formData, "id"); if (!id) return; diff --git a/src/actions/admin-housekeeping.ts b/src/actions/admin-housekeeping.ts index 3b3068cc..277451c0 100644 --- a/src/actions/admin-housekeeping.ts +++ b/src/actions/admin-housekeeping.ts @@ -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 { - 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 { } export async function deletePermission(formData: FormData): Promise { - await requireStaff(); + await requirePermission(PERMS.SETTINGS_EDIT); const raw = String(formData.get("id") ?? "").normalize("NFC"); if (!raw) return; diff --git a/src/actions/admin-ip.ts b/src/actions/admin-ip.ts index 8900f54a..797b101d 100644 --- a/src/actions/admin-ip.ts +++ b/src/actions/admin-ip.ts @@ -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 { - 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 { } export async function deleteWhitelist(formData: FormData): Promise { - 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 { } export async function addBlacklist(formData: FormData): Promise { - 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 { } export async function deleteBlacklist(formData: FormData): Promise { - await requireStaff(); + await requirePermission(PERMS.SETTINGS_EDIT); const raw = String(formData.get("id") ?? "") .normalize("NFC") .trim(); diff --git a/src/actions/admin-maintenance.ts b/src/actions/admin-maintenance.ts index 2c7d6d4b..5636c55b 100644 --- a/src/actions/admin-maintenance.ts +++ b/src/actions/admin-maintenance.ts @@ -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 { } export async function saveMaintenance(formData: FormData): Promise { - await requireStaff(); + await requirePermission(PERMS.SETTINGS_EDIT); // Checkbox: present only when ticked. Normalise to the '1'/'0' string the // emulator/Laravel side expects. diff --git a/src/actions/admin-media.ts b/src/actions/admin-media.ts index 6487af56..1d7982b9 100644 --- a/src/actions/admin-media.ts +++ b/src/actions/admin-media.ts @@ -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 { - 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 { } export async function deleteMedia(name: string): Promise { - 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 { export async function uploadMediaAndReturn( formData: FormData, ): Promise { - 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 ""; diff --git a/src/actions/admin-permissions.ts b/src/actions/admin-permissions.ts index 1a716c72..8b5d1521 100644 --- a/src/actions/admin-permissions.ts +++ b/src/actions/admin-permissions.ts @@ -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 { - 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 { } export async function updatePermission(formData: FormData): Promise { - 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 { } export async function deletePermission(formData: FormData): Promise { - const staff = await requireStaff(); + const staff = await requirePermission(PERMS.PERMISSIONS_MANAGE); const id = formPositiveBigInt(formData, "id"); if (!id) return; diff --git a/src/actions/admin-photos.ts b/src/actions/admin-photos.ts index ea6ba0e7..1b7e5773 100644 --- a/src/actions/admin-photos.ts +++ b/src/actions/admin-photos.ts @@ -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 { - await requireStaff(); + await requirePermission(PERMS.PAGES_EDIT); const id = Number(formData.get("id")); if (!(id > 0)) return; diff --git a/src/actions/admin-rare-values.ts b/src/actions/admin-rare-values.ts index b244db58..6c9e35b4 100644 --- a/src/actions/admin-rare-values.ts +++ b/src/actions/admin-rare-values.ts @@ -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 { - 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 { } export async function deleteCategory(formData: FormData): Promise { - 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 { } export async function createValue(formData: FormData): Promise { - 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 { } export async function deleteValue(formData: FormData): Promise { - await requireStaff(); + await requirePermission(PERMS.SHOP_EDIT); const id = formPositiveBigInt(formData, "id"); if (!id) return; diff --git a/src/actions/admin-tags.ts b/src/actions/admin-tags.ts index 619e0b0a..7d08f355 100644 --- a/src/actions/admin-tags.ts +++ b/src/actions/admin-tags.ts @@ -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 { - 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 { } export async function updateTag(formData: FormData): Promise { - 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 { } export async function deleteTag(formData: FormData): Promise { - const staff = await requireStaff(); + const staff = await requirePermission(PERMS.PAGES_EDIT); const id = parseId(formData.get("id")); if (id === null) return; diff --git a/src/actions/admin-teams.ts b/src/actions/admin-teams.ts index 022e03f4..3e47c340 100644 --- a/src/actions/admin-teams.ts +++ b/src/actions/admin-teams.ts @@ -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 { - 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 { } export async function deleteTeam(formData: FormData): Promise { - await requireStaff(); + await requirePermission(PERMS.USERS_EDIT); const id = BigInt(String(formData.get("id"))); await prisma.websiteTeams.delete({ where: { id } }); diff --git a/src/actions/admin-users.ts b/src/actions/admin-users.ts index bf3baf60..1b81c803 100644 --- a/src/actions/admin-users.ts +++ b/src/actions/admin-users.ts @@ -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 = new Set([ ]); export async function giveCurrency(formData: FormData): Promise { - 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 { } export async function setMotto(formData: FormData): Promise { - 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 { } export async function setRank(formData: FormData): Promise { - 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 { } export async function alertUser(formData: FormData): Promise { - 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 { } export async function disconnectUser(formData: FormData): Promise { - 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); diff --git a/src/actions/admin-vouchers.ts b/src/actions/admin-vouchers.ts index aec1794a..f6c5ddb3 100644 --- a/src/actions/admin-vouchers.ts +++ b/src/actions/admin-vouchers.ts @@ -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 { - 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 { } export async function deleteVoucher(formData: FormData): Promise { - await requireStaff(); + await requirePermission(PERMS.SHOP_EDIT); const id = formPositiveBigInt(formData, "id"); if (!id) return; diff --git a/src/actions/admin-vpn.ts b/src/actions/admin-vpn.ts index 93e4caae..2385c0e5 100644 --- a/src/actions/admin-vpn.ts +++ b/src/actions/admin-vpn.ts @@ -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 { - const staff = await requireStaff(); + const staff = await requirePermission(PERMS.SETTINGS_EDIT); // Toggle: an unchecked checkbox submits nothing, so absence === disabled. const enabled = diff --git a/src/actions/admin-writeable-boxes.ts b/src/actions/admin-writeable-boxes.ts index 178fc77b..95b14eda 100644 --- a/src/actions/admin-writeable-boxes.ts +++ b/src/actions/admin-writeable-boxes.ts @@ -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 { - 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 { } export async function updateBox(formData: FormData): Promise { - 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 { } export async function deleteBox(formData: FormData): Promise { - 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 { } export async function toggleBox(formData: FormData): Promise { - const staff = await requireStaff(); + const staff = await requirePermission(PERMS.PAGES_EDIT); const id = parseId(formData); if (id == null) return; diff --git a/src/actions/badges.ts b/src/actions/badges.ts index 9a6c90dc..d1dfdc68 100644 --- a/src/actions/badges.ts +++ b/src/actions/badges.ts @@ -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() }, diff --git a/src/actions/bulk-users.ts b/src/actions/bulk-users.ts index 92739068..a6c99215 100644 --- a/src/actions/bulk-users.ts +++ b/src/actions/bulk-users.ts @@ -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> { - 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> { - 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 }> = []; diff --git a/src/actions/catalog-bc.ts b/src/actions/catalog-bc.ts index 719280ac..6d714e5f 100644 --- a/src/actions/catalog-bc.ts +++ b/src/actions/catalog-bc.ts @@ -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) { - 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, diff --git a/src/actions/catalog-items.ts b/src/actions/catalog-items.ts index b6d2bfef..0420d85d 100644 --- a/src/actions/catalog-items.ts +++ b/src/actions/catalog-items.ts @@ -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; baseItem?: { id: number; fields: Record }; }) { - 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; diff --git a/src/actions/catalog.ts b/src/actions/catalog.ts index 26d62c23..e2a20f35 100644 --- a/src/actions/catalog.ts +++ b/src/actions/catalog.ts @@ -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): Promise { - 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> { - 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 }, diff --git a/src/actions/import-badges.ts b/src/actions/import-badges.ts index 40e91de0..dac0ed66 100644 --- a/src/actions/import-badges.ts +++ b/src/actions/import-badges.ts @@ -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 }, diff --git a/src/actions/multi-account-detect.ts b/src/actions/multi-account-detect.ts index 6e676d81..5562ae40 100644 --- a/src/actions/multi-account-detect.ts +++ b/src/actions/multi-account-detect.ts @@ -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({ diff --git a/src/actions/rooms.ts b/src/actions/rooms.ts index 298d6aa7..8e44d43a 100644 --- a/src/actions/rooms.ts +++ b/src/actions/rooms.ts @@ -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) { - 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, diff --git a/src/actions/soundtracks.ts b/src/actions/soundtracks.ts index 3d3a12d7..97a489de 100644 --- a/src/actions/soundtracks.ts +++ b/src/actions/soundtracks.ts @@ -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 }, diff --git a/src/app/admin/achievements/page.tsx b/src/app/admin/achievements/page.tsx index b30279c3..f579d8e6 100644 --- a/src/app/admin/achievements/page.tsx +++ b/src/app/admin/achievements/page.tsx @@ -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[]; diff --git a/src/app/admin/ads/[id]/page.tsx b/src/app/admin/ads/[id]/page.tsx index c79cd6be..1c1afc8f 100644 --- a/src/app/admin/ads/[id]/page.tsx +++ b/src/app/admin/ads/[id]/page.tsx @@ -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> = null; try { diff --git a/src/app/admin/ads/new/page.tsx b/src/app/admin/ads/new/page.tsx index 3004d84c..1cacdd77 100644 --- a/src/app/admin/ads/new/page.tsx +++ b/src/app/admin/ads/new/page.tsx @@ -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 (
diff --git a/src/app/admin/ads/page.tsx b/src/app/admin/ads/page.tsx index fc652ce4..73b57e62 100644 --- a/src/app/admin/ads/page.tsx +++ b/src/app/admin/ads/page.tsx @@ -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>; try { diff --git a/src/app/admin/alerts/page.tsx b/src/app/admin/alerts/page.tsx index 92a4701d..e27cd2ff 100644 --- a/src/app/admin/alerts/page.tsx +++ b/src/app/admin/alerts/page.tsx @@ -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>; diff --git a/src/app/admin/applications/page.tsx b/src/app/admin/applications/page.tsx index a3229460..1040291a 100644 --- a/src/app/admin/applications/page.tsx +++ b/src/app/admin/applications/page.tsx @@ -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 { diff --git a/src/app/admin/badges/page.tsx b/src/app/admin/badges/page.tsx index 99881216..6e223921 100644 --- a/src/app/admin/badges/page.tsx +++ b/src/app/admin/badges/page.tsx @@ -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; diff --git a/src/app/admin/calendar/[id]/page.tsx b/src/app/admin/calendar/[id]/page.tsx index c9939ef4..f094eacf 100644 --- a/src/app/admin/calendar/[id]/page.tsx +++ b/src/app/admin/calendar/[id]/page.tsx @@ -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; diff --git a/src/app/admin/calendar/page.tsx b/src/app/admin/calendar/page.tsx index 52788548..351e8f07 100644 --- a/src/app/admin/calendar/page.tsx +++ b/src/app/admin/calendar/page.tsx @@ -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[] = []; diff --git a/src/app/admin/email-templates/page.tsx b/src/app/admin/email-templates/page.tsx index 57469886..2408058d 100644 --- a/src/app/admin/email-templates/page.tsx +++ b/src/app/admin/email-templates/page.tsx @@ -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> = diff --git a/src/app/admin/favicon/page.tsx b/src/app/admin/favicon/page.tsx index fed13996..8fe024d3 100644 --- a/src/app/admin/favicon/page.tsx +++ b/src/app/admin/favicon/page.tsx @@ -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; diff --git a/src/app/admin/help-questions/[id]/page.tsx b/src/app/admin/help-questions/[id]/page.tsx index fcca6d9c..f53a2c4c 100644 --- a/src/app/admin/help-questions/[id]/page.tsx +++ b/src/app/admin/help-questions/[id]/page.tsx @@ -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< diff --git a/src/app/admin/help-questions/new/page.tsx b/src/app/admin/help-questions/new/page.tsx index f97129b7..708bcdd0 100644 --- a/src/app/admin/help-questions/new/page.tsx +++ b/src/app/admin/help-questions/new/page.tsx @@ -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 ( diff --git a/src/app/admin/help-questions/page.tsx b/src/app/admin/help-questions/page.tsx index b9c92c57..f20a05a0 100644 --- a/src/app/admin/help-questions/page.tsx +++ b/src/app/admin/help-questions/page.tsx @@ -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 diff --git a/src/app/admin/housekeeping/page.tsx b/src/app/admin/housekeeping/page.tsx index 2562acef..4d361756 100644 --- a/src/app/admin/housekeeping/page.tsx +++ b/src/app/admin/housekeeping/page.tsx @@ -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; diff --git a/src/app/admin/ip/page.tsx b/src/app/admin/ip/page.tsx index 7241c6aa..caa762ea 100644 --- a/src/app/admin/ip/page.tsx +++ b/src/app/admin/ip/page.tsx @@ -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[] = []; diff --git a/src/app/admin/logs/page.tsx b/src/app/admin/logs/page.tsx index a716fcb9..b0914827 100644 --- a/src/app/admin/logs/page.tsx +++ b/src/app/admin/logs/page.tsx @@ -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; diff --git a/src/app/admin/maintenance/page.tsx b/src/app/admin/maintenance/page.tsx index d73f877b..5157ce14 100644 --- a/src/app/admin/maintenance/page.tsx +++ b/src/app/admin/maintenance/page.tsx @@ -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 }[] = []; diff --git a/src/app/admin/media/page.tsx b/src/app/admin/media/page.tsx index d5ecef60..949a1ca8 100644 --- a/src/app/admin/media/page.tsx +++ b/src/app/admin/media/page.tsx @@ -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 ( diff --git a/src/app/admin/navigation/page.tsx b/src/app/admin/navigation/page.tsx index 28496415..caafa997 100644 --- a/src/app/admin/navigation/page.tsx +++ b/src/app/admin/navigation/page.tsx @@ -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 { diff --git a/src/app/admin/photos/page.tsx b/src/app/admin/photos/page.tsx index 63c5e319..6da662a7 100644 --- a/src/app/admin/photos/page.tsx +++ b/src/app/admin/photos/page.tsx @@ -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> = []; try { diff --git a/src/app/admin/rare-values/page.tsx b/src/app/admin/rare-values/page.tsx index f6070770..40011fe4 100644 --- a/src/app/admin/rare-values/page.tsx +++ b/src/app/admin/rare-values/page.tsx @@ -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< diff --git a/src/app/admin/rooms/[id]/furni/page.tsx b/src/app/admin/rooms/[id]/furni/page.tsx index ee193a93..a9411e13 100644 --- a/src/app/admin/rooms/[id]/furni/page.tsx +++ b/src/app/admin/rooms/[id]/furni/page.tsx @@ -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)} />
); diff --git a/src/app/admin/rooms/edit/[id]/page.tsx b/src/app/admin/rooms/edit/[id]/page.tsx index 8cf1cb66..c02b7490 100644 --- a/src/app/admin/rooms/edit/[id]/page.tsx +++ b/src/app/admin/rooms/edit/[id]/page.tsx @@ -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"); } diff --git a/src/app/admin/rooms/page.tsx b/src/app/admin/rooms/page.tsx index 5839ba63..91a6ae00 100644 --- a/src/app/admin/rooms/page.tsx +++ b/src/app/admin/rooms/page.tsx @@ -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 (
diff --git a/src/app/admin/rooms/show/[id]/page.tsx b/src/app/admin/rooms/show/[id]/page.tsx index 62345bb6..e6eab070 100644 --- a/src/app/admin/rooms/show/[id]/page.tsx +++ b/src/app/admin/rooms/show/[id]/page.tsx @@ -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 (
diff --git a/src/app/admin/subscriptions/page.tsx b/src/app/admin/subscriptions/page.tsx index c49b14e1..0006d83d 100644 --- a/src/app/admin/subscriptions/page.tsx +++ b/src/app/admin/subscriptions/page.tsx @@ -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); diff --git a/src/app/admin/tags/page.tsx b/src/app/admin/tags/page.tsx index 35eac6ae..be92cad9 100644 --- a/src/app/admin/tags/page.tsx +++ b/src/app/admin/tags/page.tsx @@ -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(); diff --git a/src/app/admin/teams/page.tsx b/src/app/admin/teams/page.tsx index 1eac967b..fb44ee51 100644 --- a/src/app/admin/teams/page.tsx +++ b/src/app/admin/teams/page.tsx @@ -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 { diff --git a/src/app/admin/transactions/page.tsx b/src/app/admin/transactions/page.tsx index 93bc4a03..9efb84d9 100644 --- a/src/app/admin/transactions/page.tsx +++ b/src/app/admin/transactions/page.tsx @@ -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"); diff --git a/src/app/admin/vouchers/page.tsx b/src/app/admin/vouchers/page.tsx index a9d29c17..32e2022d 100644 --- a/src/app/admin/vouchers/page.tsx +++ b/src/app/admin/vouchers/page.tsx @@ -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(); diff --git a/src/app/admin/vpn/page.tsx b/src/app/admin/vpn/page.tsx index 95af80be..596566a5 100644 --- a/src/app/admin/vpn/page.tsx +++ b/src/app/admin/vpn/page.tsx @@ -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"); diff --git a/src/app/admin/writeable-boxes/page.tsx b/src/app/admin/writeable-boxes/page.tsx index 7f3a4d61..6d9e331e 100644 --- a/src/app/admin/writeable-boxes/page.tsx +++ b/src/app/admin/writeable-boxes/page.tsx @@ -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({ diff --git a/src/lib/admin-operations-contract.test.ts b/src/lib/admin-operations-contract.test.ts index 0dd0a3b3..f954b190 100644 --- a/src/lib/admin-operations-contract.test.ts +++ b/src/lib/admin-operations-contract.test.ts @@ -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([]); }); });