import { and, eq, gt, isNull, or } from "drizzle-orm"; import { NextResponse } from "next/server"; import { z } from "zod"; import { db } from "@/lib/db"; import { accessIpBlocklist } from "@/lib/db/schema"; import { matchManualIpBlock } from "@/lib/security/manual-ip-blocklist"; export const runtime = "nodejs"; export const maxDuration = 5; const payloadSchema = z.object({ ip: z.string().trim().max(128).optional().nullable(), }); const BLOCKLIST_UNAVAILABLE_CACHE_MS = 5 * 60 * 1000; let blocklistUnavailableUntil = 0; let blocklistMissingTableWarned = false; function getExpectedSecret() { return process.env.ACCESS_LOG_INTERNAL_TOKEN || process.env.AUTH_SECRET || ""; } function getErrorCode(error: unknown) { const directCode = error && typeof error === "object" ? (error as { code?: unknown }).code : null; if (typeof directCode === "string") return directCode; const cause = error && typeof error === "object" ? (error as { cause?: unknown }).cause : null; const causeCode = cause && typeof cause === "object" ? (cause as { code?: unknown }).code : null; return typeof causeCode === "string" ? causeCode : null; } function getErrorMessage(error: unknown) { const messages: string[] = []; if (error instanceof Error) messages.push(error.message); const cause = error && typeof error === "object" ? (error as { cause?: unknown }).cause : null; if (cause instanceof Error) messages.push(cause.message); if (!messages.length) messages.push(String(error)); return messages.join("\n"); } function isMissingAccessIpBlocklistTableError(error: unknown) { if (getErrorCode(error) === "42P01") return true; const message = getErrorMessage(error); return ( message.includes('relation "AccessIpBlocklist" does not exist') || (message.includes("AccessIpBlocklist") && message.includes("does not exist")) ); } function markBlocklistUnavailable(error: unknown) { blocklistUnavailableUntil = Date.now() + BLOCKLIST_UNAVAILABLE_CACHE_MS; if (blocklistMissingTableWarned) return; blocklistMissingTableWarned = true; console.warn( "[AccessIpBlocklist] table is unavailable; manual IP block checks will be skipped temporarily", getErrorMessage(error) ); } export async function POST(request: Request) { const expectedSecret = getExpectedSecret(); const providedSecret = request.headers.get("x-access-log-internal-secret") || ""; if (!expectedSecret || providedSecret !== expectedSecret) { return new NextResponse("Not found", { status: 404 }); } const parsed = payloadSchema.safeParse(await request.json().catch(() => null)); if (!parsed.success || !parsed.data.ip) { return NextResponse.json({ blocked: false }); } if ( process.env.MANUAL_IP_BLOCKLIST_ENABLED === "false" || Date.now() < blocklistUnavailableUntil ) { return NextResponse.json({ blocked: false }); } try { const now = new Date(); const entries = await db .select({ id: accessIpBlocklist.id, ip: accessIpBlocklist.ip, reason: accessIpBlocklist.reason, expiresAt: accessIpBlocklist.expiresAt, }) .from(accessIpBlocklist) .where( and( eq(accessIpBlocklist.isActive, true), or( isNull(accessIpBlocklist.expiresAt), gt(accessIpBlocklist.expiresAt, now) ) ) ) .limit(1000); const activeEntries = entries.filter((entry) => entry.ip && entry.id); const matched = matchManualIpBlock(parsed.data.ip, activeEntries); return NextResponse.json({ blocked: Boolean(matched), entry: matched ? { id: matched.id, ip: matched.ip, reason: matched.reason || null, expiresAt: matched.expiresAt || null, } : null, }); } catch (error) { if (isMissingAccessIpBlocklistTableError(error)) { markBlocklistUnavailable(error); return NextResponse.json({ blocked: false }); } console.warn("[AccessIpBlocklist] internal check failed", error); return NextResponse.json({ blocked: false }); } }