feat: host key dialog and double-click connection flow
Add HostKeyDialog modal with two modes: new host (informational with blue accent) and changed key (warning with red accent). Shows hostname, key type, and fingerprint in monospace. ConnectionTree now has @dblclick handler that calls sessionStore.connect(). Session store gains a connect() method that looks up the connection, checks for existing sessions, and creates a mock session tab. Pre-loaded mock sessions removed — sessions start empty and are created on double-click. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8415c98970
commit
3898a1c3e2
105
frontend/src/components/common/HostKeyDialog.vue
Normal file
105
frontend/src/components/common/HostKeyDialog.vue
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
<template>
|
||||||
|
<Teleport to="body">
|
||||||
|
<div class="fixed inset-0 z-50 flex items-center justify-center">
|
||||||
|
<!-- Backdrop -->
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 bg-black/60"
|
||||||
|
@click="emit('reject')"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Dialog -->
|
||||||
|
<div class="relative bg-[var(--wraith-bg-secondary)] border border-[var(--wraith-border)] rounded-lg shadow-2xl w-[480px] max-w-[90vw]">
|
||||||
|
<!-- Header -->
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-3 px-5 py-4 border-b border-[var(--wraith-border)]"
|
||||||
|
:class="isChanged ? 'bg-red-500/10' : ''"
|
||||||
|
>
|
||||||
|
<!-- Warning icon for changed keys -->
|
||||||
|
<svg
|
||||||
|
v-if="isChanged"
|
||||||
|
class="w-6 h-6 text-[var(--wraith-accent-red)] shrink-0"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path d="M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575L6.457 1.047zM8 5a.75.75 0 0 0-.75.75v2.5a.75.75 0 0 0 1.5 0v-2.5A.75.75 0 0 0 8 5zm1 6a1 1 0 1 0-2 0 1 1 0 0 0 2 0z" />
|
||||||
|
</svg>
|
||||||
|
<!-- Lock icon for new keys -->
|
||||||
|
<svg
|
||||||
|
v-else
|
||||||
|
class="w-6 h-6 text-[var(--wraith-accent-blue)] shrink-0"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path d="M4 4a4 4 0 0 1 8 0v2h.25c.966 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0 1 12.25 15h-8.5A1.75 1.75 0 0 1 2 13.25v-5.5C2 6.784 2.784 6 3.75 6H4V4zm8.25 3.5h-8.5a.25.25 0 0 0-.25.25v5.5c0 .138.112.25.25.25h8.5a.25.25 0 0 0 .25-.25v-5.5a.25.25 0 0 0-.25-.25zM10.5 4a2.5 2.5 0 1 0-5 0v2h5V4z" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2 class="text-sm font-semibold text-[var(--wraith-text-primary)]">
|
||||||
|
{{ isChanged ? 'HOST KEY HAS CHANGED' : 'Unknown Host Key' }}
|
||||||
|
</h2>
|
||||||
|
<p class="text-xs text-[var(--wraith-text-muted)] mt-0.5">
|
||||||
|
{{ isChanged ? 'The server key does not match the stored key!' : 'This host has not been seen before.' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Body -->
|
||||||
|
<div class="px-5 py-4 space-y-3">
|
||||||
|
<p v-if="isChanged" class="text-xs text-[var(--wraith-accent-red)]">
|
||||||
|
WARNING: This could indicate a man-in-the-middle attack. Only accept if you know the host key was recently changed.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex items-center gap-2 text-xs">
|
||||||
|
<span class="text-[var(--wraith-text-muted)] w-16 shrink-0">Host:</span>
|
||||||
|
<span class="text-[var(--wraith-text-primary)] font-mono">{{ hostname }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 text-xs">
|
||||||
|
<span class="text-[var(--wraith-text-muted)] w-16 shrink-0">Key type:</span>
|
||||||
|
<span class="text-[var(--wraith-text-primary)] font-mono">{{ keyType }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-start gap-2 text-xs">
|
||||||
|
<span class="text-[var(--wraith-text-muted)] w-16 shrink-0">Fingerprint:</span>
|
||||||
|
<span class="text-[var(--wraith-accent-blue)] font-mono break-all select-text">{{ fingerprint }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<div class="flex items-center justify-end gap-3 px-5 py-3 border-t border-[var(--wraith-border)]">
|
||||||
|
<button
|
||||||
|
class="px-4 py-1.5 text-xs rounded bg-[var(--wraith-bg-tertiary)] text-[var(--wraith-text-secondary)] hover:text-[var(--wraith-text-primary)] hover:bg-[var(--wraith-border)] transition-colors cursor-pointer"
|
||||||
|
@click="emit('reject')"
|
||||||
|
>
|
||||||
|
Reject
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="px-4 py-1.5 text-xs rounded transition-colors cursor-pointer"
|
||||||
|
:class="
|
||||||
|
isChanged
|
||||||
|
? 'bg-[var(--wraith-accent-red)] text-white hover:bg-red-600'
|
||||||
|
: 'bg-[var(--wraith-accent-blue)] text-white hover:bg-blue-600'
|
||||||
|
"
|
||||||
|
@click="emit('accept')"
|
||||||
|
>
|
||||||
|
{{ isChanged ? 'Accept Anyway' : 'Accept' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineProps<{
|
||||||
|
hostname: string;
|
||||||
|
keyType: string;
|
||||||
|
fingerprint: string;
|
||||||
|
isChanged: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
accept: [];
|
||||||
|
reject: [];
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
@ -41,6 +41,7 @@
|
|||||||
v-for="conn in connectionStore.connectionsByGroup(group.id)"
|
v-for="conn in connectionStore.connectionsByGroup(group.id)"
|
||||||
:key="conn.id"
|
:key="conn.id"
|
||||||
class="w-full flex items-center gap-2 pl-8 pr-3 py-1.5 text-xs hover:bg-[var(--wraith-bg-tertiary)] transition-colors cursor-pointer"
|
class="w-full flex items-center gap-2 pl-8 pr-3 py-1.5 text-xs hover:bg-[var(--wraith-bg-tertiary)] transition-colors cursor-pointer"
|
||||||
|
@dblclick="handleConnect(conn)"
|
||||||
>
|
>
|
||||||
<!-- Protocol dot -->
|
<!-- Protocol dot -->
|
||||||
<span
|
<span
|
||||||
@ -64,9 +65,11 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { useConnectionStore } from "@/stores/connection.store";
|
import { useConnectionStore, type Connection } from "@/stores/connection.store";
|
||||||
|
import { useSessionStore } from "@/stores/session.store";
|
||||||
|
|
||||||
const connectionStore = useConnectionStore();
|
const connectionStore = useConnectionStore();
|
||||||
|
const sessionStore = useSessionStore();
|
||||||
|
|
||||||
// All groups expanded by default
|
// All groups expanded by default
|
||||||
const expandedGroups = ref<Set<number>>(
|
const expandedGroups = ref<Set<number>>(
|
||||||
@ -80,4 +83,9 @@ function toggleGroup(groupId: number): void {
|
|||||||
expandedGroups.value.add(groupId);
|
expandedGroups.value.add(groupId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Double-click a connection to open a new session. */
|
||||||
|
function handleConnect(conn: Connection): void {
|
||||||
|
sessionStore.connect(conn.id);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { ref, computed } from "vue";
|
import { ref, computed } from "vue";
|
||||||
|
import { useConnectionStore } from "@/stores/connection.store";
|
||||||
|
|
||||||
export interface Session {
|
export interface Session {
|
||||||
id: string;
|
id: string;
|
||||||
@ -17,13 +18,9 @@ export interface Session {
|
|||||||
* For now, mock sessions are used to render the tab bar.
|
* For now, mock sessions are used to render the tab bar.
|
||||||
*/
|
*/
|
||||||
export const useSessionStore = defineStore("session", () => {
|
export const useSessionStore = defineStore("session", () => {
|
||||||
const sessions = ref<Session[]>([
|
const sessions = ref<Session[]>([]);
|
||||||
{ id: "s1", connectionId: 1, name: "Asgard", protocol: "ssh", active: true },
|
|
||||||
{ id: "s2", connectionId: 2, name: "Docker", protocol: "ssh", active: false },
|
|
||||||
{ id: "s3", connectionId: 4, name: "CLT-VMHOST01", protocol: "rdp", active: false },
|
|
||||||
]);
|
|
||||||
|
|
||||||
const activeSessionId = ref<string | null>("s1");
|
const activeSessionId = ref<string | null>(null);
|
||||||
|
|
||||||
const activeSession = computed(() =>
|
const activeSession = computed(() =>
|
||||||
sessions.value.find((s) => s.id === activeSessionId.value) ?? null,
|
sessions.value.find((s) => s.id === activeSessionId.value) ?? null,
|
||||||
@ -61,6 +58,31 @@ export const useSessionStore = defineStore("session", () => {
|
|||||||
activeSessionId.value = id;
|
activeSessionId.value = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect to a server by connection ID.
|
||||||
|
* Creates a new session tab and sets it active.
|
||||||
|
*
|
||||||
|
* TODO: Replace with Wails binding call — SSHService.Connect(hostname, port, ...)
|
||||||
|
* For now, creates a mock session using the connection's name.
|
||||||
|
*/
|
||||||
|
function connect(connectionId: number): void {
|
||||||
|
const connectionStore = useConnectionStore();
|
||||||
|
const conn = connectionStore.connections.find((c) => c.id === connectionId);
|
||||||
|
if (!conn) return;
|
||||||
|
|
||||||
|
// Check if there's already an active session for this connection
|
||||||
|
const existing = sessions.value.find((s) => s.connectionId === connectionId);
|
||||||
|
if (existing) {
|
||||||
|
activeSessionId.value = existing.id;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Replace with Wails binding call:
|
||||||
|
// const sessionId = await SSHService.Connect(conn.hostname, conn.port, username, authMethods, cols, rows)
|
||||||
|
// For now, create a mock session
|
||||||
|
addSession(connectionId, conn.name, conn.protocol);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sessions,
|
sessions,
|
||||||
activeSessionId,
|
activeSessionId,
|
||||||
@ -69,5 +91,6 @@ export const useSessionStore = defineStore("session", () => {
|
|||||||
activateSession,
|
activateSession,
|
||||||
closeSession,
|
closeSession,
|
||||||
addSession,
|
addSession,
|
||||||
|
connect,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user