Files
Epicnabbo-Catalogus-Updated…/src/actions/admin-housekeeping.ts
T
openhands 5e8a13a84f fix: resolve all biomaly lint errors and warnings across CMS
- Fix CSS parser config (tailwindDirectives enabled)
- Fix noDangerouslySetInnerHtml via SanitizedHtml component
- Fix useExhaustiveDependencies in catalog-manager-dialog
- Fix noArrayIndexKey across 26 files (stable keys)
- Fix SVG a11y (titles, roles, aria-labels)
- Fix label/input associations (htmlFor/id pairs)
- Fix static element interactions (role + keyboard support)
- Fix noImgElement, noDescendingSpecificity (disabled - external Habbo URLs)
- Fix noNonNullAssertion, useTemplate, unused vars/imports
- Add SanitizedHtml shared component
- Migrate biome.json to 2.5.4 schema
2026-07-20 17:41:53 +02:00

896 lines
19 KiB
TypeScript

"use server";
import { revalidatePath } from "next/cache";
import { requirePermission } from "@/lib/admin/guard";
import { auth } from "@/lib/auth";
import { sessionUserId } from "@/lib/auth/session-user";
import { PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { logAudit } from "@/lib/services/audit";
function sanitize(str: string, max = 255): string {
return String(str ?? "")
.normalize("NFC")
.trim()
.slice(0, max);
}
async function currentUserId(): Promise<number | null> {
const session = await auth();
return session?.user ? sessionUserId(session.user.id) : null;
}
export async function upsertPermission(formData: FormData): Promise<void> {
await requirePermission(PERMS.SETTINGS_EDIT);
const permission = sanitize(String(formData.get("permission") ?? ""));
const minRank = Number(formData.get("minRank"));
const descriptionRaw = sanitize(String(formData.get("description") ?? ""));
const description = descriptionRaw.length > 0 ? descriptionRaw : null;
const groupName =
sanitize(String(formData.get("groupName") ?? ""), 100) || null;
const dependsOn = sanitize(String(formData.get("dependsOn") ?? "")) || null;
if (!permission || !Number.isFinite(minRank) || minRank < 0) return;
const userId = await currentUserId();
try {
const existing = await prisma.websiteHousekeepingPermissions.findUnique({
where: { permission },
});
if (existing) {
const before = {
minRank: existing.minRank,
description: existing.description,
groupName: existing.groupName,
dependsOn: existing.dependsOn,
};
await prisma.websiteHousekeepingPermissions.update({
where: { permission },
data: {
minRank,
description,
groupName,
dependsOn,
updatedBy: userId ?? undefined,
},
});
await logAudit({
userId: userId ?? 0,
action: "housekeeping.permission.update",
target: "WebsiteHousekeepingPermissions",
targetId: Number(existing.id),
before,
after: { minRank, description, groupName, dependsOn },
});
} else {
await prisma.websiteHousekeepingPermissions.create({
data: {
permission,
minRank,
description,
groupName,
dependsOn,
updatedBy: userId ?? undefined,
},
});
await logAudit({
userId: userId ?? 0,
action: "housekeeping.permission.create",
target: "WebsiteHousekeepingPermissions",
targetId: undefined,
after: { permission, minRank, description, groupName, dependsOn },
});
}
} catch {
// Swallow
}
revalidatePath("/admin/housekeeping");
}
export async function deletePermission(formData: FormData): Promise<void> {
await requirePermission(PERMS.SETTINGS_EDIT);
const raw = String(formData.get("id") ?? "").normalize("NFC");
if (!raw) return;
const userId = await currentUserId();
try {
const existing = await prisma.websiteHousekeepingPermissions.findUnique({
where: { id: BigInt(raw) },
});
if (existing) {
await prisma.websiteHousekeepingPermissions.delete({
where: { id: BigInt(raw) },
});
await logAudit({
userId: userId ?? 0,
action: "housekeeping.permission.delete",
target: "WebsiteHousekeepingPermissions",
targetId: Number(existing.id),
before: {
permission: existing.permission,
minRank: existing.minRank,
description: existing.description,
groupName: existing.groupName,
},
});
}
} catch {
// Already gone / invalid id.
}
revalidatePath("/admin/housekeeping");
}
export async function bulkImportPermissions(
formData: FormData,
): Promise<{ count: number; errors: string[] }> {
await requirePermission(PERMS.SETTINGS_EDIT);
const raw = sanitize(String(formData.get("json") ?? ""));
if (!raw) return { count: 0, errors: ["Empty input"] };
let parsed: unknown;
try {
parsed = JSON.parse(raw);
} catch {
return { count: 0, errors: ["Invalid JSON"] };
}
if (!Array.isArray(parsed)) {
return { count: 0, errors: ["Expected an array"] };
}
const userId = await currentUserId();
const errors: string[] = [];
let count = 0;
for (const item of parsed) {
const permission = sanitize(String(item.permission ?? ""));
const minRank = Number(item.minRank);
const description = sanitize(String(item.description ?? "")) || null;
const groupName = sanitize(String(item.groupName ?? ""), 100) || null;
const dependsOn = sanitize(String(item.dependsOn ?? "")) || null;
if (!permission || !Number.isFinite(minRank) || minRank < 0) {
errors.push(`Invalid entry: ${JSON.stringify(item)}`);
continue;
}
try {
await prisma.websiteHousekeepingPermissions.upsert({
where: { permission },
update: {
minRank,
description,
groupName,
dependsOn,
updatedBy: userId ?? undefined,
},
create: {
permission,
minRank,
description,
groupName,
dependsOn,
updatedBy: userId ?? undefined,
},
});
count++;
} catch {
errors.push(`Failed to upsert: ${permission}`);
}
}
revalidatePath("/admin/housekeeping");
return { count, errors };
}
export async function exportPermissions(): Promise<string> {
await requirePermission(PERMS.SETTINGS_VIEW);
const perms = await prisma.websiteHousekeepingPermissions.findMany({
orderBy: [{ groupName: "asc" }, { permission: "asc" }],
select: {
permission: true,
minRank: true,
description: true,
groupName: true,
dependsOn: true,
},
});
return JSON.stringify(perms, null, 2);
}
export async function applyPreset(formData: FormData): Promise<void> {
await requirePermission(PERMS.SETTINGS_EDIT);
const preset = String(formData.get("preset") ?? "").trim();
const userId = await currentUserId();
const presets: Record<
string,
Array<{
permission: string;
minRank: number;
description?: string;
groupName?: string;
}>
> = {
moderator: [
{
permission: "admin.dashboard",
minRank: 5,
description: "Access admin dashboard",
groupName: "Core",
},
{
permission: "admin.users.view",
minRank: 5,
description: "View user profiles",
groupName: "Users",
},
{
permission: "admin.users.ban",
minRank: 5,
description: "Ban users",
groupName: "Users",
},
{
permission: "admin.room.view",
minRank: 5,
description: "View rooms",
groupName: "Rooms",
},
{
permission: "admin.bans.view",
minRank: 5,
description: "View bans",
groupName: "Bans",
},
{
permission: "admin.logs.view",
minRank: 5,
description: "View logs",
groupName: "Logs",
},
{
permission: "admin.wordfilter.view",
minRank: 5,
description: "View wordfilter",
groupName: "Wordfilter",
},
{
permission: "mod.dashboard",
minRank: 5,
description: "Mod dashboard",
groupName: "Moderation",
},
{
permission: "mod.cfh.view",
minRank: 5,
description: "View CFH",
groupName: "Moderation",
},
{
permission: "mod.cfh.edit",
minRank: 5,
description: "Handle CFH",
groupName: "Moderation",
},
{
permission: "mod.actions",
minRank: 5,
description: "Mod actions",
groupName: "Moderation",
},
{
permission: "mod.users.view",
minRank: 5,
description: "View users (mod)",
groupName: "Moderation",
},
{
permission: "mod.bans.view",
minRank: 5,
description: "View bans (mod)",
groupName: "Moderation",
},
],
admin: [
{
permission: "admin.dashboard",
minRank: 7,
description: "Access admin dashboard",
groupName: "Core",
},
{
permission: "admin.users.view",
minRank: 7,
description: "View user profiles",
groupName: "Users",
},
{
permission: "admin.users.edit",
minRank: 7,
description: "Edit users",
groupName: "Users",
},
{
permission: "admin.users.ban",
minRank: 7,
description: "Ban users",
groupName: "Users",
},
{
permission: "admin.users.reset_password",
minRank: 7,
description: "Reset passwords",
groupName: "Users",
},
{
permission: "admin.room.view",
minRank: 7,
description: "View rooms",
groupName: "Rooms",
},
{
permission: "admin.room.edit",
minRank: 7,
description: "Edit rooms",
groupName: "Rooms",
},
{
permission: "admin.room.delete",
minRank: 7,
description: "Delete rooms",
groupName: "Rooms",
},
{
permission: "admin.bans.view",
minRank: 7,
description: "View bans",
groupName: "Bans",
},
{
permission: "admin.news.view",
minRank: 7,
description: "View news",
groupName: "News",
},
{
permission: "admin.news.edit",
minRank: 7,
description: "Edit news",
groupName: "News",
},
{
permission: "admin.logs.view",
minRank: 7,
description: "View logs",
groupName: "Logs",
},
{
permission: "admin.settings.view",
minRank: 7,
description: "View settings",
groupName: "Settings",
},
{
permission: "admin.wordfilter.view",
minRank: 7,
description: "View wordfilter",
groupName: "Wordfilter",
},
{
permission: "admin.wordfilter.edit",
minRank: 7,
description: "Edit wordfilter",
groupName: "Wordfilter",
},
{
permission: "admin.catalog.view",
minRank: 7,
description: "View catalog",
groupName: "Catalog",
},
{
permission: "admin.shop.view",
minRank: 7,
description: "View shop",
groupName: "Shop",
},
{
permission: "admin.events.view",
minRank: 7,
description: "View events",
groupName: "Events",
},
{
permission: "admin.tickets.view",
minRank: 7,
description: "View tickets",
groupName: "Tickets",
},
{
permission: "admin.moderation.view",
minRank: 7,
description: "View moderation",
groupName: "Moderation",
},
{
permission: "admin.polls.view",
minRank: 7,
description: "View polls",
groupName: "Polls",
},
{
permission: "admin.analytics.view",
minRank: 7,
description: "View analytics",
groupName: "Analytics",
},
{
permission: "admin.prefixes.view",
minRank: 7,
description: "View prefixes",
groupName: "Prefixes",
},
{
permission: "admin.banners.view",
minRank: 7,
description: "View banners",
groupName: "Banners",
},
{
permission: "admin.radio.view",
minRank: 7,
description: "View radio",
groupName: "Radio",
},
{
permission: "admin.rcon.execute",
minRank: 7,
description: "Execute RCON",
groupName: "RCON",
},
{
permission: "mod.dashboard",
minRank: 7,
description: "Mod dashboard",
groupName: "Moderation",
},
{
permission: "mod.cfh.view",
minRank: 7,
description: "View CFH",
groupName: "Moderation",
},
{
permission: "mod.cfh.edit",
minRank: 7,
description: "Handle CFH",
groupName: "Moderation",
},
{
permission: "mod.actions",
minRank: 7,
description: "Mod actions",
groupName: "Moderation",
},
],
superadmin: [
{
permission: "admin.dashboard",
minRank: 8,
description: "Access admin dashboard",
groupName: "Core",
},
{
permission: "admin.users.view",
minRank: 8,
description: "View user profiles",
groupName: "Users",
},
{
permission: "admin.users.edit",
minRank: 8,
description: "Edit users",
groupName: "Users",
},
{
permission: "admin.users.ban",
minRank: 8,
description: "Ban users",
groupName: "Users",
},
{
permission: "admin.users.reset_password",
minRank: 8,
description: "Reset passwords",
groupName: "Users",
},
{
permission: "admin.room.view",
minRank: 8,
description: "View rooms",
groupName: "Rooms",
},
{
permission: "admin.room.edit",
minRank: 8,
description: "Edit rooms",
groupName: "Rooms",
},
{
permission: "admin.room.delete",
minRank: 8,
description: "Delete rooms",
groupName: "Rooms",
},
{
permission: "admin.bans.view",
minRank: 8,
description: "View bans",
groupName: "Bans",
},
{
permission: "admin.news.view",
minRank: 8,
description: "View news",
groupName: "News",
},
{
permission: "admin.news.edit",
minRank: 8,
description: "Edit news",
groupName: "News",
},
{
permission: "admin.logs.view",
minRank: 8,
description: "View logs",
groupName: "Logs",
},
{
permission: "admin.settings.view",
minRank: 8,
description: "View settings",
groupName: "Settings",
},
{
permission: "admin.settings.edit",
minRank: 8,
description: "Edit settings",
groupName: "Settings",
},
{
permission: "admin.permissions.manage",
minRank: 8,
description: "Manage permissions",
groupName: "Settings",
},
{
permission: "admin.wordfilter.view",
minRank: 8,
description: "View wordfilter",
groupName: "Wordfilter",
},
{
permission: "admin.wordfilter.edit",
minRank: 8,
description: "Edit wordfilter",
groupName: "Wordfilter",
},
{
permission: "admin.catalog.view",
minRank: 8,
description: "View catalog",
groupName: "Catalog",
},
{
permission: "admin.catalog.edit",
minRank: 8,
description: "Edit catalog",
groupName: "Catalog",
},
{
permission: "admin.shop.view",
minRank: 8,
description: "View shop",
groupName: "Shop",
},
{
permission: "admin.shop.edit",
minRank: 8,
description: "Edit shop",
groupName: "Shop",
},
{
permission: "admin.events.view",
minRank: 8,
description: "View events",
groupName: "Events",
},
{
permission: "admin.events.edit",
minRank: 8,
description: "Edit events",
groupName: "Events",
},
{
permission: "admin.tickets.view",
minRank: 8,
description: "View tickets",
groupName: "Tickets",
},
{
permission: "admin.tickets.edit",
minRank: 8,
description: "Edit tickets",
groupName: "Tickets",
},
{
permission: "admin.moderation.view",
minRank: 8,
description: "View moderation",
groupName: "Moderation",
},
{
permission: "admin.moderation.edit",
minRank: 8,
description: "Edit moderation",
groupName: "Moderation",
},
{
permission: "admin.polls.view",
minRank: 8,
description: "View polls",
groupName: "Polls",
},
{
permission: "admin.polls.edit",
minRank: 8,
description: "Edit polls",
groupName: "Polls",
},
{
permission: "admin.analytics.view",
minRank: 8,
description: "View analytics",
groupName: "Analytics",
},
{
permission: "admin.analytics.export",
minRank: 8,
description: "Export analytics",
groupName: "Analytics",
},
{
permission: "admin.devops.view",
minRank: 8,
description: "View devops",
groupName: "DevOps",
},
{
permission: "admin.devops.edit",
minRank: 8,
description: "Edit devops",
groupName: "DevOps",
},
{
permission: "admin.prefixes.view",
minRank: 8,
description: "View prefixes",
groupName: "Prefixes",
},
{
permission: "admin.prefixes.edit",
minRank: 8,
description: "Edit prefixes",
groupName: "Prefixes",
},
{
permission: "admin.banners.view",
minRank: 8,
description: "View banners",
groupName: "Banners",
},
{
permission: "admin.banners.edit",
minRank: 8,
description: "Edit banners",
groupName: "Banners",
},
{
permission: "admin.radio.view",
minRank: 8,
description: "View radio",
groupName: "Radio",
},
{
permission: "admin.radio.edit",
minRank: 8,
description: "Edit radio",
groupName: "Radio",
},
{
permission: "admin.rcon.execute",
minRank: 8,
description: "Execute RCON",
groupName: "RCON",
},
{
permission: "admin.assets.import",
minRank: 8,
description: "Import assets",
groupName: "Assets",
},
{
permission: "admin.export",
minRank: 8,
description: "Export data",
groupName: "Export",
},
{
permission: "admin.notifications.view",
minRank: 8,
description: "View notifications",
groupName: "Notifications",
},
{
permission: "admin.notifications.edit",
minRank: 8,
description: "Edit notifications",
groupName: "Notifications",
},
{
permission: "admin.pages.view",
minRank: 8,
description: "View CMS pages",
groupName: "CMS Pages",
},
{
permission: "admin.pages.edit",
minRank: 8,
description: "Edit CMS pages",
groupName: "CMS Pages",
},
{
permission: "mod.dashboard",
minRank: 8,
description: "Mod dashboard",
groupName: "Moderation",
},
{
permission: "mod.cfh.view",
minRank: 8,
description: "View CFH",
groupName: "Moderation",
},
{
permission: "mod.cfh.edit",
minRank: 8,
description: "Handle CFH",
groupName: "Moderation",
},
{
permission: "mod.actions",
minRank: 8,
description: "Mod actions",
groupName: "Moderation",
},
{
permission: "mod.team.view",
minRank: 8,
description: "View mod team",
groupName: "Moderation",
},
{
permission: "mod.tickets.view",
minRank: 8,
description: "View mod tickets",
groupName: "Moderation",
},
{
permission: "mod.tickets.edit",
minRank: 8,
description: "Edit mod tickets",
groupName: "Moderation",
},
{
permission: "mod.users.view",
minRank: 8,
description: "View users (mod)",
groupName: "Moderation",
},
{
permission: "mod.bans.view",
minRank: 8,
description: "View bans (mod)",
groupName: "Moderation",
},
],
};
const selected = presets[preset];
if (!selected) return;
for (const item of selected) {
const { permission, minRank, description, groupName } = item;
await prisma.websiteHousekeepingPermissions.upsert({
where: { permission },
update: {
minRank,
description,
groupName,
updatedBy: userId ?? undefined,
},
create: {
permission,
minRank,
description,
groupName,
updatedBy: userId ?? undefined,
},
});
}
await logAudit({
userId: userId ?? 0,
action: "housekeeping.preset.apply",
target: "WebsiteHousekeepingPermissions",
after: { preset, count: selected.length },
});
revalidatePath("/admin/housekeeping");
}
export async function clearAllPermissions(): Promise<void> {
await requirePermission(PERMS.SETTINGS_EDIT);
const userId = await currentUserId();
const count = await prisma.websiteHousekeepingPermissions.count();
await prisma.websiteHousekeepingPermissions.deleteMany();
await logAudit({
userId: userId ?? 0,
action: "housekeeping.permission.clear_all",
target: "WebsiteHousekeepingPermissions",
after: { deletedCount: count },
});
revalidatePath("/admin/housekeeping");
}
export async function bulkDeletePermissions(
ids: bigint[],
): Promise<{ count: number }> {
await requirePermission(PERMS.SETTINGS_EDIT);
if (!ids.length) return { count: 0 };
const userId = await currentUserId();
await prisma.websiteHousekeepingPermissions.deleteMany({
where: { id: { in: ids } },
});
await logAudit({
userId: userId ?? 0,
action: "housekeeping.permission.bulk_delete",
target: "WebsiteHousekeepingPermissions",
after: { deletedCount: ids.length },
});
revalidatePath("/admin/housekeeping");
return { count: ids.length };
}