feat(ui): system proxy exit guard via beforeunload + sendBeacon (fix2)

This commit is contained in:
diegosouzapw
2026-05-28 11:21:09 -03:00
parent 53754ae93c
commit bdd65cdd5e
2 changed files with 79 additions and 1 deletions

View File

@@ -1,11 +1,12 @@
"use client";
import { useCallback, useRef, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import type { InterceptedRequest } from "@/mitm/inspector/types";
import { useTrafficStream } from "./hooks/useTrafficStream";
import { useTrafficFilters } from "./hooks/useTrafficFilters";
import { useResizablePanels } from "./hooks/useResizablePanels";
import { useSessionRecorder } from "./hooks/useSessionRecorder";
import { useSystemProxyExitGuard } from "./hooks/useSystemProxyExitGuard";
import { CaptureModesToolbar } from "./components/CaptureModesToolbar";
import { TopBarControls } from "./components/TopBarControls";
import { RequestStreamingList } from "./components/RequestStreamingList";
@@ -22,6 +23,26 @@ export function TrafficInspectorPageClient() {
const [{ listWidth, collapsed }, { startDrag, toggleCollapse }] = useResizablePanels();
const [streamState, streamActions] = useTrafficStream(filters);
const recorder = useSessionRecorder();
const [captureModes, setCaptureModes] = useState<{ systemProxy?: { applied: boolean } } | null>(
null
);
useEffect(() => {
let cancelled = false;
fetch("/api/tools/traffic-inspector/capture-modes")
.then((r) => (r.ok ? r.json() : null))
.then((data: { systemProxy?: { applied: boolean } } | null) => {
if (!cancelled) setCaptureModes(data);
})
.catch(() => {
/* best-effort */
});
return () => {
cancelled = true;
};
}, []);
useSystemProxyExitGuard({ applied: captureModes?.systemProxy?.applied ?? false });
const listContainerCallback = useCallback((el: HTMLDivElement | null) => {
listContainerRef.current = el;

View File

@@ -0,0 +1,57 @@
"use client";
import { useEffect, useRef } from "react";
interface UseSystemProxyExitGuardOpts {
applied: boolean; // current state (from GET capture-modes)
endpoint?: string; // POST /capture-modes/system-proxy
}
/**
* On unmount / page hide / beforeunload, if system proxy is applied,
* silently fires a revert request via navigator.sendBeacon (best-effort,
* survives unload) AND attaches a beforeunload listener that prompts the
* user with a native confirm dialog (browser default — text is ignored
* by most browsers but the prompt itself appears).
*/
export function useSystemProxyExitGuard(opts: UseSystemProxyExitGuardOpts): void {
// 1. Track latest 'applied' in a ref so the listener always sees fresh value
const appliedRef = useRef(opts.applied);
useEffect(() => {
appliedRef.current = opts.applied;
}, [opts.applied]);
useEffect(() => {
const endpoint =
opts.endpoint ?? "/api/tools/traffic-inspector/capture-modes/system-proxy";
const body = JSON.stringify({ action: "revert" });
const blob = new Blob([body], { type: "application/json" });
const beforeUnload = (e: BeforeUnloadEvent) => {
if (!appliedRef.current) return;
// Best-effort revert via sendBeacon (survives navigation)
try {
navigator.sendBeacon(endpoint, blob);
} catch {
/* ignore */
}
// Show confirmation prompt
e.preventDefault();
e.returnValue = "System-wide proxy still active — leave page anyway?";
return e.returnValue;
};
window.addEventListener("beforeunload", beforeUnload);
return () => {
window.removeEventListener("beforeunload", beforeUnload);
// On component unmount (SPA navigation), fire revert too
if (appliedRef.current) {
try {
navigator.sendBeacon(endpoint, blob);
} catch {
/* ignore */
}
}
};
}, [opts.endpoint]);
}