fix: sanitize traffic monitor auth surface
All checks were successful
CI and Production Smoke / smoke (push) Successful in 7s

This commit is contained in:
OG T
2026-06-11 19:37:28 +08:00
parent 2fd0570295
commit e11e70ab19
2 changed files with 52 additions and 9 deletions

View File

@@ -99,6 +99,32 @@ function explainAction(action: string) {
return EVENT_LABELS[action] || action;
}
function displayEntityId(value: string | null | undefined) {
if (!value || value === "-") return "-";
const normalized = value.toLowerCase();
if (normalized.includes("growth-kit")) return "成長導流素材";
if (normalized.includes("demand-campaign")) return "需求導流素材";
if (normalized.includes("onboarding")) return "合作說明";
if (normalized.includes("integration")) return "整合目錄";
if (normalized.includes("referral-status")) return "引薦狀態";
if (normalized.includes("proposal-intake")) return "提案入口";
if (value.length > 42) return `${value.slice(0, 18)}...${value.slice(-8)}`;
return value;
}
function displayResponseSummary(value: string | null | undefined) {
if (!value || value === "unknown") return "n/a";
const normalized = value.toLowerCase();
if (normalized.includes("growth_kit")) return "已發出成長導流素材";
if (normalized.includes("demand_campaign")) return "已發出需求導流素材";
if (normalized.includes("referral_status")) return "已回傳引薦狀態";
if (normalized.includes("onboarding")) return "已回傳合作說明";
if (normalized.includes("integrations")) return "已回傳整合目錄";
if (normalized.includes("proposal_view")) return "已記錄提案頁查看";
if (normalized.includes("proposal")) return "已記錄提案流程";
return value.replace(/_/g, " ");
}
type TrafficActorClass = "a2a" | "external_ai_agent" | "likely_ai_agent" | "other_external";
const AI_USER_AGENT_HINTS = [
@@ -925,7 +951,7 @@ export default async function TrafficDashboard({
</div>
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-6">
<h2 className="text-xl font-semibold mb-4"> Actor 20</h2>
<h2 className="text-xl font-semibold mb-4"> 20</h2>
<div className="space-y-2 max-h-80 overflow-auto">
{summary.externalActorSummary.length === 0 ? (
<p className="text-gray-500"> Actor</p>
@@ -941,7 +967,7 @@ export default async function TrafficDashboard({
</div>
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-6">
<h2 className="text-xl font-semibold mb-4">Actor </h2>
<h2 className="text-xl font-semibold mb-4"></h2>
<div className="space-y-2">
{Object.entries(summary.actorSummary).map(([actorType, count]) => (
<div key={actorType} className="flex justify-between text-sm">
@@ -989,13 +1015,13 @@ export default async function TrafficDashboard({
<td className="py-2">
<div className="text-gray-200">{explainAction(actor.latestAction)}</div>
</td>
<td className="py-2 text-gray-300">{actor.latestTaskId}</td>
<td className="py-2 text-gray-300">{displayEntityId(actor.latestTaskId)}</td>
<td className="py-2 text-gray-300">{actor.latestSourceIp}</td>
<td className="py-2 text-gray-300">{actor.latestUserAgent}</td>
<td className="py-2 text-gray-300">{actor.latestRequestId}</td>
<td className="py-2">
<div className="text-gray-200">{actor.latestResponseStatus ?? "n/a"}</div>
<div className="text-xs text-gray-500">{actor.latestResponseSummary}</div>
<div className="text-xs text-gray-500">{displayResponseSummary(actor.latestResponseSummary)}</div>
</td>
</tr>
))
@@ -1012,8 +1038,8 @@ export default async function TrafficDashboard({
.filter(([action]) => action.startsWith("EXTERNAL_"))
.sort((a, b) => b[1] - a[1])
.slice(0, 14)
.map(([action, count]) => (
<div key={action} className="flex justify-between border-b border-gray-800 py-1">
.map(([action, count], index) => (
<div key={`external-summary-${index}`} className="flex justify-between border-b border-gray-800 py-1">
<span className="text-gray-300">{explainAction(action)}</span>
<span className="text-emerald-300">{count}</span>
</div>
@@ -1047,10 +1073,10 @@ export default async function TrafficDashboard({
<div key={event.id} className="border-b border-gray-800 py-2 text-sm">
<div className="text-emerald-300">{explainAction(event.action)}</div>
<div className="text-gray-400">
{event.actorId || "unknown"}{actorClassLabel((event as { actor_class?: TrafficActorClass }).actor_class || "other_external")}{event.entityId || "-"}{ts}
{event.actorId || "unknown"}{actorClassLabel((event as { actor_class?: TrafficActorClass }).actor_class || "other_external")}{displayEntityId(event.entityId)}{ts}
</div>
<div className="text-gray-500 text-xs mt-1">
{event.response_status ?? "n/a"} / {event.response_summary}
{event.response_status ?? "n/a"} / {displayResponseSummary(event.response_summary)}
</div>
</div>
);

View File

@@ -1,21 +1,38 @@
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import {
ADMIN_TRAFFIC_TOKEN_HEADER,
adminUnauthorizedResponse,
attachAdminHeaders,
isAdminRequestAuthorized,
stripClientAdminHeaders,
} from "@/lib/admin-auth";
const TRAFFIC_MONITOR_TOKEN = process.env.TRAFFIC_MONITOR_TOKEN?.trim();
export function middleware(request: NextRequest) {
const url = request.nextUrl;
const isAdminPath = url.pathname.startsWith("/admin");
const isTrafficDashboard = url.pathname === "/traffic";
const strippedHeaders = stripClientAdminHeaders(request);
if (isTrafficDashboard && !url.searchParams.get("token")) {
if (isTrafficDashboard && process.env.NODE_ENV === "production") {
const token = url.searchParams.get("token");
if (token && TRAFFIC_MONITOR_TOKEN && token === TRAFFIC_MONITOR_TOKEN) {
const cleanUrl = url.clone();
const headers = stripClientAdminHeaders(request);
cleanUrl.searchParams.delete("token");
headers.set(ADMIN_TRAFFIC_TOKEN_HEADER, TRAFFIC_MONITOR_TOKEN);
return NextResponse.rewrite(cleanUrl, {
request: {
headers,
},
});
}
const adminTrafficUrl = url.clone();
adminTrafficUrl.pathname = "/admin/traffic";
adminTrafficUrl.searchParams.delete("token");
return NextResponse.redirect(adminTrafficUrl);
}