141 lines
4.1 KiB
TypeScript
141 lines
4.1 KiB
TypeScript
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 });
|
|
}
|
|
}
|