fix: sanitize traffic monitor auth surface
All checks were successful
CI and Production Smoke / smoke (push) Successful in 7s
All checks were successful
CI and Production Smoke / smoke (push) Successful in 7s
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user