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:
1 parent
3c8a8ff888
commit
0e89d03940
63 files changed
+420
-163
No files matched your search
@@ -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;
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,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) {
|
||||
|
||||
@@ -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") ?? "")
|
||||
|
||||
@@ -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 } });
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 "";
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 } });
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() },
|
||||
|
||||
@@ -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 }> = [];
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>>;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
@@ -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>> =
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 }[] = [];
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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([]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user