fix: wire vault persistence, connection loading, and MobaXterm import to real Go backend
Replace all TODO stubs in frontend stores with real Wails Call.ByName bindings. The app store now calls WraithApp.IsFirstRun/CreateVault/Unlock so vault state persists across launches. The connection store loads from ConnectionService.ListConnections/ListGroups instead of hardcoded mock data. The import dialog calls a new WraithApp.ImportMobaConf method that parses the file, creates groups and connections in SQLite, and stores host keys. ConnectionEditDialog also uses real Go CRUD calls. MainLayout loads connections on mount after vault unlock. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
fbd2fd4f80
commit
a1dce82d99
@ -88,6 +88,13 @@
|
||||
<p class="text-xs text-[var(--wraith-text-muted)]">
|
||||
Passwords are not imported (MobaXTerm uses proprietary encryption).
|
||||
</p>
|
||||
|
||||
<div
|
||||
v-if="importError"
|
||||
class="text-sm text-[var(--wraith-accent-red)] bg-[var(--wraith-accent-red)]/10 border border-[var(--wraith-accent-red)]/20 rounded px-3 py-2"
|
||||
>
|
||||
{{ importError }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -116,10 +123,11 @@
|
||||
</button>
|
||||
<button
|
||||
v-if="step === 'preview'"
|
||||
class="px-3 py-1.5 text-xs text-white bg-[#238636] hover:bg-[#2ea043] rounded transition-colors cursor-pointer"
|
||||
:disabled="importing"
|
||||
class="px-3 py-1.5 text-xs text-white bg-[#238636] hover:bg-[#2ea043] rounded transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
@click="doImport"
|
||||
>
|
||||
Import
|
||||
{{ importing ? "Importing..." : "Import" }}
|
||||
</button>
|
||||
<button
|
||||
v-if="step === 'complete'"
|
||||
@ -136,13 +144,23 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { Call } from "@wailsio/runtime";
|
||||
import { useConnectionStore } from "@/stores/connection.store";
|
||||
|
||||
/** Fully qualified Go method name prefix for WraithApp bindings. */
|
||||
const APP = "github.com/vstockwell/wraith/internal/app.WraithApp";
|
||||
|
||||
type Step = "select" | "preview" | "complete";
|
||||
|
||||
const connectionStore = useConnectionStore();
|
||||
|
||||
const visible = ref(false);
|
||||
const step = ref<Step>("select");
|
||||
const fileName = ref("");
|
||||
const fileInput = ref<HTMLInputElement | null>(null);
|
||||
const fileContent = ref("");
|
||||
const importError = ref<string | null>(null);
|
||||
const importing = ref(false);
|
||||
|
||||
const preview = ref({
|
||||
connections: 0,
|
||||
@ -155,6 +173,8 @@ function open(): void {
|
||||
visible.value = true;
|
||||
step.value = "select";
|
||||
fileName.value = "";
|
||||
fileContent.value = "";
|
||||
importError.value = null;
|
||||
}
|
||||
|
||||
function close(): void {
|
||||
@ -183,11 +203,10 @@ function handleDrop(event: DragEvent): void {
|
||||
async function processFile(file: File): Promise<void> {
|
||||
fileName.value = file.name;
|
||||
|
||||
// TODO: Replace with Wails binding — ImporterService.Preview(fileData)
|
||||
// For now, read and mock-parse the file to show a preview
|
||||
const text = await file.text();
|
||||
fileContent.value = text;
|
||||
|
||||
// Simple mock parse to count items
|
||||
// Client-side preview parse to count items for display
|
||||
const lines = text.split("\n");
|
||||
let groups = 0;
|
||||
let connections = 0;
|
||||
@ -237,10 +256,21 @@ async function processFile(file: File): Promise<void> {
|
||||
step.value = "preview";
|
||||
}
|
||||
|
||||
function doImport(): void {
|
||||
// TODO: Replace with Wails binding — ImporterService.Import(fileData)
|
||||
// For now, just show success
|
||||
step.value = "complete";
|
||||
async function doImport(): Promise<void> {
|
||||
if (!fileContent.value) return;
|
||||
importing.value = true;
|
||||
importError.value = null;
|
||||
|
||||
try {
|
||||
await Call.ByName(`${APP}.ImportMobaConf`, fileContent.value);
|
||||
// Refresh connection store so sidebar shows imported connections
|
||||
await connectionStore.loadAll();
|
||||
step.value = "complete";
|
||||
} catch (e) {
|
||||
importError.value = e instanceof Error ? e.message : String(e) || "Import failed";
|
||||
} finally {
|
||||
importing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ open, close, visible });
|
||||
|
||||
@ -165,6 +165,10 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from "vue";
|
||||
import { useConnectionStore, type Connection } from "@/stores/connection.store";
|
||||
import { Call } from "@wailsio/runtime";
|
||||
|
||||
/** Fully qualified Go method name prefix for ConnectionService bindings. */
|
||||
const CONN = "github.com/vstockwell/wraith/internal/connections.ConnectionService";
|
||||
|
||||
interface ConnectionForm {
|
||||
name: string;
|
||||
@ -252,7 +256,7 @@ function close(): void {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
function save(): void {
|
||||
async function save(): Promise<void> {
|
||||
if (!isValid.value) return;
|
||||
|
||||
const tags = tagsInput.value
|
||||
@ -260,32 +264,35 @@ function save(): void {
|
||||
.map((t) => t.trim())
|
||||
.filter((t) => t.length > 0);
|
||||
|
||||
if (isEditing.value && editingId.value !== null) {
|
||||
// TODO: Replace with Wails binding — ConnectionService.UpdateConnection(id, input)
|
||||
const conn = connectionStore.connections.find((c) => c.id === editingId.value);
|
||||
if (conn) {
|
||||
conn.name = form.value.name;
|
||||
conn.hostname = form.value.hostname;
|
||||
conn.port = form.value.port;
|
||||
conn.protocol = form.value.protocol;
|
||||
conn.groupId = form.value.groupId ?? conn.groupId;
|
||||
conn.tags = tags;
|
||||
try {
|
||||
if (isEditing.value && editingId.value !== null) {
|
||||
await Call.ByName(`${CONN}.UpdateConnection`, editingId.value, {
|
||||
name: form.value.name,
|
||||
hostname: form.value.hostname,
|
||||
port: form.value.port,
|
||||
groupId: form.value.groupId,
|
||||
color: form.value.color,
|
||||
tags,
|
||||
notes: form.value.notes,
|
||||
});
|
||||
} else {
|
||||
await Call.ByName(`${CONN}.CreateConnection`, {
|
||||
name: form.value.name,
|
||||
hostname: form.value.hostname,
|
||||
port: form.value.port,
|
||||
protocol: form.value.protocol,
|
||||
groupId: form.value.groupId,
|
||||
color: form.value.color,
|
||||
tags,
|
||||
notes: form.value.notes,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// TODO: Replace with Wails binding — ConnectionService.CreateConnection(input)
|
||||
const newId = Math.max(...connectionStore.connections.map((c) => c.id), 0) + 1;
|
||||
connectionStore.connections.push({
|
||||
id: newId,
|
||||
name: form.value.name,
|
||||
hostname: form.value.hostname,
|
||||
port: form.value.port,
|
||||
protocol: form.value.protocol,
|
||||
groupId: form.value.groupId ?? 1,
|
||||
tags,
|
||||
});
|
||||
// Refresh connections from backend
|
||||
await connectionStore.loadAll();
|
||||
close();
|
||||
} catch (err) {
|
||||
console.error("Failed to save connection:", err);
|
||||
}
|
||||
|
||||
close();
|
||||
}
|
||||
|
||||
defineExpose({ openNew, openEdit, close, visible });
|
||||
|
||||
@ -383,8 +383,10 @@ function handleKeydown(event: KeyboardEvent): void {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
document.addEventListener("keydown", handleKeydown);
|
||||
// Load connections and groups from the Go backend after vault unlock
|
||||
await connectionStore.loadAll();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
import { Call } from "@wailsio/runtime";
|
||||
|
||||
/** Fully qualified Go method name prefix for WraithApp bindings. */
|
||||
const APP = "github.com/vstockwell/wraith/internal/app.WraithApp";
|
||||
|
||||
/**
|
||||
* Wraith application store.
|
||||
* Manages unlock state, first-run detection, and vault operations.
|
||||
*
|
||||
* Once Wails v3 bindings are generated, the mock calls below will be
|
||||
* replaced with actual WraithApp.IsFirstRun(), CreateVault(), Unlock(), etc.
|
||||
*/
|
||||
export const useAppStore = defineStore("app", () => {
|
||||
const isUnlocked = ref(false);
|
||||
@ -17,8 +18,7 @@ export const useAppStore = defineStore("app", () => {
|
||||
/** Check whether the vault has been created before. */
|
||||
async function checkFirstRun(): Promise<void> {
|
||||
try {
|
||||
// TODO: replace with Wails binding — WraithApp.IsFirstRun()
|
||||
isFirstRun.value = true;
|
||||
isFirstRun.value = await Call.ByName(`${APP}.IsFirstRun`) as boolean;
|
||||
} catch {
|
||||
isFirstRun.value = true;
|
||||
} finally {
|
||||
@ -30,12 +30,11 @@ export const useAppStore = defineStore("app", () => {
|
||||
async function createVault(password: string): Promise<void> {
|
||||
error.value = null;
|
||||
try {
|
||||
// TODO: replace with Wails binding — WraithApp.CreateVault(password)
|
||||
void password;
|
||||
await Call.ByName(`${APP}.CreateVault`, password);
|
||||
isFirstRun.value = false;
|
||||
isUnlocked.value = true;
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : "Failed to create vault";
|
||||
error.value = e instanceof Error ? e.message : String(e) || "Failed to create vault";
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@ -44,11 +43,10 @@ export const useAppStore = defineStore("app", () => {
|
||||
async function unlock(password: string): Promise<void> {
|
||||
error.value = null;
|
||||
try {
|
||||
// TODO: replace with Wails binding — WraithApp.Unlock(password)
|
||||
void password;
|
||||
await Call.ByName(`${APP}.Unlock`, password);
|
||||
isUnlocked.value = true;
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : "Invalid master password";
|
||||
error.value = e instanceof Error ? e.message : String(e) || "Invalid master password";
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref, computed } from "vue";
|
||||
import { Call } from "@wailsio/runtime";
|
||||
|
||||
/** Fully qualified Go method name prefix for ConnectionService bindings. */
|
||||
const CONN = "github.com/vstockwell/wraith/internal/connections.ConnectionService";
|
||||
|
||||
export interface Connection {
|
||||
id: number;
|
||||
@ -7,37 +11,35 @@ export interface Connection {
|
||||
hostname: string;
|
||||
port: number;
|
||||
protocol: "ssh" | "rdp";
|
||||
groupId: number;
|
||||
groupId: number | null;
|
||||
credentialId?: number | null;
|
||||
color?: string;
|
||||
tags?: string[];
|
||||
notes?: string;
|
||||
options?: string;
|
||||
sortOrder?: number;
|
||||
lastConnected?: string | null;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface Group {
|
||||
id: number;
|
||||
name: string;
|
||||
parentId: number | null;
|
||||
sortOrder?: number;
|
||||
icon?: string;
|
||||
children?: Group[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Connection store.
|
||||
* Manages connections, groups, and search state.
|
||||
*
|
||||
* Uses mock data until Wails bindings are connected.
|
||||
* Loads data from the Go backend via Wails bindings.
|
||||
*/
|
||||
export const useConnectionStore = defineStore("connection", () => {
|
||||
const connections = ref<Connection[]>([
|
||||
{ id: 1, name: "Asgard", hostname: "192.168.1.4", port: 22, protocol: "ssh", groupId: 1, tags: ["Prod"] },
|
||||
{ id: 2, name: "Docker", hostname: "155.254.29.221", port: 22, protocol: "ssh", groupId: 1, tags: ["Prod"] },
|
||||
{ id: 3, name: "Predator Mac", hostname: "192.168.1.214", port: 22, protocol: "ssh", groupId: 1 },
|
||||
{ id: 4, name: "CLT-VMHOST01", hostname: "100.64.1.204", port: 3389, protocol: "rdp", groupId: 1 },
|
||||
{ id: 5, name: "ITFlow", hostname: "192.154.253.106", port: 22, protocol: "ssh", groupId: 2 },
|
||||
{ id: 6, name: "Mautic", hostname: "192.154.253.112", port: 22, protocol: "ssh", groupId: 2 },
|
||||
]);
|
||||
|
||||
const groups = ref<Group[]>([
|
||||
{ id: 1, name: "Vantz's Stuff", parentId: null },
|
||||
{ id: 2, name: "MSPNerd", parentId: null },
|
||||
]);
|
||||
|
||||
const connections = ref<Connection[]>([]);
|
||||
const groups = ref<Group[]>([]);
|
||||
const searchQuery = ref("");
|
||||
|
||||
/** Filter connections by search query. */
|
||||
@ -70,16 +72,31 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
return connectionsByGroup(groupId).length > 0;
|
||||
}
|
||||
|
||||
/** Load connections from backend (mock for now). */
|
||||
/** Load connections from the Go backend. */
|
||||
async function loadConnections(): Promise<void> {
|
||||
// TODO: replace with Wails binding — ConnectionService.ListConnections()
|
||||
// connections.value = await ConnectionService.ListConnections();
|
||||
try {
|
||||
const conns = await Call.ByName(`${CONN}.ListConnections`) as Connection[];
|
||||
connections.value = conns || [];
|
||||
} catch (err) {
|
||||
console.error("Failed to load connections:", err);
|
||||
connections.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
/** Load groups from backend (mock for now). */
|
||||
/** Load groups from the Go backend. */
|
||||
async function loadGroups(): Promise<void> {
|
||||
// TODO: replace with Wails binding — ConnectionService.ListGroups()
|
||||
// groups.value = await ConnectionService.ListGroups();
|
||||
try {
|
||||
const grps = await Call.ByName(`${CONN}.ListGroups`) as Group[];
|
||||
groups.value = grps || [];
|
||||
} catch (err) {
|
||||
console.error("Failed to load groups:", err);
|
||||
groups.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
/** Load both connections and groups from the Go backend. */
|
||||
async function loadAll(): Promise<void> {
|
||||
await Promise.all([loadConnections(), loadGroups()]);
|
||||
}
|
||||
|
||||
return {
|
||||
@ -91,5 +108,6 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
groupHasResults,
|
||||
loadConnections,
|
||||
loadGroups,
|
||||
loadAll,
|
||||
};
|
||||
});
|
||||
|
||||
@ -12,6 +12,7 @@ import (
|
||||
"github.com/vstockwell/wraith/internal/connections"
|
||||
"github.com/vstockwell/wraith/internal/credentials"
|
||||
"github.com/vstockwell/wraith/internal/db"
|
||||
"github.com/vstockwell/wraith/internal/importer"
|
||||
"github.com/vstockwell/wraith/internal/plugin"
|
||||
"github.com/vstockwell/wraith/internal/rdp"
|
||||
"github.com/vstockwell/wraith/internal/session"
|
||||
@ -228,3 +229,61 @@ func (a *WraithApp) initCredentials() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ImportMobaConf parses a MobaXTerm .mobaconf file and imports its contents
|
||||
// (groups, connections, host keys) into the database.
|
||||
func (a *WraithApp) ImportMobaConf(fileContent string) (*plugin.ImportResult, error) {
|
||||
imp := &importer.MobaConfImporter{}
|
||||
result, err := imp.Parse([]byte(fileContent))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse mobaconf: %w", err)
|
||||
}
|
||||
|
||||
// Create groups and track name -> ID mapping
|
||||
groupMap := make(map[string]int64)
|
||||
for _, g := range result.Groups {
|
||||
created, err := a.Connections.CreateGroup(g.Name, nil)
|
||||
if err != nil {
|
||||
slog.Warn("failed to create group during import", "name", g.Name, "error", err)
|
||||
continue
|
||||
}
|
||||
groupMap[g.Name] = created.ID
|
||||
}
|
||||
|
||||
// Create connections
|
||||
for _, c := range result.Connections {
|
||||
var groupID *int64
|
||||
if id, ok := groupMap[c.GroupName]; ok {
|
||||
groupID = &id
|
||||
}
|
||||
_, err := a.Connections.CreateConnection(connections.CreateConnectionInput{
|
||||
Name: c.Name,
|
||||
Hostname: c.Hostname,
|
||||
Port: c.Port,
|
||||
Protocol: c.Protocol,
|
||||
GroupID: groupID,
|
||||
Notes: c.Notes,
|
||||
})
|
||||
if err != nil {
|
||||
slog.Warn("failed to create connection during import", "name", c.Name, "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Store host keys
|
||||
for _, hk := range result.HostKeys {
|
||||
_, err := a.db.Exec(
|
||||
`INSERT OR IGNORE INTO host_keys (hostname, port, key_type, fingerprint) VALUES (?, ?, ?, ?)`,
|
||||
hk.Hostname, hk.Port, hk.KeyType, hk.Fingerprint,
|
||||
)
|
||||
if err != nil {
|
||||
slog.Warn("failed to store host key during import", "hostname", hk.Hostname, "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
slog.Info("mobaconf import complete",
|
||||
"groups", len(result.Groups),
|
||||
"connections", len(result.Connections),
|
||||
"hostKeys", len(result.HostKeys),
|
||||
)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user