"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"; const DEFAULT_CLOUD_URL = process.env.NEXT_PUBLIC_CLOUD_URL || ""; const CLOUD_ACTION_TIMEOUT_MS = 15000; 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 sync state const [requireApiKey, setRequireApiKey] = useState(false); 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(""); // "syncing" | "verifying" | "disabling" | "" const { copied, copy } = useCopyToClipboard(); useEffect(() => { fetchData(); loadCloudSettings(); }, []); 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 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 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); } // Refresh keys list if new key was created 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 { // Step 1: Sync latest data from cloud await postCloudAction("sync"); setSyncStep("disabling"); // Step 2: Disable cloud 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) { console.log("Error disabling cloud:", 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 () => { // Strip trailing /v1 or /v1/ and trailing slashes 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); } }; 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"); const cloudEndpointNew = cloudUrl ? `${cloudUrl}/v1` : ""; // Hydration fix: Only access window on client side useEffect(() => { if (typeof window !== "undefined") { setBaseUrl(`${window.location.origin}/v1`); } }, []); if (loading) { return (
{cloudEnabled ? "Using Cloud Proxy" : "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
)}
https://9router.com is a pre-configured worker ready to use. You can also deploy your own.
Worker URL
Deploy your own worker from app/cloud/ directory.{" "}
Setup guide →
What you will get
Note
{syncStep === "syncing" && "Syncing data to cloud..."} {syncStep === "verifying" && "Verifying connection..."}
Save this key now!
This is the only time you will see this key. Store it securely.
Warning
All auth sessions will be deleted from cloud.
{syncStep === "syncing" && "Syncing latest data..."} {syncStep === "disabling" && "Disabling cloud..."}
Are you sure you want to disable cloud proxy?