@centinel/nextjs
Version:
Package designed to add Centinel Analytica functionality to Next.js applications
121 lines (120 loc) • 4.67 kB
JavaScript
import { NextResponse } from "next/server";
export class CentinelMiddleware {
constructor(config) {
this.config = config;
}
async validate(event) {
const request = event.request;
try {
const clientInfo = this.getClientInfo(request);
const payload = this.createPayload(request, clientInfo);
const validationResult = await this.callValidator(payload);
if (validationResult.decision === "block") {
return {
response: NextResponse.redirect(new URL("/block", request.url)),
decision: "block",
};
}
if (validationResult.decision === "redirect") {
const redirectUrl = validationResult.redirect_url || "/interstitial";
const redirectResponse = NextResponse.redirect(new URL(redirectUrl, request.url));
if (validationResult.cookies?.length) {
this.applyCookies(redirectResponse, validationResult.cookies, request);
}
return {
response: redirectResponse,
decision: "redirect",
};
}
return {
response: NextResponse.next(),
decision: validationResult.decision,
};
}
catch {
return {
response: NextResponse.next(),
decision: "allow",
};
}
}
getClientInfo(request) {
const forwardedFor = request.headers.get("x-forwarded-for");
const realIp = request.headers.get("x-real-ip");
const cfConnectingIp = request.headers.get("cf-connecting-ip");
return {
ip: cfConnectingIp ||
forwardedFor?.split(",")[0]?.trim() ||
realIp ||
"unknown",
referer: request.headers.get("referer") || request.headers.get("referrer") || "",
};
}
createPayload(request, clientInfo) {
const headers = {};
request.headers.forEach((value, key) => {
headers[key] = value;
});
// Try to get the real host from headers, fallback to nextUrl.hostname
let realHost = request.headers.get("x-forwarded-host") ||
request.headers.get("host") ||
request.nextUrl.hostname;
if (realHost === "localhost") {
realHost = `localhost:${process.env.PORT || request.headers.get("x-forwarded-port") || "3000"}`;
}
// Try to get protocol from x-forwarded-proto, fallback to nextUrl.protocol
const proto = request.headers.get("x-forwarded-proto") ||
request.nextUrl.protocol.replace(":", "");
const protocol = proto.endsWith(":") ? proto : proto + ":";
const requestUrl = `${protocol}//${realHost}${request.nextUrl.pathname}${request.nextUrl.search}`;
return {
cookie: request.cookies.get("_centinel")?.value,
url: requestUrl,
ip: clientInfo.ip,
referrer: clientInfo.referer,
method: request.method,
headers,
};
}
async callValidator(payload) {
try {
const response = await fetch("https://validator.centinelanalytica.com/validate", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": this.config.secretKey,
},
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error(`Validator service responded with ${response.status}`);
}
const result = await response.json();
return {
success: result.success !== false,
decision: result.decision || "allow",
redirect_url: result.redirect_url,
cookies: result.cookies || [],
};
}
catch (error) {
return {
success: false,
decision: "allow", // Fail through
};
}
}
applyCookies(response, cookies, request) {
const realHost = this.getHost(request);
cookies.forEach((cookie) => {
response.cookies.set(cookie.name, cookie.value, {
path: cookie.path || "/",
domain: cookie.domain || realHost,
});
});
}
getHost(request) {
const host = request.headers.get("host");
return host?.includes("localhost") ? undefined : host || undefined;
}
}