fix: SSH password prompt on auth failure, version from Go backend, visible errors
All checks were successful
Build & Sign Wraith / Build Windows + Sign (push) Successful in 1m5s

- ConnectSSH returns NO_CREDENTIALS error when no credential is stored
- Frontend catches auth failures and prompts for username/password
- ConnectSSHWithPassword method for ad-hoc password auth
- Version loaded from Go backend (build-time -ldflags) in settings + unlock screen
- Connection errors shown as alert() instead of silent console.error
- Added UpdateService.CurrentVersion() and WraithApp.GetVersion()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vantz Stockwell 2026-03-17 11:38:08 -04:00
parent b46c20b0d0
commit 163af456b4
5 changed files with 108 additions and 21 deletions

View File

@ -335,6 +335,9 @@ onMounted(async () => {
if (theme) settings.value.terminalTheme = theme;
if (fontSize) settings.value.fontSize = Number(fontSize);
if (scrollback) settings.value.scrollbackBuffer = Number(scrollback);
// Load version from Go backend
const ver = await Call.ByName(`${APP}.GetVersion`) as string;
if (ver) currentVersion.value = ver;
} catch (err) {
console.error("Failed to load settings:", err);
}
@ -360,7 +363,8 @@ watch(() => settings.value.scrollbackBuffer, (val) => {
// --- Update check state ---
type UpdateCheckState = "idle" | "checking" | "found" | "up-to-date" | "error";
const updateCheckState = ref<UpdateCheckState>("idle");
const currentVersion = ref("0.1.0-dev");
const APP = "github.com/vstockwell/wraith/internal/app.WraithApp";
const currentVersion = ref("loading...");
const latestVersion = ref("");
function open(): void {

View File

@ -68,7 +68,7 @@
<!-- Version -->
<p class="text-center text-xs text-[var(--wraith-text-muted)] mt-4">
v1.0.0-alpha
v{{ appVersion }}
</p>
</div>
</div>
@ -76,17 +76,24 @@
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { Call } from "@wailsio/runtime";
import { useAppStore } from "@/stores/app.store";
const APP = "github.com/vstockwell/wraith/internal/app.WraithApp";
const appStore = useAppStore();
const password = ref("");
const confirmPassword = ref("");
const submitting = ref(false);
const passwordInput = ref<HTMLInputElement | null>(null);
const appVersion = ref("...");
onMounted(() => {
onMounted(async () => {
passwordInput.value?.focus();
try {
const ver = await Call.ByName(`${APP}.GetVersion`) as string;
if (ver) appVersion.value = ver;
} catch { /* ignore */ }
});
async function handleSubmit(): Promise<void> {

View File

@ -17,6 +17,7 @@ export const useSessionStore = defineStore("session", () => {
const sessions = ref<Session[]>([]);
const activeSessionId = ref<string | null>(null);
const connecting = ref(false);
const lastError = ref<string | null>(null);
const activeSession = computed(() =>
sessions.value.find((s) => s.id === activeSessionId.value) ?? null,
@ -72,13 +73,38 @@ export const useSessionStore = defineStore("session", () => {
connecting.value = true;
try {
if (conn.protocol === "ssh") {
// Call Go backend — resolves credentials, builds auth, returns sessionID
const sessionId = await Call.ByName(
let sessionId: string;
try {
// Try with stored credentials first
sessionId = await Call.ByName(
`${APP}.ConnectSSH`,
connectionId,
120, // cols (will be resized by xterm.js fit addon)
40, // rows
) as string;
} catch (sshErr: any) {
const errMsg = typeof sshErr === "string" ? sshErr : sshErr?.message ?? String(sshErr);
// If no credentials, prompt for username/password
if (errMsg.includes("NO_CREDENTIALS") || errMsg.includes("unable to authenticate")) {
const username = prompt(`Username for ${conn.hostname}:`, "root");
if (!username) throw new Error("Connection cancelled");
const password = prompt(`Password for ${username}@${conn.hostname}:`);
if (password === null) throw new Error("Connection cancelled");
sessionId = await Call.ByName(
`${APP}.ConnectSSHWithPassword`,
connectionId,
username,
password,
120,
40,
) as string;
} else {
throw sshErr;
}
}
sessions.value.push({
id: sessionId,
@ -106,9 +132,12 @@ export const useSessionStore = defineStore("session", () => {
});
activeSessionId.value = sessionId;
}
} catch (err) {
console.error("Connection failed:", err);
// TODO: Show error toast in UI
} catch (err: any) {
const msg = typeof err === "string" ? err : err?.message ?? String(err);
console.error("Connection failed:", msg);
lastError.value = msg;
// Show error as native alert so it's visible without DevTools
alert(`Connection failed: ${msg}`);
} finally {
connecting.value = false;
}
@ -120,6 +149,7 @@ export const useSessionStore = defineStore("session", () => {
activeSession,
sessionCount,
connecting,
lastError,
activateSession,
closeSession,
connect,

View File

@ -301,15 +301,9 @@ func (a *WraithApp) ConnectSSH(connectionID int64, cols, rows int) (string, erro
}
}
// Fallback: keyboard-interactive (prompts for password)
// If no stored credentials, return an error telling the frontend to prompt
if len(authMethods) == 0 {
slog.Info("no stored credentials, SSH will attempt keyboard-interactive auth")
authMethods = append(authMethods, gossh.KeyboardInteractive(
func(name, instruction string, questions []string, echos []bool) ([]string, error) {
// No interactive prompt available yet — return empty
return make([]string, len(questions)), nil
},
))
return "", fmt.Errorf("NO_CREDENTIALS: no credentials configured for this connection — assign a credential or use ConnectSSHWithPassword")
}
sessionID, err := a.SSH.Connect(conn.Hostname, conn.Port, username, authMethods, cols, rows)
@ -337,6 +331,53 @@ func (a *WraithApp) ConnectSSH(connectionID int64, cols, rows int) (string, erro
return sessionID, nil
}
// ConnectSSHWithPassword opens an SSH session using an ad-hoc username/password.
// Used when no stored credential exists — the frontend prompts and passes creds directly.
func (a *WraithApp) ConnectSSHWithPassword(connectionID int64, username, password string, cols, rows int) (string, error) {
conn, err := a.Connections.GetConnection(connectionID)
if err != nil {
return "", fmt.Errorf("connection not found: %w", err)
}
authMethods := []gossh.AuthMethod{
gossh.Password(password),
gossh.KeyboardInteractive(func(name, instruction string, questions []string, echos []bool) ([]string, error) {
answers := make([]string, len(questions))
for i := range answers {
answers[i] = password
}
return answers, nil
}),
}
sessionID, err := a.SSH.Connect(conn.Hostname, conn.Port, username, authMethods, cols, rows)
if err != nil {
return "", fmt.Errorf("SSH connect failed: %w", err)
}
// Register SFTP client
if sess, ok := a.SSH.GetSession(sessionID); ok && sess != nil {
sftpClient, err := sftplib.NewClient(sess.Client)
if err != nil {
slog.Warn("failed to create SFTP client", "error", err)
} else {
a.SFTP.RegisterClient(sessionID, sftpClient)
}
}
if _, err := a.db.Exec("UPDATE connections SET last_connected = CURRENT_TIMESTAMP WHERE id = ?", connectionID); err != nil {
slog.Warn("failed to update last_connected", "error", err)
}
slog.Info("SSH session started (ad-hoc password)", "sessionID", sessionID, "host", conn.Hostname, "user", username)
return sessionID, nil
}
// GetVersion returns the build-time version string.
func (a *WraithApp) GetVersion() string {
return a.Updater.CurrentVersion()
}
// DisconnectSession closes an active SSH session and its SFTP client.
func (a *WraithApp) DisconnectSession(sessionID string) error {
a.SFTP.RemoveClient(sessionID)

View File

@ -36,6 +36,11 @@ type UpdateService struct {
httpClient *http.Client
}
// CurrentVersion returns the build-time version string.
func (u *UpdateService) CurrentVersion() string {
return u.currentVersion
}
// NewUpdateService creates an UpdateService that compares against the
// supplied currentVersion (semver without "v" prefix, e.g. "0.2.0").
func NewUpdateService(currentVersion string) *UpdateService {