mirror of
https://github.com/decolua/9router.git
synced 2026-05-08 12:01:28 +00:00
Fix MITM
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* OpenAI to Cursor Request Translator
|
||||
* - assistant tool_calls → kept as-is (Cursor generates tool calls)
|
||||
* - Claude tool_use blocks → converted to OpenAI tool_calls format
|
||||
* - tool results → converted to user message string
|
||||
*/
|
||||
import { register } from "../index.js";
|
||||
@@ -14,6 +15,16 @@ function extractContent(content) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Build a map of tool_use_id → tool_name from the previous assistant message
|
||||
function getToolNameMap(prevMsg) {
|
||||
const map = {};
|
||||
if (!prevMsg?.tool_calls) return map;
|
||||
for (const tc of prevMsg.tool_calls) {
|
||||
if (tc.id && tc.function?.name) map[tc.id] = tc.function.name;
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
function convertMessages(messages) {
|
||||
const result = [];
|
||||
|
||||
@@ -26,7 +37,25 @@ function convertMessages(messages) {
|
||||
}
|
||||
|
||||
if (msg.role === "user") {
|
||||
result.push({ role: "user", content: extractContent(msg.content) || "" });
|
||||
if (Array.isArray(msg.content)) {
|
||||
const parts = [];
|
||||
const prevMsg = result[result.length - 1];
|
||||
const nameMap = getToolNameMap(prevMsg);
|
||||
for (const block of msg.content) {
|
||||
if (block.type === "text") {
|
||||
parts.push(block.text);
|
||||
} else if (block.type === "tool_result") {
|
||||
// Claude format: user message with tool_result blocks
|
||||
const toolResultText = extractContent(block.content) || "";
|
||||
const toolCallId = block.tool_use_id || "";
|
||||
const toolName = nameMap[toolCallId] || "";
|
||||
parts.push(`<tool_result>\n<tool_name>${toolName}</tool_name>\n<tool_call_id>${toolCallId}</tool_call_id>\n<result>${toolResultText}</result>\n</tool_result>`);
|
||||
}
|
||||
}
|
||||
result.push({ role: "user", content: parts.join("\n") || "" });
|
||||
} else {
|
||||
result.push({ role: "user", content: extractContent(msg.content) || "" });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -34,10 +63,10 @@ function convertMessages(messages) {
|
||||
// Strip system-reminder tags injected by Claude Code
|
||||
const raw = extractContent(msg.content) || "";
|
||||
const toolContent = raw.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, "").trim();
|
||||
// Find matching tool name from previous assistant message
|
||||
const prevMsg = result[result.length - 1];
|
||||
const toolName = prevMsg?.tool_calls?.[0]?.function?.name || "";
|
||||
const nameMap = getToolNameMap(prevMsg);
|
||||
const toolCallId = msg.tool_call_id || "";
|
||||
const toolName = nameMap[toolCallId] || "";
|
||||
result.push({
|
||||
role: "user",
|
||||
content: `<tool_result>\n<tool_name>${toolName}</tool_name>\n<tool_call_id>${toolCallId}</tool_call_id>\n<result>${toolContent}</result>\n</tool_result>`
|
||||
@@ -46,10 +75,28 @@ function convertMessages(messages) {
|
||||
}
|
||||
|
||||
if (msg.role === "assistant") {
|
||||
const content = extractContent(msg.content) || "";
|
||||
let content = extractContent(msg.content) || "";
|
||||
let tool_calls = null;
|
||||
|
||||
if (msg.tool_calls && msg.tool_calls.length > 0) {
|
||||
// Strip `index` field — not needed in history, may confuse Cursor
|
||||
const tool_calls = msg.tool_calls.map(({ index, ...tc }) => tc);
|
||||
// OpenAI format: strip `index` field
|
||||
tool_calls = msg.tool_calls.map(({ index, ...tc }) => tc);
|
||||
} else if (Array.isArray(msg.content)) {
|
||||
// Claude format: extract tool_use blocks from content array
|
||||
const extracted = msg.content
|
||||
.filter(b => b.type === "tool_use")
|
||||
.map(b => ({
|
||||
id: b.id,
|
||||
type: "function",
|
||||
function: {
|
||||
name: b.name,
|
||||
arguments: JSON.stringify(b.input || {})
|
||||
}
|
||||
}));
|
||||
if (extracted.length > 0) tool_calls = extracted;
|
||||
}
|
||||
|
||||
if (tool_calls) {
|
||||
result.push({ role: "assistant", content, tool_calls });
|
||||
} else if (content) {
|
||||
result.push({ role: "assistant", content });
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "9router-app",
|
||||
"version": "0.2.92",
|
||||
"version": "0.2.95",
|
||||
"description": "9Router web dashboard",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -18,19 +18,6 @@ export default function ProviderDetailPage() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [providerNode, setProviderNode] = useState(null);
|
||||
const [showOAuthModal, setShowOAuthModal] = useState(false);
|
||||
|
||||
// Auto-reopen OAuthModal if pending auth exists (survives HMR/reload)
|
||||
useEffect(() => {
|
||||
try {
|
||||
const raw = localStorage.getItem("oauth_pending_auth");
|
||||
if (raw) {
|
||||
const data = JSON.parse(raw);
|
||||
if (data.provider === providerId && Date.now() - data.timestamp < 300000) {
|
||||
setShowOAuthModal(true);
|
||||
}
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}, [providerId]);
|
||||
const [showAddApiKeyModal, setShowAddApiKeyModal] = useState(false);
|
||||
const [showEditModal, setShowEditModal] = useState(false);
|
||||
const [showEditNodeModal, setShowEditNodeModal] = useState(false);
|
||||
|
||||
@@ -3,43 +3,6 @@
|
||||
import { Suspense, useEffect, useState } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
|
||||
const OAUTH_SESSION_KEY = "oauth_pending_auth";
|
||||
|
||||
/**
|
||||
* Direct exchange: callback page calls exchange API itself
|
||||
* when relay to opener fails (e.g. HMR reload destroyed listeners)
|
||||
*/
|
||||
async function directExchange(code, state) {
|
||||
try {
|
||||
const raw = localStorage.getItem(OAUTH_SESSION_KEY);
|
||||
if (!raw) return false;
|
||||
|
||||
const session = JSON.parse(raw);
|
||||
// Expired (5 min)
|
||||
if (Date.now() - session.timestamp > 300000) {
|
||||
localStorage.removeItem(OAUTH_SESSION_KEY);
|
||||
return false;
|
||||
}
|
||||
|
||||
const res = await fetch(`/api/oauth/${session.provider}/exchange`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
code,
|
||||
redirectUri: session.redirectUri,
|
||||
codeVerifier: session.codeVerifier,
|
||||
state,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
localStorage.removeItem(OAUTH_SESSION_KEY);
|
||||
return res.ok && data.success;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* OAuth Callback Page Content
|
||||
*/
|
||||
@@ -102,43 +65,11 @@ function CallbackContent() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
setTimeout(() => {
|
||||
setStatus("success");
|
||||
setTimeout(() => {
|
||||
window.close();
|
||||
setTimeout(() => setStatus("done"), 500);
|
||||
}, 1500);
|
||||
}, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Try direct exchange FIRST (before relay may be lost to HMR)
|
||||
// Then relay as backup for normal flow
|
||||
const handleExchange = async () => {
|
||||
const pending = localStorage.getItem(OAUTH_SESSION_KEY);
|
||||
if (pending) {
|
||||
// Direct exchange - works even if opener was destroyed by HMR
|
||||
const ok = await directExchange(code, state);
|
||||
if (ok) {
|
||||
setStatus("success");
|
||||
setTimeout(() => {
|
||||
window.close();
|
||||
setTimeout(() => setStatus("done"), 500);
|
||||
}, 1500);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: relay succeeded and OAuthModal handled it
|
||||
setStatus("success");
|
||||
setTimeout(() => {
|
||||
window.close();
|
||||
setTimeout(() => setStatus("done"), 500);
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
handleExchange();
|
||||
setStatus("success");
|
||||
setTimeout(() => {
|
||||
window.close();
|
||||
setTimeout(() => setStatus("done"), 500);
|
||||
}, 1500);
|
||||
}, [searchParams]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -3,6 +3,7 @@ const path = require("path");
|
||||
const fs = require("fs");
|
||||
const os = require("os");
|
||||
const net = require("net");
|
||||
const https = require("https");
|
||||
const crypto = require("crypto");
|
||||
const { addDNSEntry, removeDNSEntry, checkDNSEntry } = require("./dns/dnsConfig");
|
||||
|
||||
@@ -49,12 +50,14 @@ function getCachedPassword() { return globalThis.__mitmSudoPassword || null; }
|
||||
function setCachedPassword(pwd) { globalThis.__mitmSudoPassword = pwd; }
|
||||
|
||||
// Check if a PID is alive
|
||||
// EACCES = process exists but no permission (e.g. root process) → still alive
|
||||
// ESRCH = process does not exist → dead
|
||||
function isProcessAlive(pid) {
|
||||
try {
|
||||
process.kill(pid, 0);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
} catch (err) {
|
||||
return err.code === "EACCES";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,53 +168,36 @@ function checkPort443Free() {
|
||||
* Get PID and process name currently holding port 443
|
||||
* Returns { pid, name } or null if port is free / cannot determine
|
||||
*/
|
||||
function getPort443Owner() {
|
||||
function getPort443Owner(sudoPassword) {
|
||||
return new Promise((resolve) => {
|
||||
const cmd = IS_WIN
|
||||
? `netstat -ano | findstr ":443 "`
|
||||
// Only match TCP processes actually LISTEN-ing on port 443 (not outbound UDP/QUIC)
|
||||
: `lsof -i TCP:${MITM_PORT} -n -P -sTCP:LISTEN`;
|
||||
|
||||
exec(cmd, (err, stdout) => {
|
||||
if (err || !stdout.trim()) return resolve(null);
|
||||
|
||||
let pid = null;
|
||||
|
||||
if (IS_WIN) {
|
||||
// netstat line: " TCP 0.0.0.0:443 0.0.0.0:0 LISTENING 1234"
|
||||
if (IS_WIN) {
|
||||
exec(`netstat -ano | findstr ":443 "`, (err, stdout) => {
|
||||
if (err || !stdout.trim()) return resolve(null);
|
||||
for (const line of stdout.split("\n")) {
|
||||
const match = line.match(/LISTENING\s+(\d+)/i);
|
||||
if (match) { pid = parseInt(match[1], 10); break; }
|
||||
}
|
||||
} else {
|
||||
// lsof line: "node 1234 user ..."
|
||||
for (const line of stdout.split("\n").slice(1)) {
|
||||
const parts = line.trim().split(/\s+/);
|
||||
if (parts.length >= 2) { pid = parseInt(parts[1], 10); break; }
|
||||
}
|
||||
}
|
||||
|
||||
if (!pid || isNaN(pid)) return resolve(null);
|
||||
|
||||
// Get process name by PID
|
||||
const nameCmd = IS_WIN
|
||||
? `tasklist /FI "PID eq ${pid}" /FO CSV /NH`
|
||||
: `ps -p ${pid} -o comm=`;
|
||||
|
||||
exec(nameCmd, (e2, out2) => {
|
||||
let name = "unknown";
|
||||
if (!e2 && out2.trim()) {
|
||||
if (IS_WIN) {
|
||||
// CSV: "node.exe","1234",...
|
||||
const m = out2.match(/"([^"]+)"/);
|
||||
if (m) name = m[1];
|
||||
} else {
|
||||
name = out2.trim();
|
||||
if (match) {
|
||||
const pid = parseInt(match[1], 10);
|
||||
exec(`tasklist /FI "PID eq ${pid}" /FO CSV /NH`, (e2, out2) => {
|
||||
const m = out2?.match(/"([^"]+)"/);
|
||||
resolve({ pid, name: m ? m[1] : "unknown" });
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
resolve({ pid, name });
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Use ps to find node process running server.js (no sudo needed)
|
||||
exec(`ps aux | grep "[s]erver.js"`, (err, stdout) => {
|
||||
if (!stdout?.trim()) return resolve(null);
|
||||
for (const line of stdout.split("\n")) {
|
||||
const parts = line.trim().split(/\s+/);
|
||||
const pid = parseInt(parts[1], 10);
|
||||
if (!isNaN(pid)) return resolve({ pid, name: "node" });
|
||||
}
|
||||
resolve(null);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -254,6 +240,37 @@ async function killLeftoverMitm(sudoPassword) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll MITM health endpoint until server is up or timeout.
|
||||
* Returns { ok, pid } on success, null on timeout.
|
||||
*/
|
||||
function pollMitmHealth(timeoutMs) {
|
||||
return new Promise((resolve) => {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
const check = () => {
|
||||
const req = https.request(
|
||||
{ hostname: "127.0.0.1", port: 443, path: "/_mitm_health", method: "GET", rejectUnauthorized: false },
|
||||
(res) => {
|
||||
let body = "";
|
||||
res.on("data", (d) => { body += d; });
|
||||
res.on("end", () => {
|
||||
try {
|
||||
const json = JSON.parse(body);
|
||||
resolve(json.ok === true ? { ok: true, pid: json.pid || null } : null);
|
||||
} catch { resolve(null); }
|
||||
});
|
||||
}
|
||||
);
|
||||
req.on("error", () => {
|
||||
if (Date.now() < deadline) setTimeout(check, 500);
|
||||
else resolve(null);
|
||||
});
|
||||
req.end();
|
||||
};
|
||||
check();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get MITM status
|
||||
*/
|
||||
@@ -319,19 +336,33 @@ async function startMitm(apiKey, sudoPassword) {
|
||||
await killLeftoverMitm(sudoPassword);
|
||||
|
||||
// Check port 443 availability BEFORE modifying system
|
||||
// "no-permission" = EACCES: port may be held by a root process, check via lsof/netstat
|
||||
const portStatus = await checkPort443Free();
|
||||
if (portStatus === "in-use") {
|
||||
const owner = await getPort443Owner();
|
||||
let ownerDesc = "another process";
|
||||
if (owner) {
|
||||
if (portStatus === "in-use" || portStatus === "no-permission") {
|
||||
const owner = await getPort443Owner(sudoPassword);
|
||||
if (owner && owner.name === "node") {
|
||||
// Orphan MITM node process — kill it and continue
|
||||
console.log(`[MITM] Killing orphan node process on port 443 (PID ${owner.pid})...`);
|
||||
try {
|
||||
if (IS_WIN) {
|
||||
await new Promise((resolve) => exec(`taskkill /F /PID ${owner.pid}`, resolve));
|
||||
} else {
|
||||
const { execWithPassword } = require("./dns/dnsConfig");
|
||||
await execWithPassword(`kill -9 ${owner.pid}`, sudoPassword);
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 800));
|
||||
} catch {
|
||||
// best effort — continue anyway
|
||||
}
|
||||
} else if (owner) {
|
||||
const shortName = owner.name.includes("/")
|
||||
? owner.name.split("/").filter(Boolean).pop()
|
||||
: owner.name;
|
||||
ownerDesc = `"${shortName}" (PID ${owner.pid})`;
|
||||
throw new Error(
|
||||
`Port 443 is already in use by "${shortName}" (PID ${owner.pid}). Stop that process first, then retry.`
|
||||
);
|
||||
}
|
||||
throw new Error(
|
||||
`Port 443 is already in use by ${ownerDesc}. Stop that process first, then retry.`
|
||||
);
|
||||
// owner === null + no-permission → likely just needs sudo, proceed
|
||||
}
|
||||
|
||||
// 1. Generate SSL certificate if not exists
|
||||
@@ -358,12 +389,13 @@ async function startMitm(apiKey, sudoPassword) {
|
||||
console.log("Starting MITM server...");
|
||||
|
||||
if (IS_WIN) {
|
||||
const nodePath = process.execPath;
|
||||
const envArgs = `$env:ROUTER_API_KEY='${apiKey}'; $env:NODE_ENV='production'; & '${nodePath}' '${SERVER_PATH}'`;
|
||||
// Launch elevated node via PowerShell RunAs (triggers UAC prompt)
|
||||
const nodePath = process.execPath.replace(/'/g, "''");
|
||||
const serverPath = SERVER_PATH.replace(/'/g, "''");
|
||||
serverProcess = spawn("powershell", [
|
||||
"-Command",
|
||||
`Start-Process powershell -ArgumentList '-NoProfile','-Command','${envArgs.replace(/'/g, "''")}' -Verb RunAs -PassThru`
|
||||
], { detached: false, stdio: ["ignore", "pipe", "pipe"] });
|
||||
"-NoProfile", "-Command",
|
||||
`$env:ROUTER_API_KEY='${apiKey}'; $env:NODE_ENV='production'; Start-Process '${nodePath}' -ArgumentList '''${serverPath}''' -Verb RunAs -WindowStyle Hidden`
|
||||
], { stdio: "ignore", env: process.env });
|
||||
} else {
|
||||
// sudo -S: read password from stdin, -E: preserve env vars
|
||||
// Pass ROUTER_API_KEY inline via env=... wrapper to avoid sudo stripping env
|
||||
@@ -399,20 +431,22 @@ async function startMitm(apiKey, sudoPassword) {
|
||||
try { fs.unlinkSync(PID_FILE); } catch { /* ignore */ }
|
||||
});
|
||||
|
||||
// Wait up to 8s — sudo + Node startup takes longer than plain spawn
|
||||
const started = await new Promise((resolve) => {
|
||||
let resolved = false;
|
||||
const done = (val) => { if (!resolved) { resolved = true; resolve(val); } };
|
||||
const timeout = setTimeout(() => done(true), 8000);
|
||||
serverProcess.once("exit", () => { clearTimeout(timeout); done(false); });
|
||||
});
|
||||
// Wait for server to be ready by polling health endpoint
|
||||
const health = await pollMitmHealth(IS_WIN ? 12000 : 8000);
|
||||
|
||||
if (!started) {
|
||||
if (!health) {
|
||||
if (IS_WIN) serverProcess = null;
|
||||
try { await removeDNSEntry(sudoPassword); } catch { /* best effort */ }
|
||||
const reason = startError || "Check sudo password or port 443 access.";
|
||||
throw new Error(`MITM server failed to start. ${reason}`);
|
||||
}
|
||||
|
||||
// On Windows, use real PID from health check (launcher exits immediately after UAC)
|
||||
if (IS_WIN && health.pid) {
|
||||
serverPid = health.pid;
|
||||
fs.writeFileSync(PID_FILE, String(serverPid));
|
||||
}
|
||||
|
||||
await saveMitmSettings(true, sudoPassword);
|
||||
if (sudoPassword) setCachedPassword(sudoPassword);
|
||||
|
||||
|
||||
@@ -173,6 +173,13 @@ async function intercept(req, res, bodyBuffer, mappedModel) {
|
||||
}
|
||||
|
||||
const server = https.createServer(sslOptions, async (req, res) => {
|
||||
// Health check endpoint for startup verification
|
||||
if (req.url === "/_mitm_health") {
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ ok: true, pid: process.pid }));
|
||||
return;
|
||||
}
|
||||
|
||||
const bodyBuffer = await collectBodyRaw(req);
|
||||
|
||||
// Save request log if enabled
|
||||
|
||||
@@ -5,34 +5,6 @@ import PropTypes from "prop-types";
|
||||
import { Modal, Button, Input } from "@/shared/components";
|
||||
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
|
||||
|
||||
const OAUTH_SESSION_KEY = "oauth_pending_auth";
|
||||
|
||||
function saveAuthSession(provider, data) {
|
||||
try {
|
||||
localStorage.setItem(OAUTH_SESSION_KEY, JSON.stringify({ provider, ...data, timestamp: Date.now() }));
|
||||
} catch { /* storage unavailable */ }
|
||||
}
|
||||
|
||||
function loadAuthSession(provider) {
|
||||
try {
|
||||
const raw = localStorage.getItem(OAUTH_SESSION_KEY);
|
||||
if (!raw) return null;
|
||||
const data = JSON.parse(raw);
|
||||
// Only restore if same provider and within 5 minutes
|
||||
if (data.provider !== provider || Date.now() - data.timestamp > 300000) {
|
||||
localStorage.removeItem(OAUTH_SESSION_KEY);
|
||||
return null;
|
||||
}
|
||||
return data;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function clearAuthSession() {
|
||||
try { localStorage.removeItem(OAUTH_SESSION_KEY); } catch { /* ignore */ }
|
||||
}
|
||||
|
||||
/**
|
||||
* OAuth Modal Component
|
||||
* - Localhost: Auto callback via popup message
|
||||
@@ -84,11 +56,9 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error);
|
||||
|
||||
clearAuthSession();
|
||||
setStep("success");
|
||||
onSuccess?.();
|
||||
} catch (err) {
|
||||
clearAuthSession();
|
||||
setError(err.message);
|
||||
setStep("error");
|
||||
}
|
||||
@@ -182,7 +152,6 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
|
||||
if (!res.ok) throw new Error(data.error);
|
||||
|
||||
setAuthData({ ...data, redirectUri });
|
||||
saveAuthSession(provider, { codeVerifier: data.codeVerifier, redirectUri, state: data.state });
|
||||
|
||||
// For Codex or non-localhost: use manual input mode
|
||||
if (provider === "codex" || !isLocalhost) {
|
||||
@@ -207,19 +176,6 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
|
||||
// Reset state and start OAuth when modal opens
|
||||
useEffect(() => {
|
||||
if (isOpen && provider) {
|
||||
// Try restore pending auth from localStorage (survives HMR/reload)
|
||||
const saved = loadAuthSession(provider);
|
||||
if (saved) {
|
||||
setAuthData({ codeVerifier: saved.codeVerifier, redirectUri: saved.redirectUri, state: saved.state });
|
||||
setStep("waiting");
|
||||
setCallbackUrl("");
|
||||
setError(null);
|
||||
setIsDeviceCode(false);
|
||||
setDeviceData(null);
|
||||
setPolling(false);
|
||||
return; // Don't restart OAuth — just re-listen for callback
|
||||
}
|
||||
|
||||
setAuthData(null);
|
||||
setCallbackUrl("");
|
||||
setError(null);
|
||||
@@ -249,14 +205,6 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
|
||||
}
|
||||
|
||||
if (code) {
|
||||
// Skip if callback page already handled exchange (localStorage cleared)
|
||||
const stillPending = localStorage.getItem(OAUTH_SESSION_KEY);
|
||||
if (!stillPending) {
|
||||
callbackProcessedRef.current = true;
|
||||
setStep("success");
|
||||
onSuccess?.();
|
||||
return;
|
||||
}
|
||||
callbackProcessedRef.current = true;
|
||||
await exchangeTokens(code, state);
|
||||
}
|
||||
@@ -303,11 +251,10 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
|
||||
const stored = localStorage.getItem("oauth_callback");
|
||||
if (stored) {
|
||||
const data = JSON.parse(stored);
|
||||
// Only use if recent (within 30 seconds)
|
||||
if (data.timestamp && Date.now() - data.timestamp < 30000) {
|
||||
handleCallback(data);
|
||||
localStorage.removeItem("oauth_callback");
|
||||
}
|
||||
localStorage.removeItem("oauth_callback");
|
||||
}
|
||||
} catch {
|
||||
// localStorage may be unavailable or data may be malformed - ignore silently
|
||||
@@ -346,7 +293,6 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
|
||||
|
||||
// Clear session on modal close
|
||||
const handleClose = useCallback(() => {
|
||||
clearAuthSession();
|
||||
onClose();
|
||||
}, [onClose]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user