fix: SSH password prompt on auth failure, version from Go backend, visible errors
- 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:
parent
b46c20b0d0
commit
19207fb6ee
@ -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 {
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -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(
|
||||
`${APP}.ConnectSSH`,
|
||||
connectionId,
|
||||
120, // cols (will be resized by xterm.js fit addon)
|
||||
40, // rows
|
||||
) as string;
|
||||
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,
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user