Feat : Tailscale
This commit is contained in:
@@ -4,7 +4,8 @@ import { useState, useEffect, useCallback } from "react";
|
||||
import { Card, CardSkeleton } from "@/shared/components";
|
||||
import { CLI_TOOLS } from "@/shared/constants/cliTools";
|
||||
import { getModelsByProviderId, PROVIDER_ID_TO_ALIAS } from "@/shared/constants/models";
|
||||
import { ClaudeToolCard, CodexToolCard, DroidToolCard, OpenClawToolCard, DefaultToolCard, OpenCodeToolCard } from "./components";
|
||||
import { ClaudeToolCard, CodexToolCard, DroidToolCard, OpenClawToolCard, DefaultToolCard, OpenCodeToolCard, MitmLinkCard } from "./components";
|
||||
import { MITM_TOOLS } from "@/shared/constants/cliTools";
|
||||
|
||||
const CLOUD_URL = process.env.NEXT_PUBLIC_CLOUD_URL;
|
||||
|
||||
@@ -184,23 +185,18 @@ export default function CLIToolsPageClient({ machineId }) {
|
||||
};
|
||||
|
||||
const regularTools = Object.entries(CLI_TOOLS);
|
||||
const mitmTools = Object.entries(MITM_TOOLS);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* {!hasActiveProviders && (
|
||||
<Card className="border-yellow-500/50 bg-yellow-500/5">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="material-symbols-outlined text-yellow-500">warning</span>
|
||||
<div>
|
||||
<p className="font-medium text-yellow-600 dark:text-yellow-400">No active providers</p>
|
||||
<p className="text-sm text-text-muted">Please add and connect providers first to configure CLI tools.</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)} */}
|
||||
<div className="flex flex-col gap-4">
|
||||
{regularTools.map(([toolId, tool]) => renderToolCard(toolId, tool))}
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
{mitmTools.map(([toolId, tool]) => (
|
||||
<MitmLinkCard key={toolId} tool={tool} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { Card } from "@/shared/components";
|
||||
import Image from "next/image";
|
||||
|
||||
/**
|
||||
* Clickable card for MITM tools — navigates to /dashboard/mitm on click.
|
||||
*/
|
||||
export default function MitmLinkCard({ tool }) {
|
||||
return (
|
||||
<Link href="/dashboard/mitm" className="block">
|
||||
<Card padding="sm" className="overflow-hidden hover:border-primary/50 transition-colors cursor-pointer">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="size-8 flex items-center justify-center shrink-0">
|
||||
<Image
|
||||
src={tool.image}
|
||||
alt={tool.name}
|
||||
width={32}
|
||||
height={32}
|
||||
className="size-8 object-contain rounded-lg"
|
||||
sizes="32px"
|
||||
onError={(e) => { e.target.style.display = "none"; }}
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-medium text-sm">{tool.name}</h3>
|
||||
<span className="px-1.5 py-0.5 text-[10px] font-medium bg-purple-500/10 text-purple-600 dark:text-purple-400 rounded-full">MITM</span>
|
||||
</div>
|
||||
<p className="text-xs text-text-muted truncate">{tool.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className="material-symbols-outlined text-text-muted text-[20px]">chevron_right</span>
|
||||
</div>
|
||||
</Card>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -8,4 +8,5 @@ export { default as OpenCodeToolCard } from "./OpenCodeToolCard";
|
||||
export { default as CopilotToolCard } from "./CopilotToolCard";
|
||||
export { default as MitmServerCard } from "./MitmServerCard";
|
||||
export { default as MitmToolCard } from "./MitmToolCard";
|
||||
export { default as MitmLinkCard } from "./MitmLinkCard";
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getTunnelStatus } from "@/lib/tunnel/tunnelManager";
|
||||
import { getTunnelStatus, getTailscaleStatus } from "@/lib/tunnel/tunnelManager";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const status = await getTunnelStatus();
|
||||
return NextResponse.json(status);
|
||||
const [tunnel, tailscale] = await Promise.all([getTunnelStatus(), getTailscaleStatus()]);
|
||||
return NextResponse.json({ tunnel, tailscale });
|
||||
} catch (error) {
|
||||
console.error("Tunnel status error:", error);
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import os from "os";
|
||||
import { execSync } from "child_process";
|
||||
import { NextResponse } from "next/server";
|
||||
import { isTailscaleInstalled, isTailscaleLoggedIn, TAILSCALE_SOCKET } from "@/lib/tunnel/tailscale";
|
||||
|
||||
const EXTENDED_PATH = `/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:${process.env.PATH || ""}`;
|
||||
|
||||
function hasBrew() {
|
||||
try { execSync("which brew", { stdio: "ignore", env: { ...process.env, PATH: EXTENDED_PATH } }); return true; } catch { return false; }
|
||||
}
|
||||
|
||||
function isDaemonRunning() {
|
||||
try {
|
||||
// Use custom socket + --json; exit 0 even when not logged in
|
||||
execSync(`tailscale --socket ${TAILSCALE_SOCKET} status --json`, {
|
||||
stdio: "ignore",
|
||||
env: { ...process.env, PATH: EXTENDED_PATH },
|
||||
timeout: 3000
|
||||
});
|
||||
return true;
|
||||
} catch {
|
||||
// Fallback: check if tailscaled process is alive
|
||||
try {
|
||||
execSync("pgrep -x tailscaled", { stdio: "ignore", timeout: 2000 });
|
||||
return true;
|
||||
} catch { return false; }
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const installed = isTailscaleInstalled();
|
||||
const platform = os.platform();
|
||||
const brewAvailable = platform === "darwin" && hasBrew();
|
||||
const daemonRunning = installed ? isDaemonRunning() : false;
|
||||
const loggedIn = daemonRunning ? isTailscaleLoggedIn() : false;
|
||||
return NextResponse.json({ installed, loggedIn, platform, brewAvailable, daemonRunning });
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { disableTailscale } from "@/lib/tunnel/tunnelManager";
|
||||
|
||||
export async function POST() {
|
||||
try {
|
||||
const result = await disableTailscale();
|
||||
return NextResponse.json(result);
|
||||
} catch (error) {
|
||||
console.error("Tailscale disable error:", error);
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { enableTailscale } from "@/lib/tunnel/tunnelManager";
|
||||
|
||||
export async function POST() {
|
||||
try {
|
||||
const result = await enableTailscale();
|
||||
return NextResponse.json(result);
|
||||
} catch (error) {
|
||||
console.error("Tailscale enable error:", error);
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
"use server";
|
||||
|
||||
import os from "os";
|
||||
import { execSync } from "child_process";
|
||||
import { installTailscale } from "@/lib/tunnel/tailscale";
|
||||
import { getCachedPassword, loadEncryptedPassword, initDbHooks } from "@/mitm/manager";
|
||||
import { getSettings, updateSettings } from "@/lib/localDb";
|
||||
import { loadState, generateShortId } from "@/lib/tunnel/state.js";
|
||||
|
||||
initDbHooks(getSettings, updateSettings);
|
||||
|
||||
const EXTENDED_PATH = `/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:${process.env.PATH || ""}`;
|
||||
|
||||
function hasBrew() {
|
||||
try { execSync("which brew", { stdio: "ignore", env: { ...process.env, PATH: EXTENDED_PATH } }); return true; } catch { return false; }
|
||||
}
|
||||
|
||||
export async function POST(request) {
|
||||
const body = await request.json().catch(() => ({}));
|
||||
const platform = os.platform();
|
||||
const isWindows = platform === "win32";
|
||||
const isBrew = platform === "darwin" && hasBrew();
|
||||
const needsPassword = !isWindows && !isBrew;
|
||||
|
||||
const sudoPassword = body.sudoPassword || getCachedPassword() || await loadEncryptedPassword() || "";
|
||||
|
||||
if (needsPassword && !sudoPassword.trim()) {
|
||||
return new Response(JSON.stringify({ error: "Sudo password is required" }), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
const shortId = loadState()?.shortId || generateShortId();
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
const send = (event, data) => {
|
||||
controller.enqueue(encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`));
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await installTailscale(sudoPassword, shortId, (msg) => {
|
||||
send("progress", { message: msg });
|
||||
});
|
||||
send("done", { success: true, authUrl: result?.authUrl || null });
|
||||
} catch (error) {
|
||||
console.error("Tailscale install error:", error);
|
||||
const msg = error.message?.includes("incorrect password") || error.message?.includes("Sorry")
|
||||
? "Wrong sudo password"
|
||||
: error.message;
|
||||
send("error", { error: msg });
|
||||
} finally {
|
||||
controller.close();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { startLogin } from "@/lib/tunnel/tailscale";
|
||||
import { loadState, generateShortId } from "@/lib/tunnel/state.js";
|
||||
|
||||
export async function POST() {
|
||||
try {
|
||||
const shortId = loadState()?.shortId || generateShortId();
|
||||
const result = await startLogin(shortId);
|
||||
return NextResponse.json(result);
|
||||
} catch (error) {
|
||||
console.error("Tailscale login error:", error);
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
"use server";
|
||||
|
||||
import { NextResponse } from "next/server";
|
||||
import { startDaemonWithPassword } from "@/lib/tunnel/tailscale";
|
||||
import { getCachedPassword, loadEncryptedPassword, initDbHooks } from "@/mitm/manager";
|
||||
import { getSettings, updateSettings } from "@/lib/localDb";
|
||||
|
||||
initDbHooks(getSettings, updateSettings);
|
||||
|
||||
export async function POST(request) {
|
||||
try {
|
||||
const body = await request.json().catch(() => ({}));
|
||||
// Use provided password, or fall back to cached/stored MITM password
|
||||
const password = body.sudoPassword || getCachedPassword() || await loadEncryptedPassword() || "";
|
||||
await startDaemonWithPassword(password);
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error("Tailscale start daemon error:", error);
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user