import { headers } from "next/headers"; import { redirect } from "next/navigation"; import { auth } from "@/lib/auth"; import { isIpBlacklisted, recordRequest } from "@/lib/services/abuse-guard"; import { prisma } from "@/lib/prisma"; import { siteSettings } from "@/lib/services/site-settings"; // Paths that must never be gated (otherwise banned/maintenance loop forever). const EXEMPT = ["/banned", "/maintenance", "/login", "/register", "/forgot", "/reset", "/api"]; function isExempt(path: string): boolean { return EXEMPT.some((p) => path === p || path.startsWith(`${p}/`)); } /** * Site-wide access enforcement (called from the root layout): routes non-staff * to /maintenance when maintenance mode is on, and banned users to /banned. * Runs in the Node runtime so it can query the DB. The redirect decision is * computed inside try/catch and the redirect() (which throws NEXT_REDIRECT) is * issued OUTSIDE it. */ export async function enforceSiteAccess(): Promise { const h = await headers(); const path = h.get("x-pathname") ?? "/"; const ip = h.get("x-real-client-ip") ?? h.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "0.0.0.0"; // Abuse/DDoS guard: count this request and block flooding IPs (no-op unless // enabled in settings). Best-effort — never let it throw past the guard. void recordRequest(ip).catch(() => {}); if (isExempt(path)) return; let target: string | null = null; try { // App-level IP blacklist (auto-populated by the abuse guard + /admin/ip). if (await isIpBlacklisted(ip)) target = "/banned"; const session = await auth(); const rank = session?.user?.rank ?? 0; if (!target && (await siteSettings.getBool("maintenance_enabled", false))) { const minLogin = Number(await siteSettings.get("min_maintenance_login_rank", "7")) || 7; if (rank < minLogin) target = "/maintenance"; } if (!target && session?.user?.id) { const now = Math.floor(Date.now() / 1000); const ban = await prisma.ban.findFirst({ where: { userId: Number(session.user.id), banExpire: { gt: now } }, select: { id: true }, }); if (ban) target = "/banned"; } } catch { // On any failure, fail open (don't lock the whole site out on a DB hiccup). } if (target) redirect(target); }