mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-10-09 15:58:34 +00:00
6400ce1e75
* fix(terminal): detach stale keychain identity when saving retry credentials When a host references a Keychain identity (host.identityId) with wrong credentials, re-entering credentials via the auth-retry dialog's "Continue & Save" saved them onto the host but kept the identity reference. On reconnect, resolveHostAuth prefers identity credentials over host fields, so the stale keychain credentials kept failing. Clear identityId when the user explicitly saves new credentials so the saved username/password (or key) actually takes effect. Fixes #1956 Co-authored-by: Cursor <cursoragent@cursor.com> * fix(terminal): keep saved retry credentials over group-inherited identity Writing identityId: undefined is treated as "unset" by applyGroupDefaults, so a group-level identity would be re-inherited on reconnect and shadow the credentials saved from the auth-retry dialog. Store an empty string instead and register identityId in EMPTY_STRING_OVERRIDES_GROUP_DEFAULT so the explicit "no identity" override sticks. All consumers check identityId truthiness, so "" behaves as "no identity". Addresses Codex review on #1970. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
179 lines
5.3 KiB
TypeScript
179 lines
5.3 KiB
TypeScript
import type { Terminal as XTerm } from "@xterm/xterm";
|
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
import type { RefObject } from "react";
|
|
import type { Host, TerminalSession } from "../../../types";
|
|
import type { PendingAuth } from "../runtime/createTerminalSessionStarters";
|
|
import type { TerminalAuthMethod } from "../TerminalAuthDialog";
|
|
import { logger } from "../../../lib/logger";
|
|
|
|
export const buildSavedAuthHostUpdate = (
|
|
host: Host,
|
|
auth: {
|
|
authMethod: TerminalAuthMethod;
|
|
username: string;
|
|
password: string;
|
|
keyId: string | null;
|
|
},
|
|
): Host => ({
|
|
...host,
|
|
username: auth.username,
|
|
authMethod: auth.authMethod,
|
|
password: auth.authMethod === "password" ? auth.password : undefined,
|
|
savePassword: auth.authMethod === "password" ? true : host.savePassword,
|
|
identityFileId:
|
|
auth.authMethod === "key" || auth.authMethod === "certificate"
|
|
? (auth.keyId ?? undefined)
|
|
: undefined,
|
|
// Detach stale Keychain identity on explicit credential save (#1956):
|
|
// resolveHostAuth prefers identity credentials over host fields.
|
|
// Empty string (not undefined) so applyGroupDefaults treats this as an explicit
|
|
// host-level override and does not re-inherit a group-level identity; consumers
|
|
// check host.identityId truthiness so "" behaves as "no identity".
|
|
identityId: "",
|
|
});
|
|
|
|
export const useTerminalAuthState = ({
|
|
host,
|
|
pendingAuthRef,
|
|
termRef,
|
|
onUpdateHost,
|
|
onStartSession,
|
|
setStatus,
|
|
setProgressLogs,
|
|
}: {
|
|
host: Host;
|
|
pendingAuthRef: RefObject<PendingAuth>;
|
|
termRef: RefObject<XTerm | null>;
|
|
onUpdateHost?: (host: Host) => void;
|
|
onStartSession: (term: XTerm) => void;
|
|
setStatus: (status: TerminalSession["status"]) => void;
|
|
setProgressLogs: (next: string[] | ((prev: string[]) => string[])) => void;
|
|
}) => {
|
|
const [needsAuth, setNeedsAuth] = useState(false);
|
|
const [authRetryMessage, setAuthRetryMessage] = useState<string | null>(null);
|
|
const [authUsername, setAuthUsername] = useState(host.username || "root");
|
|
const [authMethod, setAuthMethod] = useState<TerminalAuthMethod>("password");
|
|
const [authPassword, setAuthPassword] = useState("");
|
|
const [authKeyId, setAuthKeyId] = useState<string | null>(null);
|
|
const [authPassphrase, setAuthPassphrase] = useState("");
|
|
const [showAuthPassword, setShowAuthPassword] = useState(false);
|
|
const [showAuthPassphrase, setShowAuthPassphrase] = useState(false);
|
|
const [saveCredentials, setSaveCredentials] = useState(true);
|
|
|
|
useEffect(() => {
|
|
setNeedsAuth(false);
|
|
setAuthRetryMessage(null);
|
|
setAuthUsername(host.username || "root");
|
|
setAuthPassword("");
|
|
setAuthKeyId(null);
|
|
setAuthPassphrase("");
|
|
setShowAuthPassword(false);
|
|
setShowAuthPassphrase(false);
|
|
setSaveCredentials(true);
|
|
}, [host.id, host.username]);
|
|
|
|
const isValid = useMemo(() => {
|
|
if (!authUsername.trim()) return false;
|
|
if (authMethod === "password") return authPassword.trim().length > 0;
|
|
if (authMethod === "key" || authMethod === "certificate") return !!authKeyId;
|
|
return false;
|
|
}, [authKeyId, authMethod, authPassword, authUsername]);
|
|
|
|
const resetForRetry = useCallback(() => {
|
|
setNeedsAuth(false);
|
|
setAuthRetryMessage(null);
|
|
pendingAuthRef.current = null;
|
|
}, [pendingAuthRef]);
|
|
|
|
const submit = useCallback(
|
|
(opts?: { saveToHost?: boolean }) => {
|
|
if (!isValid) return;
|
|
|
|
const shouldSave = opts?.saveToHost ?? saveCredentials;
|
|
pendingAuthRef.current = {
|
|
authMethod,
|
|
username: authUsername,
|
|
password: authMethod === "password" ? authPassword : undefined,
|
|
keyId:
|
|
authMethod === "key" || authMethod === "certificate"
|
|
? (authKeyId ?? undefined)
|
|
: undefined,
|
|
passphrase:
|
|
authMethod === "key" || authMethod === "certificate"
|
|
? authPassphrase || undefined
|
|
: undefined,
|
|
savedToHost: shouldSave && Boolean(onUpdateHost),
|
|
};
|
|
|
|
if (shouldSave && onUpdateHost) {
|
|
onUpdateHost(
|
|
buildSavedAuthHostUpdate(host, {
|
|
authMethod,
|
|
username: authUsername,
|
|
password: authPassword,
|
|
keyId: authKeyId,
|
|
}),
|
|
);
|
|
}
|
|
|
|
setNeedsAuth(false);
|
|
setAuthRetryMessage(null);
|
|
setStatus("connecting");
|
|
setProgressLogs(["Authenticating with provided credentials..."]);
|
|
|
|
const term = termRef.current;
|
|
if (!term) return;
|
|
|
|
try {
|
|
term.clear?.();
|
|
} catch (err) {
|
|
logger.warn("Failed to clear terminal", err);
|
|
}
|
|
|
|
onStartSession(term);
|
|
},
|
|
[
|
|
authKeyId,
|
|
authMethod,
|
|
authPassphrase,
|
|
authPassword,
|
|
authUsername,
|
|
host,
|
|
isValid,
|
|
onStartSession,
|
|
onUpdateHost,
|
|
pendingAuthRef,
|
|
saveCredentials,
|
|
setProgressLogs,
|
|
setStatus,
|
|
termRef,
|
|
],
|
|
);
|
|
|
|
return {
|
|
needsAuth,
|
|
setNeedsAuth,
|
|
authRetryMessage,
|
|
setAuthRetryMessage,
|
|
authUsername,
|
|
setAuthUsername,
|
|
authMethod,
|
|
setAuthMethod,
|
|
authPassword,
|
|
setAuthPassword,
|
|
authKeyId,
|
|
setAuthKeyId,
|
|
authPassphrase,
|
|
setAuthPassphrase,
|
|
showAuthPassword,
|
|
setShowAuthPassword,
|
|
showAuthPassphrase,
|
|
setShowAuthPassphrase,
|
|
saveCredentials,
|
|
setSaveCredentials,
|
|
isValid,
|
|
resetForRetry,
|
|
submit,
|
|
};
|
|
};
|