"use client"; import { useState, useEffect } from "react"; import PropTypes from "prop-types"; import { Card, Button, Input, Modal, CardSkeleton, Toggle } from "@/shared/components"; import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; /* ========== CLOUD CODE — COMMENTED OUT (replaced by Tunnel) ========== const DEFAULT_CLOUD_URL = process.env.NEXT_PUBLIC_CLOUD_URL || ""; const CLOUD_ACTION_TIMEOUT_MS = 15000; ========== END CLOUD CODE ========== */ const TUNNEL_BENEFITS = [ { icon: "public", title: "Access Anywhere", desc: "Use your API from any network" }, { icon: "group", title: "Share Endpoint", desc: "Share URL with team members" }, { icon: "code", title: "Use in Cursor/Cline", desc: "Connect AI tools remotely" }, { icon: "lock", title: "Encrypted", desc: "End-to-end TLS via Cloudflare" }, ]; const TUNNEL_ACTION_TIMEOUT_MS = 90000; export default function APIPageClient({ machineId }) { const [keys, setKeys] = useState([]); const [loading, setLoading] = useState(true); const [showAddModal, setShowAddModal] = useState(false); const [newKeyName, setNewKeyName] = useState(""); const [createdKey, setCreatedKey] = useState(null); /* ========== CLOUD STATE — COMMENTED OUT (replaced by Tunnel) ========== const [cloudEnabled, setCloudEnabled] = useState(false); const [cloudUrl, setCloudUrl] = useState(DEFAULT_CLOUD_URL); const [cloudUrlInput, setCloudUrlInput] = useState(DEFAULT_CLOUD_URL); const [cloudUrlSaving, setCloudUrlSaving] = useState(false); const [showCloudModal, setShowCloudModal] = useState(false); const [showDisableModal, setShowDisableModal] = useState(false); const [showSetupModal, setShowSetupModal] = useState(false); const [setupStatus, setSetupStatus] = useState(null); const [cloudSyncing, setCloudSyncing] = useState(false); const [cloudStatus, setCloudStatus] = useState(null); const [syncStep, setSyncStep] = useState(""); ========== END CLOUD STATE ========== */ // Tunnel state const [requireApiKey, setRequireApiKey] = useState(false); const [tunnelEnabled, setTunnelEnabled] = useState(false); const [tunnelUrl, setTunnelUrl] = useState(""); const [tunnelPublicUrl, setTunnelPublicUrl] = useState(""); const [tunnelShortId, setTunnelShortId] = useState(""); const [tunnelLoading, setTunnelLoading] = useState(false); const [tunnelProgress, setTunnelProgress] = useState(""); const [tunnelStatus, setTunnelStatus] = useState(null); const [showDisableModal, setShowDisableModal] = useState(false); const [showEnableModal, setShowEnableModal] = useState(false); // API key visibility toggle state const [visibleKeys, setVisibleKeys] = useState(new Set()); const { copied, copy } = useCopyToClipboard(); useEffect(() => { fetchData(); loadSettings(); }, []); /* ========== CLOUD FUNCTIONS — COMMENTED OUT (replaced by Tunnel) ========== const postCloudAction = async (action, timeoutMs = CLOUD_ACTION_TIMEOUT_MS) => { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeoutMs); try { const res = await fetch("/api/sync/cloud", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action }), signal: controller.signal, }); const data = await res.json().catch(() => ({})); return { ok: res.ok, status: res.status, data }; } catch (error) { if (error?.name === "AbortError") { return { ok: false, status: 408, data: { error: "Cloud request timeout" } }; } return { ok: false, status: 500, data: { error: error.message || "Cloud request failed" } }; } finally { clearTimeout(timeoutId); } }; const loadCloudSettings = async () => { try { const res = await fetch("/api/settings"); if (res.ok) { const data = await res.json(); setCloudEnabled(data.cloudEnabled || false); setRequireApiKey(data.requireApiKey || false); const url = data.cloudUrl || DEFAULT_CLOUD_URL; setCloudUrl(url); setCloudUrlInput(url); } } catch (error) { console.log("Error loading cloud settings:", error); } }; const handleCloudToggle = (checked) => { if (checked) { setShowCloudModal(true); } else { setShowDisableModal(true); } }; const handleEnableCloud = async () => { setCloudSyncing(true); setSyncStep("syncing"); try { const { ok, data } = await postCloudAction("enable"); if (ok) { setSyncStep("verifying"); if (data.verified) { setCloudEnabled(true); setCloudStatus({ type: "success", message: "Cloud Proxy connected and verified!" }); setShowCloudModal(false); } else { setCloudEnabled(true); setCloudStatus({ type: "warning", message: data.verifyError || "Connected but verification failed" }); setShowCloudModal(false); } if (data.createdKey) await fetchData(); } else { setCloudStatus({ type: "error", message: data.error || "Failed to enable cloud" }); } } catch (error) { setCloudStatus({ type: "error", message: error.message }); } finally { setCloudSyncing(false); setSyncStep(""); } }; const handleConfirmDisable = async () => { setCloudSyncing(true); setSyncStep("syncing"); try { await postCloudAction("sync"); setSyncStep("disabling"); const { ok, data } = await postCloudAction("disable"); if (ok) { setCloudEnabled(false); setCloudStatus({ type: "success", message: "Cloud disabled" }); setShowDisableModal(false); } else { setCloudStatus({ type: "error", message: data.error || "Failed to disable cloud" }); } } catch (error) { setCloudStatus({ type: "error", message: "Failed to disable cloud" }); } finally { setCloudSyncing(false); setSyncStep(""); } }; const handleSyncCloud = async () => { if (!cloudEnabled) return; setCloudSyncing(true); try { const { ok, data } = await postCloudAction("sync"); if (ok) setCloudStatus({ type: "success", message: "Synced successfully" }); else setCloudStatus({ type: "error", message: data.error }); } catch (error) { setCloudStatus({ type: "error", message: error.message }); } finally { setCloudSyncing(false); } }; const handleSaveCloudUrl = async () => { const trimmed = cloudUrlInput.trim().replace(/\/v1\/?$/, "").replace(/\/+$/, ""); if (!trimmed) return; setCloudUrlSaving(true); setSetupStatus(null); try { const res = await fetch("/api/settings", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ cloudUrl: trimmed }), }); if (res.ok) { setCloudUrl(trimmed); setCloudUrlInput(trimmed); setSetupStatus({ type: "success", message: "Worker URL saved" }); } else { setSetupStatus({ type: "error", message: "Failed to save Worker URL" }); } } catch (error) { setSetupStatus({ type: "error", message: error.message }); } finally { setCloudUrlSaving(false); } }; const handleCheckCloud = async () => { if (!cloudUrl) return; setCloudSyncing(true); setSetupStatus(null); try { const { ok, data } = await postCloudAction("check", 8000); if (ok) setSetupStatus({ type: "success", message: data.message || "Worker is running" }); else setSetupStatus({ type: "error", message: data.error || "Check failed" }); } catch { setSetupStatus({ type: "error", message: "Cannot reach worker" }); } finally { setCloudSyncing(false); } }; ========== END CLOUD FUNCTIONS ========== */ const loadSettings = async () => { try { const [settingsRes, tunnelRes] = await Promise.all([ fetch("/api/settings"), fetch("/api/tunnel/status") ]); if (settingsRes.ok) { const data = await settingsRes.json(); setRequireApiKey(data.requireApiKey || false); } if (tunnelRes.ok) { const data = await tunnelRes.json(); setTunnelEnabled(data.enabled || false); setTunnelUrl(data.tunnelUrl || ""); setTunnelPublicUrl(data.publicUrl || ""); setTunnelShortId(data.shortId || ""); } } catch (error) { console.log("Error loading settings:", error); } }; const handleRequireApiKey = async (value) => { try { const res = await fetch("/api/settings", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ requireApiKey: value }), }); if (res.ok) setRequireApiKey(value); } catch (error) { console.log("Error updating requireApiKey:", error); } }; const fetchData = async () => { try { const keysRes = await fetch("/api/keys"); const keysData = await keysRes.json(); if (keysRes.ok) { setKeys(keysData.keys || []); } } catch (error) { console.log("Error fetching data:", error); } finally { setLoading(false); } }; const handleEnableTunnel = async () => { setShowEnableModal(false); setTunnelLoading(true); setTunnelStatus(null); setTunnelProgress("Connecting to server..."); const progressSteps = [ { delay: 2000, msg: "Creating tunnel..." }, { delay: 5000, msg: "Starting cloudflared..." }, { delay: 15000, msg: "Establishing connections..." }, { delay: 30000, msg: "Waiting for tunnel ready..." }, ]; const timers = progressSteps.map(({ delay, msg }) => setTimeout(() => setTunnelProgress(msg), delay) ); try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), TUNNEL_ACTION_TIMEOUT_MS); const res = await fetch("/api/tunnel/enable", { method: "POST", signal: controller.signal, }); clearTimeout(timeoutId); timers.forEach(clearTimeout); const data = await res.json(); if (res.ok) { setTunnelEnabled(true); setTunnelUrl(data.tunnelUrl || ""); setTunnelPublicUrl(data.publicUrl || ""); setTunnelShortId(data.shortId || ""); setTunnelStatus({ type: "success", message: "Tunnel connected!" }); } else { setTunnelStatus({ type: "error", message: data.error || "Failed to enable tunnel" }); } } catch (error) { timers.forEach(clearTimeout); const msg = error?.name === "AbortError" ? "Tunnel creation timed out" : error.message; setTunnelStatus({ type: "error", message: msg }); } finally { setTunnelLoading(false); setTunnelProgress(""); } }; const handleDisableTunnel = async () => { setTunnelLoading(true); setTunnelStatus(null); try { const res = await fetch("/api/tunnel/disable", { method: "POST" }); const data = await res.json(); if (res.ok) { setTunnelEnabled(false); setTunnelUrl(""); setTunnelPublicUrl(""); setTunnelStatus({ type: "success", message: "Tunnel disabled" }); setShowDisableModal(false); } else { setTunnelStatus({ type: "error", message: data.error || "Failed to disable tunnel" }); } } catch (error) { setTunnelStatus({ type: "error", message: error.message }); } finally { setTunnelLoading(false); } }; const handleCreateKey = async () => { if (!newKeyName.trim()) return; try { const res = await fetch("/api/keys", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: newKeyName }), }); const data = await res.json(); if (res.ok) { setCreatedKey(data.key); await fetchData(); setNewKeyName(""); setShowAddModal(false); } } catch (error) { console.log("Error creating key:", error); } }; const handleDeleteKey = async (id) => { if (!confirm("Delete this API key?")) return; try { const res = await fetch(`/api/keys/${id}`, { method: "DELETE" }); if (res.ok) { setKeys(keys.filter((k) => k.id !== id)); // Clean up visibility state setVisibleKeys(prev => { const next = new Set(prev); next.delete(id); return next; }); } } catch (error) { console.log("Error deleting key:", error); } }; const handleToggleKey = async (id, isActive) => { try { const res = await fetch(`/api/keys/${id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ isActive }), }); if (res.ok) { setKeys(prev => prev.map(k => k.id === id ? { ...k, isActive } : k)); } } catch (error) { console.log("Error toggling key:", error); } }; const maskKey = (fullKey) => { if (!fullKey) return ""; return fullKey.length > 8 ? fullKey.slice(0, 8) + "..." : fullKey; }; const toggleKeyVisibility = (keyId) => { setVisibleKeys(prev => { const next = new Set(prev); if (next.has(keyId)) next.delete(keyId); else next.add(keyId); return next; }); }; const [baseUrl, setBaseUrl] = useState("/v1"); // Hydration fix: Only access window on client side useEffect(() => { if (typeof window !== "undefined") { setBaseUrl(`${window.location.origin}/v1`); } }, []); if (loading) { return (
); } const currentEndpoint = tunnelEnabled && tunnelPublicUrl ? `${tunnelPublicUrl}/v1` : baseUrl; return (
{/* Endpoint Card */}

API Endpoint

{tunnelEnabled ? "Using Tunnel" : "Using Local Server"}

{tunnelEnabled ? ( ) : ( )}
{/* Endpoint URL */}
{/* Tunnel Status */} {tunnelStatus && (
{tunnelStatus.message}
)}
{/* API Keys */}

API Keys

Require API key

Requests without a valid key will be rejected

handleRequireApiKey(!requireApiKey)} />
{keys.length === 0 ? (
vpn_key

No API keys yet

Create your first API key to get started

) : (
{keys.map((key) => (

{key.name}

{visibleKeys.has(key.id) ? key.key : maskKey(key.key)}

Created {new Date(key.createdAt).toLocaleDateString()}

{key.isActive === false && (

Paused

)}
{ if (key.isActive && !checked) { if (confirm(`Pause API key "${key.name}"?\n\nThis key will stop working immediately but can be resumed later.`)) { handleToggleKey(key.id, checked); } } else { handleToggleKey(key.id, checked); } }} title={key.isActive ? "Pause key" : "Resume key"} />
))}
)}
{/* CLOUD MODALS — COMMENTED OUT (replaced by Tunnel) */} {/* Setup Cloud Modal — removed */} {/* Cloud Enable Modal — removed */} {/* Add Key Modal */} { setShowAddModal(false); setNewKeyName(""); }} >
setNewKeyName(e.target.value)} placeholder="Production Key" />
{/* Created Key Modal */} setCreatedKey(null)} >

Save this key now!

This is the only time you will see this key. Store it securely.

{/* Enable Tunnel Modal */} setShowEnableModal(false)} >
cloud_upload

Cloudflare Tunnel

Expose your local 9Router to the internet. No port forwarding, no static IP needed. Share endpoint URL with your team or use it in Cursor, Cline, and other AI tools from anywhere.

{TUNNEL_BENEFITS.map((benefit) => (
{benefit.icon}

{benefit.title}

{benefit.desc}

))}

Requires outbound port 7844 (TCP/UDP). Connection may take 10-30s.

{/* Disable Tunnel Modal */} !tunnelLoading && setShowDisableModal(false)} >
warning

Warning

The tunnel will be disconnected. Remote access will stop working.

Are you sure you want to disable the tunnel?

); } APIPageClient.propTypes = { machineId: PropTypes.string.isRequired, };