feat(proxy): add outbound HTTP proxy support for OAuth + provider requests
- Patch Node fetch via undici ProxyAgent when HTTP_PROXY/HTTPS_PROXY/ALL_PROXY is set - Ensure proxy patch is loaded for both chat pipeline and OAuth token exchange - Add Dashboard Settings → Network to edit outbound proxy and apply immediately - Persist outbound proxy settings in local db and initialize on server startup - Move proxy helpers to src/lib/network/ for better structure - Rename src/proxy.js → src/dashboardGuard.js to avoid naming confusion - Re-apply proxy env after DB import - Fix: close old dispatcher on proxy URL change to prevent connection pool leak - Fix: idempotency guard to avoid patching globalThis.fetch multiple times Made-with: Cursor
This commit is contained in:
1 parent
833069caac
commit
5a015e5b4d
14 files changed
+450
-29
No files matched your search
@@ -16,12 +16,25 @@ export default function ProfilePage() {
|
||||
const [dbLoading, setDbLoading] = useState(false);
|
||||
const [dbStatus, setDbStatus] = useState({ type: "", message: "" });
|
||||
const importFileRef = useRef(null);
|
||||
const [proxyForm, setProxyForm] = useState({
|
||||
outboundProxyEnabled: false,
|
||||
outboundProxyUrl: "",
|
||||
outboundNoProxy: "",
|
||||
});
|
||||
const [proxyStatus, setProxyStatus] = useState({ type: "", message: "" });
|
||||
const [proxyLoading, setProxyLoading] = useState(false);
|
||||
const [proxyTestLoading, setProxyTestLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/settings")
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
setSettings(data);
|
||||
setProxyForm({
|
||||
outboundProxyEnabled: data?.outboundProxyEnabled === true,
|
||||
outboundProxyUrl: data?.outboundProxyUrl || "",
|
||||
outboundNoProxy: data?.outboundNoProxy || "",
|
||||
});
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
@@ -30,6 +43,103 @@ export default function ProfilePage() {
|
||||
});
|
||||
}, []);
|
||||
|
||||
const updateOutboundProxy = async (e) => {
|
||||
e.preventDefault();
|
||||
if (settings.outboundProxyEnabled !== true) return;
|
||||
setProxyLoading(true);
|
||||
setProxyStatus({ type: "", message: "" });
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/settings", {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
outboundProxyUrl: proxyForm.outboundProxyUrl,
|
||||
outboundNoProxy: proxyForm.outboundNoProxy,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
setSettings((prev) => ({ ...prev, ...data }));
|
||||
setProxyStatus({ type: "success", message: "Proxy settings applied" });
|
||||
} else {
|
||||
setProxyStatus({ type: "error", message: data.error || "Failed to update proxy settings" });
|
||||
}
|
||||
} catch (err) {
|
||||
setProxyStatus({ type: "error", message: "An error occurred" });
|
||||
} finally {
|
||||
setProxyLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const testOutboundProxy = async () => {
|
||||
if (settings.outboundProxyEnabled !== true) return;
|
||||
|
||||
const proxyUrl = (proxyForm.outboundProxyUrl || "").trim();
|
||||
if (!proxyUrl) {
|
||||
setProxyStatus({ type: "error", message: "Please enter a Proxy URL to test" });
|
||||
return;
|
||||
}
|
||||
|
||||
setProxyTestLoading(true);
|
||||
setProxyStatus({ type: "", message: "" });
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/settings/proxy-test", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ proxyUrl }),
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
if (res.ok && data?.ok) {
|
||||
setProxyStatus({
|
||||
type: "success",
|
||||
message: `Proxy test OK (${data.status}) in ${data.elapsedMs}ms`,
|
||||
});
|
||||
} else {
|
||||
setProxyStatus({
|
||||
type: "error",
|
||||
message: data?.error || "Proxy test failed",
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
setProxyStatus({ type: "error", message: "An error occurred" });
|
||||
} finally {
|
||||
setProxyTestLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const updateOutboundProxyEnabled = async (outboundProxyEnabled) => {
|
||||
setProxyLoading(true);
|
||||
setProxyStatus({ type: "", message: "" });
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/settings", {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ outboundProxyEnabled }),
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
setSettings((prev) => ({ ...prev, ...data }));
|
||||
setProxyForm((prev) => ({ ...prev, outboundProxyEnabled: data?.outboundProxyEnabled === true }));
|
||||
setProxyStatus({
|
||||
type: "success",
|
||||
message: outboundProxyEnabled ? "Proxy enabled" : "Proxy disabled",
|
||||
});
|
||||
} else {
|
||||
setProxyStatus({ type: "error", message: data.error || "Failed to update proxy settings" });
|
||||
}
|
||||
} catch (err) {
|
||||
setProxyStatus({ type: "error", message: "An error occurred" });
|
||||
} finally {
|
||||
setProxyLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePasswordChange = async (e) => {
|
||||
e.preventDefault();
|
||||
if (passwords.new !== passwords.confirm) {
|
||||
@@ -379,6 +489,77 @@ export default function ProfilePage() {
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Network */}
|
||||
<Card>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="p-2 rounded-lg bg-purple-500/10 text-purple-500">
|
||||
<span className="material-symbols-outlined text-[20px]">wifi</span>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold">Network</h3>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-medium">Outbound Proxy</p>
|
||||
<p className="text-sm text-text-muted">Enable proxy for OAuth + provider outbound requests.</p>
|
||||
</div>
|
||||
<Toggle
|
||||
checked={settings.outboundProxyEnabled === true}
|
||||
onChange={() => updateOutboundProxyEnabled(!(settings.outboundProxyEnabled === true))}
|
||||
disabled={loading || proxyLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{settings.outboundProxyEnabled === true && (
|
||||
<form onSubmit={updateOutboundProxy} className="flex flex-col gap-4 pt-2 border-t border-border/50">
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="font-medium">Proxy URL</label>
|
||||
<Input
|
||||
placeholder="http://127.0.0.1:7897"
|
||||
value={proxyForm.outboundProxyUrl}
|
||||
onChange={(e) => setProxyForm((prev) => ({ ...prev, outboundProxyUrl: e.target.value }))}
|
||||
disabled={loading || proxyLoading}
|
||||
/>
|
||||
<p className="text-sm text-text-muted">Leave empty to inherit existing env proxy (if any).</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 pt-2 border-t border-border/50">
|
||||
<label className="font-medium">No Proxy</label>
|
||||
<Input
|
||||
placeholder="localhost,127.0.0.1"
|
||||
value={proxyForm.outboundNoProxy}
|
||||
onChange={(e) => setProxyForm((prev) => ({ ...prev, outboundNoProxy: e.target.value }))}
|
||||
disabled={loading || proxyLoading}
|
||||
/>
|
||||
<p className="text-sm text-text-muted">Comma-separated hostnames/domains to bypass the proxy.</p>
|
||||
</div>
|
||||
|
||||
<div className="pt-2 border-t border-border/50 flex items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
loading={proxyTestLoading}
|
||||
disabled={loading || proxyLoading}
|
||||
onClick={testOutboundProxy}
|
||||
>
|
||||
Test proxy URL
|
||||
</Button>
|
||||
<Button type="submit" variant="primary" loading={proxyLoading}>
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{proxyStatus.message && (
|
||||
<p className={`text-sm ${proxyStatus.type === "error" ? "text-red-500" : "text-green-500"} pt-2 border-t border-border/50`}>
|
||||
{proxyStatus.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Theme Preferences */}
|
||||
<Card>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
|
||||
Reference in new issue
Block a user