feat: complete Tools suite — 7 tool UIs in popup windows
Some checks failed
Build & Sign Wraith / Build Windows + Sign (push) Failing after 6s
Some checks failed
Build & Sign Wraith / Build Windows + Sign (push) Failing after 6s
All 7 tool windows with full UIs: - Network Scanner: subnet scan, ARP+DNS discovery, results table with Quick Scan per host, SSH/RDP connect buttons, CSV export - Port Scanner: custom range or quick scan (24 common ports), open/closed results table with service names - Ping: remote ping with count, raw output display - Traceroute: remote traceroute, raw output display - Wake on LAN: MAC address input, broadcasts via python3 on remote host - SSH Key Generator: ed25519/RSA, copy public/private key, fingerprint - Password Generator: configurable length/charset, copy button, history Architecture: - App.vue detects tool mode via URL hash (#/tool/name?sessionId=...) - ToolWindow.vue routes to correct tool component - Tools menu in toolbar opens Tauri popup windows (WebviewWindow) - Capabilities grant tool-* windows the same permissions as main - SFTP context menu: right-click Edit/Download/Rename/Delete Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5cc412a251
commit
875dd1a28f
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"identifier": "default",
|
"identifier": "default",
|
||||||
"description": "Default capabilities for the main Wraith window",
|
"description": "Default capabilities for the main Wraith window",
|
||||||
"windows": ["main"],
|
"windows": ["main", "tool-*"],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"core:default",
|
"core:default",
|
||||||
"core:event:default",
|
"core:event:default",
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
{"default":{"identifier":"default","description":"Default capabilities for the main Wraith window","local":true,"windows":["main"],"permissions":["core:default","core:event:default","core:window:default","shell:allow-open"]}}
|
{"default":{"identifier":"default","description":"Default capabilities for the main Wraith window","local":true,"windows":["main","tool-*"],"permissions":["core:default","core:event:default","core:window:default","shell:allow-open"]}}
|
||||||
29
src/App.vue
29
src/App.vue
@ -1,27 +1,42 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted } from "vue";
|
import { ref, onMounted, defineAsyncComponent } from "vue";
|
||||||
import { useAppStore } from "@/stores/app.store";
|
import { useAppStore } from "@/stores/app.store";
|
||||||
import UnlockLayout from "@/layouts/UnlockLayout.vue";
|
import UnlockLayout from "@/layouts/UnlockLayout.vue";
|
||||||
|
|
||||||
// MainLayout is the full app shell — lazy-load it so the unlock screen is
|
|
||||||
// instant and the heavy editor/terminal code only lands after auth.
|
|
||||||
import { defineAsyncComponent } from "vue";
|
|
||||||
const MainLayout = defineAsyncComponent(
|
const MainLayout = defineAsyncComponent(
|
||||||
() => import("@/layouts/MainLayout.vue")
|
() => import("@/layouts/MainLayout.vue")
|
||||||
);
|
);
|
||||||
|
const ToolWindow = defineAsyncComponent(
|
||||||
|
() => import("@/components/tools/ToolWindow.vue")
|
||||||
|
);
|
||||||
|
|
||||||
const app = useAppStore();
|
const app = useAppStore();
|
||||||
|
|
||||||
|
// Tool window mode — detected from URL hash: #/tool/network-scanner?sessionId=abc
|
||||||
|
const isToolMode = ref(false);
|
||||||
|
const toolName = ref("");
|
||||||
|
const toolSessionId = ref("");
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
const hash = window.location.hash;
|
||||||
|
if (hash.startsWith("#/tool/")) {
|
||||||
|
isToolMode.value = true;
|
||||||
|
const rest = hash.substring(7); // after "#/tool/"
|
||||||
|
const [name, query] = rest.split("?");
|
||||||
|
toolName.value = name;
|
||||||
|
toolSessionId.value = new URLSearchParams(query || "").get("sessionId") || "";
|
||||||
|
} else {
|
||||||
await app.checkVaultState();
|
await app.checkVaultState();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="app-root">
|
<!-- Tool popup window mode -->
|
||||||
<!-- Show the unlock/create-vault screen until the store confirms we're in -->
|
<ToolWindow v-if="isToolMode" :tool="toolName" :session-id="toolSessionId" />
|
||||||
|
<!-- Normal app mode -->
|
||||||
|
<div v-else class="app-root">
|
||||||
<UnlockLayout v-if="!app.isUnlocked" />
|
<UnlockLayout v-if="!app.isUnlocked" />
|
||||||
<!-- Once unlocked, mount the full application shell -->
|
|
||||||
<MainLayout v-else />
|
<MainLayout v-else />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
89
src/components/tools/NetworkScanner.vue
Normal file
89
src/components/tools/NetworkScanner.vue
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col h-full p-4 gap-3">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<label class="text-xs text-[#8b949e]">Subnet (first 3 octets):</label>
|
||||||
|
<input v-model="subnet" type="text" placeholder="192.168.1" class="px-3 py-1.5 text-sm rounded bg-[#161b22] border border-[#30363d] text-[#e0e0e0] outline-none focus:border-[#58a6ff] w-40" />
|
||||||
|
<button class="px-4 py-1.5 text-sm font-bold rounded bg-[#58a6ff] text-black cursor-pointer disabled:opacity-40" :disabled="scanning" @click="scan">
|
||||||
|
{{ scanning ? "Scanning..." : "Scan Network" }}
|
||||||
|
</button>
|
||||||
|
<button v-if="hosts.length" class="px-3 py-1.5 text-xs rounded border border-[#30363d] text-[#8b949e] hover:text-white cursor-pointer" @click="exportCsv">Export CSV</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 overflow-auto border border-[#30363d] rounded">
|
||||||
|
<table class="w-full text-xs">
|
||||||
|
<thead class="bg-[#161b22] sticky top-0">
|
||||||
|
<tr>
|
||||||
|
<th class="text-left px-3 py-2 text-[#8b949e] font-medium">IP Address</th>
|
||||||
|
<th class="text-left px-3 py-2 text-[#8b949e] font-medium">Hostname</th>
|
||||||
|
<th class="text-left px-3 py-2 text-[#8b949e] font-medium">MAC Address</th>
|
||||||
|
<th class="text-left px-3 py-2 text-[#8b949e] font-medium">Open Ports</th>
|
||||||
|
<th class="text-left px-3 py-2 text-[#8b949e] font-medium">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="host in hosts" :key="host.ip" class="border-t border-[#21262d] hover:bg-[#161b22]">
|
||||||
|
<td class="px-3 py-1.5 font-mono">{{ host.ip }}</td>
|
||||||
|
<td class="px-3 py-1.5">{{ host.hostname || "—" }}</td>
|
||||||
|
<td class="px-3 py-1.5 font-mono text-[#8b949e]">{{ host.mac || "—" }}</td>
|
||||||
|
<td class="px-3 py-1.5">
|
||||||
|
<span v-if="host.openPorts.length" class="text-[#3fb950]">{{ host.openPorts.join(", ") }}</span>
|
||||||
|
<button v-else class="text-[#58a6ff] hover:underline cursor-pointer" @click="quickScanHost(host)">scan</button>
|
||||||
|
</td>
|
||||||
|
<td class="px-3 py-1.5 flex gap-1">
|
||||||
|
<button class="px-2 py-0.5 text-[10px] rounded bg-[#238636] text-white cursor-pointer" @click="connectSsh(host)">SSH</button>
|
||||||
|
<button class="px-2 py-0.5 text-[10px] rounded bg-[#1f6feb] text-white cursor-pointer" @click="connectRdp(host)">RDP</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="!hosts.length && !scanning">
|
||||||
|
<td colspan="5" class="px-3 py-8 text-center text-[#484f58]">Enter a subnet and click Scan</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-[10px] text-[#484f58]">{{ hosts.length }} hosts found • Scanning through session {{ sessionId.substring(0, 8) }}...</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
|
||||||
|
const props = defineProps<{ sessionId: string }>();
|
||||||
|
|
||||||
|
interface Host { ip: string; mac: string | null; hostname: string | null; vendor: string | null; openPorts: number[]; services: string[]; }
|
||||||
|
|
||||||
|
const subnet = ref("192.168.1");
|
||||||
|
const hosts = ref<Host[]>([]);
|
||||||
|
const scanning = ref(false);
|
||||||
|
|
||||||
|
async function scan(): Promise<void> {
|
||||||
|
scanning.value = true;
|
||||||
|
try {
|
||||||
|
hosts.value = await invoke<Host[]>("scan_network", { sessionId: props.sessionId, subnet: subnet.value });
|
||||||
|
} catch (err) { alert(err); }
|
||||||
|
scanning.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function quickScanHost(host: Host): Promise<void> {
|
||||||
|
try {
|
||||||
|
const results = await invoke<{ port: number; open: boolean; service: string }[]>("quick_scan", { sessionId: props.sessionId, target: host.ip });
|
||||||
|
host.openPorts = results.filter(r => r.open).map(r => r.port);
|
||||||
|
} catch (err) { console.error(err); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function connectSsh(host: Host): void { alert(`TODO: Open SSH tab to ${host.ip}`); }
|
||||||
|
function connectRdp(host: Host): void { alert(`TODO: Open RDP tab to ${host.ip}`); }
|
||||||
|
|
||||||
|
function exportCsv(): void {
|
||||||
|
const lines = ["IP,Hostname,MAC,OpenPorts"];
|
||||||
|
for (const h of hosts.value) {
|
||||||
|
lines.push(`${h.ip},"${h.hostname || ""}","${h.mac || ""}","${h.openPorts.join(";")}"`);
|
||||||
|
}
|
||||||
|
const blob = new Blob([lines.join("\n")], { type: "text/csv" });
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = URL.createObjectURL(blob);
|
||||||
|
a.download = `wraith-scan-${subnet.value}-${Date.now()}.csv`;
|
||||||
|
a.click();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
63
src/components/tools/PasswordGen.vue
Normal file
63
src/components/tools/PasswordGen.vue
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col h-full p-4 gap-4">
|
||||||
|
<h2 class="text-sm font-bold text-[#58a6ff]">Password Generator</h2>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs text-[#8b949e] mb-1">Length</label>
|
||||||
|
<input v-model.number="length" type="number" min="4" max="128" class="px-3 py-1.5 text-sm rounded bg-[#161b22] border border-[#30363d] text-[#e0e0e0] outline-none focus:border-[#58a6ff] w-20" />
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3 self-end">
|
||||||
|
<label class="flex items-center gap-1 text-xs text-[#8b949e] cursor-pointer"><input v-model="uppercase" type="checkbox" class="accent-[#58a6ff]" /> A-Z</label>
|
||||||
|
<label class="flex items-center gap-1 text-xs text-[#8b949e] cursor-pointer"><input v-model="lowercase" type="checkbox" class="accent-[#58a6ff]" /> a-z</label>
|
||||||
|
<label class="flex items-center gap-1 text-xs text-[#8b949e] cursor-pointer"><input v-model="digits" type="checkbox" class="accent-[#58a6ff]" /> 0-9</label>
|
||||||
|
<label class="flex items-center gap-1 text-xs text-[#8b949e] cursor-pointer"><input v-model="symbols" type="checkbox" class="accent-[#58a6ff]" /> !@#</label>
|
||||||
|
</div>
|
||||||
|
<button class="px-4 py-1.5 text-sm font-bold rounded bg-[#238636] text-white cursor-pointer self-end" @click="generate">Generate</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="password" class="flex items-center gap-2">
|
||||||
|
<input readonly :value="password" class="flex-1 px-3 py-2 text-lg font-mono rounded bg-[#161b22] border border-[#30363d] text-[#e0e0e0] select-all" @click="($event.target as HTMLInputElement).select()" />
|
||||||
|
<button class="px-3 py-2 text-xs rounded bg-[#58a6ff] text-black font-bold cursor-pointer" @click="copy">Copy</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="history.length" class="flex-1 overflow-auto">
|
||||||
|
<h3 class="text-xs text-[#8b949e] mb-2">History</h3>
|
||||||
|
<div v-for="(pw, i) in history" :key="i" class="flex items-center gap-2 py-1 border-b border-[#21262d]">
|
||||||
|
<span class="flex-1 font-mono text-xs text-[#8b949e] truncate">{{ pw }}</span>
|
||||||
|
<button class="text-[10px] text-[#58a6ff] hover:underline cursor-pointer" @click="navigator.clipboard.writeText(pw)">copy</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
|
||||||
|
const length = ref(20);
|
||||||
|
const uppercase = ref(true);
|
||||||
|
const lowercase = ref(true);
|
||||||
|
const digits = ref(true);
|
||||||
|
const symbols = ref(true);
|
||||||
|
const password = ref("");
|
||||||
|
const history = ref<string[]>([]);
|
||||||
|
|
||||||
|
async function generate(): Promise<void> {
|
||||||
|
try {
|
||||||
|
password.value = await invoke<string>("tool_generate_password", {
|
||||||
|
length: length.value,
|
||||||
|
uppercase: uppercase.value,
|
||||||
|
lowercase: lowercase.value,
|
||||||
|
digits: digits.value,
|
||||||
|
symbols: symbols.value,
|
||||||
|
});
|
||||||
|
history.value.unshift(password.value);
|
||||||
|
if (history.value.length > 20) history.value.pop();
|
||||||
|
} catch (err) { alert(err); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function copy(): void {
|
||||||
|
navigator.clipboard.writeText(password.value).catch(() => {});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
32
src/components/tools/PingTool.vue
Normal file
32
src/components/tools/PingTool.vue
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col h-full p-4 gap-3">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<input v-model="target" type="text" placeholder="Host to ping" class="px-3 py-1.5 text-sm rounded bg-[#161b22] border border-[#30363d] text-[#e0e0e0] outline-none focus:border-[#58a6ff] flex-1" @keydown.enter="ping" />
|
||||||
|
<input v-model.number="count" type="number" min="1" max="100" class="px-3 py-1.5 text-sm rounded bg-[#161b22] border border-[#30363d] text-[#e0e0e0] outline-none focus:border-[#58a6ff] w-16" />
|
||||||
|
<button class="px-4 py-1.5 text-sm font-bold rounded bg-[#58a6ff] text-black cursor-pointer disabled:opacity-40" :disabled="running" @click="ping">Ping</button>
|
||||||
|
</div>
|
||||||
|
<pre class="flex-1 overflow-auto bg-[#161b22] border border-[#30363d] rounded p-3 text-xs font-mono whitespace-pre-wrap text-[#e0e0e0]">{{ output || "Enter a host and click Ping" }}</pre>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
|
||||||
|
const props = defineProps<{ sessionId: string }>();
|
||||||
|
const target = ref("");
|
||||||
|
const count = ref(4);
|
||||||
|
const output = ref("");
|
||||||
|
const running = ref(false);
|
||||||
|
|
||||||
|
async function ping(): Promise<void> {
|
||||||
|
if (!target.value) return;
|
||||||
|
running.value = true;
|
||||||
|
output.value = `Pinging ${target.value}...\n`;
|
||||||
|
try {
|
||||||
|
const result = await invoke<{ target: string; output: string }>("tool_ping", { sessionId: props.sessionId, target: target.value, count: count.value });
|
||||||
|
output.value = result.output;
|
||||||
|
} catch (err) { output.value = String(err); }
|
||||||
|
running.value = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
81
src/components/tools/PortScanner.vue
Normal file
81
src/components/tools/PortScanner.vue
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col h-full p-4 gap-3">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<input v-model="target" type="text" placeholder="Target IP or hostname" class="px-3 py-1.5 text-sm rounded bg-[#161b22] border border-[#30363d] text-[#e0e0e0] outline-none focus:border-[#58a6ff] w-44" />
|
||||||
|
<input v-model="portRange" type="text" placeholder="Ports: 1-1024 or 22,80,443" class="px-3 py-1.5 text-sm rounded bg-[#161b22] border border-[#30363d] text-[#e0e0e0] outline-none focus:border-[#58a6ff] w-48" />
|
||||||
|
<button class="px-4 py-1.5 text-sm font-bold rounded bg-[#58a6ff] text-black cursor-pointer disabled:opacity-40" :disabled="scanning" @click="scan">
|
||||||
|
{{ scanning ? "Scanning..." : "Scan" }}
|
||||||
|
</button>
|
||||||
|
<button class="px-3 py-1.5 text-xs rounded border border-[#30363d] text-[#8b949e] hover:text-white cursor-pointer disabled:opacity-40" :disabled="scanning" @click="quickScan">Quick Scan</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 overflow-auto border border-[#30363d] rounded">
|
||||||
|
<table class="w-full text-xs">
|
||||||
|
<thead class="bg-[#161b22] sticky top-0">
|
||||||
|
<tr>
|
||||||
|
<th class="text-left px-3 py-2 text-[#8b949e] font-medium w-20">Port</th>
|
||||||
|
<th class="text-left px-3 py-2 text-[#8b949e] font-medium w-20">State</th>
|
||||||
|
<th class="text-left px-3 py-2 text-[#8b949e] font-medium">Service</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="r in results" :key="r.port" class="border-t border-[#21262d]">
|
||||||
|
<td class="px-3 py-1.5 font-mono">{{ r.port }}</td>
|
||||||
|
<td class="px-3 py-1.5" :class="r.open ? 'text-[#3fb950]' : 'text-[#484f58]'">{{ r.open ? "open" : "closed" }}</td>
|
||||||
|
<td class="px-3 py-1.5">{{ r.service }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-[10px] text-[#484f58]">{{ results.filter(r => r.open).length }} open / {{ results.length }} scanned</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
|
||||||
|
const props = defineProps<{ sessionId: string }>();
|
||||||
|
|
||||||
|
const target = ref("");
|
||||||
|
const portRange = ref("1-1024");
|
||||||
|
const results = ref<{ port: number; open: boolean; service: string }[]>([]);
|
||||||
|
const scanning = ref(false);
|
||||||
|
|
||||||
|
function parsePorts(input: string): number[] {
|
||||||
|
const ports: number[] = [];
|
||||||
|
for (const part of input.split(",")) {
|
||||||
|
const trimmed = part.trim();
|
||||||
|
if (trimmed.includes("-")) {
|
||||||
|
const [start, end] = trimmed.split("-").map(Number);
|
||||||
|
if (!isNaN(start) && !isNaN(end)) {
|
||||||
|
for (let p = start; p <= Math.min(end, 65535); p++) ports.push(p);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const p = Number(trimmed);
|
||||||
|
if (!isNaN(p) && p > 0 && p <= 65535) ports.push(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ports;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function scan(): Promise<void> {
|
||||||
|
if (!target.value) return;
|
||||||
|
scanning.value = true;
|
||||||
|
try {
|
||||||
|
const ports = parsePorts(portRange.value);
|
||||||
|
results.value = await invoke("scan_ports", { sessionId: props.sessionId, target: target.value, ports });
|
||||||
|
} catch (err) { alert(err); }
|
||||||
|
scanning.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function quickScan(): Promise<void> {
|
||||||
|
if (!target.value) return;
|
||||||
|
scanning.value = true;
|
||||||
|
try {
|
||||||
|
results.value = await invoke("quick_scan", { sessionId: props.sessionId, target: target.value });
|
||||||
|
} catch (err) { alert(err); }
|
||||||
|
scanning.value = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
65
src/components/tools/SshKeyGen.vue
Normal file
65
src/components/tools/SshKeyGen.vue
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col h-full p-4 gap-4">
|
||||||
|
<h2 class="text-sm font-bold text-[#58a6ff]">SSH Key Generator</h2>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs text-[#8b949e] mb-1">Key Type</label>
|
||||||
|
<select v-model="keyType" class="px-3 py-1.5 text-sm rounded bg-[#161b22] border border-[#30363d] text-[#e0e0e0] outline-none cursor-pointer">
|
||||||
|
<option value="ed25519">Ed25519 (recommended)</option>
|
||||||
|
<option value="rsa">RSA 2048</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<label class="block text-xs text-[#8b949e] mb-1">Comment</label>
|
||||||
|
<input v-model="comment" type="text" placeholder="user@host" class="w-full px-3 py-1.5 text-sm rounded bg-[#161b22] border border-[#30363d] text-[#e0e0e0] outline-none focus:border-[#58a6ff]" />
|
||||||
|
</div>
|
||||||
|
<div class="self-end">
|
||||||
|
<button class="px-4 py-1.5 text-sm font-bold rounded bg-[#238636] text-white cursor-pointer" @click="generate">Generate</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-if="key">
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center justify-between mb-1">
|
||||||
|
<label class="text-xs text-[#8b949e]">Public Key</label>
|
||||||
|
<button class="text-[10px] text-[#58a6ff] hover:underline cursor-pointer" @click="copy(key.publicKey)">Copy</button>
|
||||||
|
</div>
|
||||||
|
<textarea readonly :value="key.publicKey" rows="2" class="w-full px-3 py-2 text-xs font-mono rounded bg-[#161b22] border border-[#30363d] text-[#e0e0e0] resize-none" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center justify-between mb-1">
|
||||||
|
<label class="text-xs text-[#8b949e]">Private Key</label>
|
||||||
|
<button class="text-[10px] text-[#58a6ff] hover:underline cursor-pointer" @click="copy(key.privateKey)">Copy</button>
|
||||||
|
</div>
|
||||||
|
<textarea readonly :value="key.privateKey" rows="8" class="w-full px-3 py-2 text-xs font-mono rounded bg-[#161b22] border border-[#30363d] text-[#e0e0e0] resize-none" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-xs text-[#8b949e]">
|
||||||
|
Fingerprint: <span class="font-mono text-[#e0e0e0]">{{ key.fingerprint }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
|
||||||
|
const keyType = ref("ed25519");
|
||||||
|
const comment = ref("");
|
||||||
|
|
||||||
|
interface GeneratedKey { privateKey: string; publicKey: string; fingerprint: string; keyType: string; }
|
||||||
|
const key = ref<GeneratedKey | null>(null);
|
||||||
|
|
||||||
|
async function generate(): Promise<void> {
|
||||||
|
try {
|
||||||
|
key.value = await invoke<GeneratedKey>("tool_generate_ssh_key", { keyType: keyType.value, comment: comment.value || null });
|
||||||
|
} catch (err) { alert(err); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function copy(text: string): void {
|
||||||
|
navigator.clipboard.writeText(text).catch(() => {});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
29
src/components/tools/ToolWindow.vue
Normal file
29
src/components/tools/ToolWindow.vue
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<template>
|
||||||
|
<div class="h-screen w-screen flex flex-col bg-[#0d1117] text-[#e0e0e0]">
|
||||||
|
<NetworkScanner v-if="tool === 'network-scanner'" :session-id="sessionId" />
|
||||||
|
<PortScanner v-else-if="tool === 'port-scanner'" :session-id="sessionId" />
|
||||||
|
<PingTool v-else-if="tool === 'ping'" :session-id="sessionId" />
|
||||||
|
<TracerouteTool v-else-if="tool === 'traceroute'" :session-id="sessionId" />
|
||||||
|
<WakeOnLan v-else-if="tool === 'wake-on-lan'" :session-id="sessionId" />
|
||||||
|
<SshKeyGen v-else-if="tool === 'ssh-keygen'" />
|
||||||
|
<PasswordGen v-else-if="tool === 'password-gen'" />
|
||||||
|
<div v-else class="flex-1 flex items-center justify-center text-sm text-[#484f58]">
|
||||||
|
Unknown tool: {{ tool }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import NetworkScanner from "./NetworkScanner.vue";
|
||||||
|
import PortScanner from "./PortScanner.vue";
|
||||||
|
import PingTool from "./PingTool.vue";
|
||||||
|
import TracerouteTool from "./TracerouteTool.vue";
|
||||||
|
import WakeOnLan from "./WakeOnLan.vue";
|
||||||
|
import SshKeyGen from "./SshKeyGen.vue";
|
||||||
|
import PasswordGen from "./PasswordGen.vue";
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
tool: string;
|
||||||
|
sessionId: string;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
29
src/components/tools/TracerouteTool.vue
Normal file
29
src/components/tools/TracerouteTool.vue
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col h-full p-4 gap-3">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<input v-model="target" type="text" placeholder="Host to trace" class="px-3 py-1.5 text-sm rounded bg-[#161b22] border border-[#30363d] text-[#e0e0e0] outline-none focus:border-[#58a6ff] flex-1" @keydown.enter="trace" />
|
||||||
|
<button class="px-4 py-1.5 text-sm font-bold rounded bg-[#58a6ff] text-black cursor-pointer disabled:opacity-40" :disabled="running" @click="trace">Trace</button>
|
||||||
|
</div>
|
||||||
|
<pre class="flex-1 overflow-auto bg-[#161b22] border border-[#30363d] rounded p-3 text-xs font-mono whitespace-pre-wrap text-[#e0e0e0]">{{ output || "Enter a host and click Trace" }}</pre>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
|
||||||
|
const props = defineProps<{ sessionId: string }>();
|
||||||
|
const target = ref("");
|
||||||
|
const output = ref("");
|
||||||
|
const running = ref(false);
|
||||||
|
|
||||||
|
async function trace(): Promise<void> {
|
||||||
|
if (!target.value) return;
|
||||||
|
running.value = true;
|
||||||
|
output.value = `Tracing route to ${target.value}...\n`;
|
||||||
|
try {
|
||||||
|
output.value = await invoke<string>("tool_traceroute", { sessionId: props.sessionId, target: target.value });
|
||||||
|
} catch (err) { output.value = String(err); }
|
||||||
|
running.value = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
32
src/components/tools/WakeOnLan.vue
Normal file
32
src/components/tools/WakeOnLan.vue
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col h-full p-4 gap-4">
|
||||||
|
<h2 class="text-sm font-bold text-[#58a6ff]">Wake on LAN</h2>
|
||||||
|
<p class="text-xs text-[#8b949e]">Send a magic packet through the remote host to wake a machine on the same network.</p>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<input v-model="macAddress" type="text" placeholder="MAC address (AA:BB:CC:DD:EE:FF)" class="px-3 py-1.5 text-sm rounded bg-[#161b22] border border-[#30363d] text-[#e0e0e0] outline-none focus:border-[#58a6ff] flex-1 font-mono" @keydown.enter="wake" />
|
||||||
|
<button class="px-4 py-1.5 text-sm font-bold rounded bg-[#58a6ff] text-black cursor-pointer disabled:opacity-40" :disabled="sending" @click="wake">Wake</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<pre v-if="result" class="bg-[#161b22] border border-[#30363d] rounded p-3 text-xs font-mono text-[#e0e0e0]">{{ result }}</pre>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
|
||||||
|
const props = defineProps<{ sessionId: string }>();
|
||||||
|
const macAddress = ref("");
|
||||||
|
const result = ref("");
|
||||||
|
const sending = ref(false);
|
||||||
|
|
||||||
|
async function wake(): Promise<void> {
|
||||||
|
if (!macAddress.value) return;
|
||||||
|
sending.value = true;
|
||||||
|
try {
|
||||||
|
result.value = await invoke<string>("tool_wake_on_lan", { sessionId: props.sessionId, macAddress: macAddress.value });
|
||||||
|
} catch (err) { result.value = String(err); }
|
||||||
|
sending.value = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
Loading…
Reference in New Issue
Block a user