- Create new @link-stack/logger package wrapping Pino for structured logging - Replace all console.log/error/warn statements across the monorepo - Configure environment-aware logging (pretty-print in dev, JSON in prod) - Add automatic redaction of sensitive fields (passwords, tokens, etc.) - Remove dead commented-out logger file from bridge-worker - Follow Pino's standard argument order (context object first, message second) - Support log levels via LOG_LEVEL environment variable - Export TypeScript types for better IDE support This provides consistent, structured logging across all applications and packages, making debugging easier and production logs more parseable.
135 lines
3.6 KiB
TypeScript
135 lines
3.6 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
import { withAuth, NextRequestWithAuth } from "next-auth/middleware";
|
|
import { createLogger } from "@link-stack/logger";
|
|
|
|
const logger = createLogger('link-middleware');
|
|
|
|
const rewriteURL = (
|
|
request: NextRequestWithAuth,
|
|
originBaseURL: string,
|
|
destinationBaseURL: string,
|
|
headers: any = {},
|
|
) => {
|
|
logger.debug({
|
|
originBaseURL,
|
|
destinationBaseURL,
|
|
headerKeys: Object.keys(headers)
|
|
}, "Rewriting URL");
|
|
let path = request.url.replace(originBaseURL, "");
|
|
if (path.startsWith("/")) {
|
|
path = path.slice(1);
|
|
}
|
|
const destinationURL = `${destinationBaseURL}/${path}`;
|
|
logger.debug({ from: request.url, to: destinationURL }, "URL rewrite");
|
|
const requestHeaders = new Headers(request.headers);
|
|
|
|
requestHeaders.delete("x-forwarded-user");
|
|
requestHeaders.delete("x-forwarded-roles");
|
|
requestHeaders.delete("connection");
|
|
|
|
for (const [key, value] of Object.entries(headers)) {
|
|
requestHeaders.set(key, value as string);
|
|
}
|
|
|
|
return NextResponse.rewrite(new URL(destinationURL), {
|
|
request: { headers: requestHeaders },
|
|
});
|
|
};
|
|
|
|
const checkRewrites = async (request: NextRequestWithAuth) => {
|
|
const linkBaseURL = process.env.LINK_URL ?? "http://localhost:3000";
|
|
logger.debug({ linkBaseURL }, "Link base URL");
|
|
const opensearchBaseURL =
|
|
process.env.OPENSEARCH_DASHBOARDS_URL ??
|
|
"http://opensearch-dashboards:5601";
|
|
|
|
const { token } = request.nextauth;
|
|
const email = token?.email?.toLowerCase() ?? "unknown";
|
|
const roles = (token?.roles as string[]) ?? [];
|
|
let headers = {
|
|
"x-forwarded-user": email,
|
|
"x-forwarded-roles": roles.join(","),
|
|
};
|
|
|
|
if (request.nextUrl.pathname.startsWith("/dashboards")) {
|
|
return rewriteURL(
|
|
request,
|
|
`${linkBaseURL}/dashboards`,
|
|
opensearchBaseURL,
|
|
headers,
|
|
);
|
|
} else {
|
|
const isDev = process.env.NODE_ENV === "development";
|
|
const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
|
|
const cspHeader = `
|
|
default-src 'self';
|
|
frame-src 'self' https://digiresilience.org;
|
|
connect-src 'self';
|
|
script-src 'self' 'nonce-${nonce}' 'strict-dynamic' ${isDev ? "'unsafe-eval'" : ""};
|
|
style-src 'self' 'unsafe-inline';
|
|
img-src 'self' blob: data:;
|
|
font-src 'self';
|
|
object-src 'none';
|
|
base-uri 'self';
|
|
form-action 'self';
|
|
frame-ancestors 'self';
|
|
upgrade-insecure-requests;
|
|
`;
|
|
const contentSecurityPolicyHeaderValue = cspHeader
|
|
.replace(/\s{2,}/g, " ")
|
|
.trim();
|
|
|
|
const requestHeaders = new Headers(request.headers);
|
|
requestHeaders.set("x-nonce", nonce);
|
|
requestHeaders.set(
|
|
"Content-Security-Policy",
|
|
contentSecurityPolicyHeaderValue,
|
|
);
|
|
|
|
const response = NextResponse.next({
|
|
request: {
|
|
headers: requestHeaders,
|
|
},
|
|
});
|
|
|
|
response.headers.set(
|
|
"Content-Security-Policy",
|
|
contentSecurityPolicyHeaderValue,
|
|
);
|
|
|
|
return response;
|
|
};
|
|
}
|
|
|
|
export default withAuth(checkRewrites, {
|
|
callbacks: {
|
|
authorized: ({ token, req }) => {
|
|
if (process.env.SETUP_MODE === "true") {
|
|
return true;
|
|
}
|
|
|
|
const path = req.nextUrl.pathname;
|
|
const roles: any = token?.roles ?? [];
|
|
|
|
if (path.startsWith("/login")) {
|
|
return true;
|
|
}
|
|
|
|
if (path.startsWith("/admin") && !roles.includes("admin")) {
|
|
return false;
|
|
}
|
|
|
|
if (roles.includes("admin") || roles.includes("agent")) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
},
|
|
},
|
|
});
|
|
|
|
export const config = {
|
|
matcher: [
|
|
"/((?!ws|wss|api/signal|api/whatsapp|_next/static|_next/image|favicon.ico).*)",
|
|
],
|
|
}
|