From e11e70ab19242876c74b786ed6015cb47e33b4da Mon Sep 17 00:00:00 2001 From: OG T Date: Thu, 11 Jun 2026 19:37:28 +0800 Subject: [PATCH] fix: sanitize traffic monitor auth surface --- apps/web/src/app/traffic/page.tsx | 42 +++++++++++++++++++++++++------ apps/web/src/middleware.ts | 19 +++++++++++++- 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/apps/web/src/app/traffic/page.tsx b/apps/web/src/app/traffic/page.tsx index 914fd3e..5492c2d 100644 --- a/apps/web/src/app/traffic/page.tsx +++ b/apps/web/src/app/traffic/page.tsx @@ -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({
-

外部來源 Actor 前 20

+

外部來源前 20

{summary.externalActorSummary.length === 0 ? (

目前區間內尚無外部 Actor。

@@ -941,7 +967,7 @@ export default async function TrafficDashboard({
-

Actor 類型分布

+

來源類型分布

{Object.entries(summary.actorSummary).map(([actorType, count]) => (
@@ -989,13 +1015,13 @@ export default async function TrafficDashboard({
{explainAction(actor.latestAction)}
- {actor.latestTaskId} + {displayEntityId(actor.latestTaskId)} {actor.latestSourceIp} {actor.latestUserAgent} {actor.latestRequestId}
{actor.latestResponseStatus ?? "n/a"}
-
{actor.latestResponseSummary}
+
{displayResponseSummary(actor.latestResponseSummary)}
)) @@ -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]) => ( -
+ .map(([action, count], index) => ( +
{explainAction(action)} {count}
@@ -1047,10 +1073,10 @@ export default async function TrafficDashboard({
{explainAction(event.action)}
- 來源:{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}
- 回應:{event.response_status ?? "n/a"} / {event.response_summary} + 回應:{event.response_status ?? "n/a"} / {displayResponseSummary(event.response_summary)}
); diff --git a/apps/web/src/middleware.ts b/apps/web/src/middleware.ts index fb16093..46b39c1 100644 --- a/apps/web/src/middleware.ts +++ b/apps/web/src/middleware.ts @@ -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); }