fix(admin): P0 integrity — permanent bans, ACL sidebar, rank guards
Deploy / release (push) Skipped
Deploy / deploy (push) Successful in 1m36s

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
SimoandCursor committed 2026-07-22 19:01:59 +02:00
1 parent 11004626c7
commit 75cdfdebe4
14 files changed
+458 -68

No files matched your search

+3 -3
View File
@@ -2,6 +2,7 @@ import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server";
import { createBan, liftBan } from "@/actions/admin-bans";
import { StatusCard } from "@/components/admin/dashboard";
import { activeBanWhere, unixNow } from "@/lib/bans";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
@@ -18,11 +19,11 @@ export default async function AdminBans() {
}
const t = await getTranslations("pages.admin.bans");
const now = Math.floor(Date.now() / 1000);
const now = unixNow();
let bans: Awaited<ReturnType<typeof prisma.ban.findMany>> = [];
try {
bans = await prisma.ban.findMany({
where: { banExpire: { gt: now } },
where: activeBanWhere(now),
orderBy: { timestamp: "desc" },
take: 100,
});
@@ -30,7 +31,6 @@ export default async function AdminBans() {
bans = [];
}
// banExpire of 0 means permanent in the emulator schema.
const permanent = bans.filter((b) => b.banExpire === 0).length;
const accountBans = bans.filter((b) => b.type === "account").length;
+13 -1
View File
@@ -10,7 +10,9 @@ import { AdminTopbar } from "@/components/admin/admin-topbar";
import { LanguageSwitcher } from "@/components/language-switcher";
import { ThemeSwitcher } from "@/components/theme-switcher";
import { requireStaff } from "@/lib/admin/guard";
import { collectNavPermissionSlugs } from "@/lib/admin-nav";
import { setCsrfCookie } from "@/lib/foundation/security";
import { canAccess, getAdminContext } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { siteSettings } from "@/lib/services/site-settings";
@@ -66,6 +68,13 @@ async function Sidebar({
}) {
const t = await getTranslations("pages.admin.nav");
const initial = staff.username.charAt(0).toUpperCase();
const { permissions } = await getAdminContext();
const isSuperAdmin = permissions.isSuperAdmin;
const allowedPermissions = isSuperAdmin
? []
: collectNavPermissionSlugs().filter((slug) =>
canAccess(permissions, slug, staff.rank),
);
return (
<aside
@@ -93,7 +102,10 @@ async function Sidebar({
</div>
</div>
<AdminSidebarNav />
<AdminSidebarNav
allowedPermissions={allowedPermissions}
isSuperAdmin={isSuperAdmin}
/>
<div className="shrink-0 border-t border-[var(--admin-border)] px-3 py-3">
<Link
+2 -1
View File
@@ -9,6 +9,7 @@ import {
import Link from "next/link";
import { getTranslations } from "next-intl/server";
import { StatusCard } from "@/components/admin/dashboard";
import { activeBanWhere } from "@/lib/bans";
import { prisma } from "@/lib/prisma";
export const dynamic = "force-dynamic";
@@ -31,7 +32,7 @@ export default async function AdminDashboard() {
prisma.user.count({ where: { online: "1" } }),
prisma.websiteArticles.count(),
prisma.ban.count({
where: { banExpire: { gt: Math.floor(Date.now() / 1000) } },
where: activeBanWhere(),
}),
]);
} catch {}