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",
|
||||
"description": "Default capabilities for the main Wraith window",
|
||||
"windows": ["main"],
|
||||
"windows": ["main", "tool-*"],
|
||||
"permissions": [
|
||||
"core: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"]}}
|
||||
31
src/App.vue
31
src/App.vue
@ -1,27 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from "vue";
|
||||
import { ref, onMounted, defineAsyncComponent } from "vue";
|
||||
import { useAppStore } from "@/stores/app.store";
|
||||
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(
|
||||
() => import("@/layouts/MainLayout.vue")
|
||||
);
|
||||
const ToolWindow = defineAsyncComponent(
|
||||
() => import("@/components/tools/ToolWindow.vue")
|
||||
);
|
||||
|
||||
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 () => {
|
||||
await app.checkVaultState();
|
||||
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();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app-root">
|
||||
<!-- Show the unlock/create-vault screen until the store confirms we're in -->
|
||||
<!-- Tool popup window mode -->
|
||||
<ToolWindow v-if="isToolMode" :tool="toolName" :session-id="toolSessionId" />
|
||||
<!-- Normal app mode -->
|
||||
<div v-else class="app-root">
|
||||
<UnlockLayout v-if="!app.isUnlocked" />
|
||||
<!-- Once unlocked, mount the full application shell -->
|
||||
<MainLayout v-else />
|
||||
</div>
|
||||
</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