import { asc, eq } from "drizzle-orm"; import Link from "next/link"; import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; import { AclModelPermission, AclPermission, AclRole, db, WebsiteHousekeepingPermissions, } from "@/lib/db"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { getAuditLogs } from "@/lib/services/audit"; import { AuditSection, ExportSection } from "./client"; // TODO: Cache Components adoption. Refactor this route so this opt-out can be removed. // See: https://nextjs.org/docs/app/guides/migrating-to-cache-components export const instant = false; export default async function AdminHousekeeping({ searchParams, }: { searchParams: Promise<{ tab?: | "overview" | "manage" | "import" | "ranks" | "presets" | "test" | "audit" | "compare"; group?: string; }>; }) { const ctx = await getAdminContext(); if (!canAccess(ctx.permissions, PERMS.SETTINGS_VIEW, ctx.session.user.rank)) { redirect("/admin"); } const t = await getTranslations("pages.admin.housekeeping"); const sp = await searchParams; const tab = sp.tab ?? "overview"; const allPermissions = await db .select() .from(WebsiteHousekeepingPermissions) .orderBy( asc(WebsiteHousekeepingPermissions.groupName), asc(WebsiteHousekeepingPermissions.permission), ) .catch((): (typeof WebsiteHousekeepingPermissions.$inferSelect)[] => []); const ranks = allPermissions.map((p) => p.minRank); const maxRank = ranks.length ? Math.max(...ranks) : 0; const totalMin = ranks.reduce((sum, r) => sum + r, 0); const groupCounts: Record = {}; for (const p of allPermissions) { const g: string = p.groupName ?? "Uncategorized"; groupCounts[g] = (groupCounts[g] || 0) + 1; } const rankGroups: Record< string, (typeof WebsiteHousekeepingPermissions.$inferSelect)[] > = {}; for (const p of allPermissions) { const rk = String(p.minRank); if (!rankGroups[rk]) rankGroups[rk] = []; rankGroups[rk].push(p); } const aclSlugs = await db .select({ slug: AclPermission.slug }) .from(AclPermission) .catch(() => [] as { slug: string }[]); const aclSlugSet = new Set(aclSlugs.map((p) => p.slug)); const aclRolePerms = await db .select({ roleSlug: AclRole.slug, permissionSlug: AclPermission.slug, }) .from(AclModelPermission) .innerJoin( AclPermission, eq(AclModelPermission.permissionId, AclPermission.id), ) .innerJoin(AclRole, eq(AclModelPermission.modelId, AclRole.id)) .where(eq(AclModelPermission.modelType, "Role")) .catch((): { roleSlug: string; permissionSlug: string }[] => []); const aclMinRankBySlug = new Map(); for (const row of aclRolePerms) { const roleSlug = row.roleSlug; if (!roleSlug.startsWith("rank_")) continue; const rank = Number(roleSlug.slice(5)); if (!Number.isFinite(rank)) continue; const slug = row.permissionSlug; const prev = aclMinRankBySlug.get(slug); if (prev === undefined || rank < prev) aclMinRankBySlug.set(slug, rank); } const compareRows = allPermissions.map((p) => { const inAcl = aclSlugSet.has(p.permission); const aclMin = aclMinRankBySlug.get(p.permission); return { permission: p.permission, hkMinRank: p.minRank, inAcl, aclMinRank: aclMin ?? null, aligned: inAcl && aclMin !== undefined ? aclMin === p.minRank : false, }; }); const missingInAcl = compareRows.filter((r) => !r.inAcl).length; const rankMismatch = compareRows.filter( (r) => r.inAcl && r.aclMinRank !== null && r.aclMinRank !== r.hkMinRank, ).length; const alignedCount = compareRows.filter((r) => r.aligned).length; const auditLogs = await getAuditLogs({ search: "housekeeping", perPage: 25, }).catch(() => null); return (

{t("legacyBannerTitle")}

{t("legacyBannerBody")}{" "} {t("legacyBannerCta")}

{t("writesRetired")}

{t("tabOverview")} {t("tabCompare")} {t("tabRanks")} {t("tabExportOnly")} {t("tabAudit")}
{tab === "overview" && ( <>
{t("permissionsCount", { count: allPermissions.length })} {t("highestMinRank", { rank: maxRank })} {t("totalMinRank", { total: totalMin })} {t("groupCount", { count: Object.keys(groupCounts).length })} {t("compareAligned", { count: alignedCount })} {t("compareMissingAcl", { count: missingInAcl })} {t("compareMismatch", { count: rankMismatch })}

{t("perGroup")}

{Object.entries(groupCounts) .sort(([a], [b]) => a.localeCompare(b)) .map(([group, count]) => (
{group}
{count} {count === 1 ? t("permission") : t("permissions")}
))}

{t("perRank")}

{Object.entries(rankGroups) .sort(([a], [b]) => Number(b) - Number(a)) .map(([rank, perms]) => ( ))}
{t("rank")} {t("count")} {t("permissions")}
Rank {rank} {perms.length} {perms.map((p) => p.permission).join(", ")}
)} {tab === "compare" && (

{t("tabCompare")}

{t("compareHint")}

{compareRows.map((row) => { const status = !row.inAcl ? t("statusMissingAcl") : row.aligned ? t("statusAligned") : t("statusMismatch"); return ( ); })}
{t("colPermission")} {t("colMinRank")} {t("colAclPresent")} {t("colAclMinRank")} {t("colStatus")}
{row.permission} {row.hkMinRank} {row.inAcl ? "✓" : "—"} {row.aclMinRank ?? "—"} {status}
)} {tab === "import" && (

{t("exportOnlyHint")}

)} {tab === "ranks" && (

{t("perRank")}

{t("rankOverviewHint")}

{Object.keys(rankGroups).length === 0 ? (
{t("noPermissions")}
) : (
{Object.entries(rankGroups) .sort(([a], [b]) => Number(b) - Number(a)) .map(([rank, perms]) => { const groupsInRank: Record = {}; for (const p of perms) { const g = p.groupName ?? "Uncategorized"; if (!groupsInRank[g]) groupsInRank[g] = []; groupsInRank[g].push(p); } return (

{t("rank")} {rank}

{perms.length} {t("permissions")}
{Object.entries(groupsInRank) .sort(([a], [b]) => a.localeCompare(b)) .map(([group, groupPerms]) => (
{group}
    {groupPerms.map((p) => (
  • {p.permission} {p.dependsOn ? ( → {p.dependsOn} ) : null}
  • ))}
))}
); })}
)}
)} {tab === "audit" && }
); }