"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 [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); 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 || ""); 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 || ""); 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(""); 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)); } } 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 [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 (
{tunnelEnabled ? "Using Tunnel" : "Using Local Server"}
Require API key
Requests without a valid key will be rejected
No API keys yet
Create your first API key to get started
{key.name}
{key.key}
Created {new Date(key.createdAt).toLocaleDateString()}
{key.isActive === false && (Paused
)}Save this key now!
This is the only time you will see this key. Store it securely.
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.
{benefit.title}
{benefit.desc}
Requires outbound port 7844 (TCP/UDP). Connection may take 10-30s.
Warning
The tunnel will be disconnected. Remote access will stop working.
Are you sure you want to disable the tunnel?