Files
EpicNext-Cms/src/lib/access-guard.ts
T
openhands 41be6835bf Add missing admin action files and navigation links
- Add 11 missing server action files: badges, bulk-users, catalog, catalog-bc, catalog-items, import-badges, import-furni, multi-account-detect, permissions, rooms, soundtracks
- Add missing admin navigation links: tickets, sounds, translations, import, radio sub-pages
- Add translation keys for all new navigation items
2026-07-11 12:01:05 +02:00

60 lines
2.3 KiB
TypeScript

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<void> {
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);
}