"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 { const session = await auth(); return session?.user ? sessionUserId(session.user.id) : null; } export async function upsertPermission(formData: FormData): Promise { 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 { 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 { await requirePermission(PERMS.SETTINGS_EDIT); const raw = sanitize(String(formData.get("json") ?? "")); const errors: string[] = []; let count = 0; if (!raw) return; let parsed: unknown; try { parsed = JSON.parse(raw); } catch { return; } if (!Array.isArray(parsed)) { return; } const userId = await currentUserId(); 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"); } export async function exportPermissions(): Promise { 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 { 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 { 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 testRankPermission(formData: FormData): Promise { await requirePermission(PERMS.SETTINGS_VIEW); const rank = Number(formData.get("testRank")); if (!Number.isFinite(rank) || rank < 0) return; const perms = await prisma.websiteHousekeepingPermissions.findMany({ where: { minRank: { lte: rank } }, orderBy: [{ groupName: "asc" }, { permission: "asc" }], select: { permission: true }, }); const granted = perms.map((p) => p.permission); // Result is displayed via the revalidated page revalidatePath( "/admin/housekeeping?tab=test&result=" + encodeURIComponent(JSON.stringify({ rank, permissions: granted })), ); } export async function getAuditHistory(): Promise { await requirePermission(PERMS.SETTINGS_VIEW); const { getAuditLogs } = await import("@/lib/services/audit"); const result = await getAuditLogs({ search: "housekeeping", perPage: 50 }); return result.rows; }