Add SSH TOTP authentication support (#350)

* Add SSH TOTP authentication support

- Implement keyboard-interactive authentication for SSH connections
- Add TOTP dialog component for Terminal and File Manager
- Handle TOTP prompts in WebSocket and HTTP connections
- Disable Server Stats for TOTP-enabled servers
- Add i18n support for TOTP-related messages

* Update src/backend/ssh/server-stats.ts

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* Update src/backend/ssh/file-manager.ts

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

---------

Co-authored-by: ZacharyZcR <zacharyzcr1984@gmail.com>
Co-authored-by: Karmaa <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
This commit was merged in pull request #350.
This commit is contained in:
ZacharyZcR
2025-10-08 09:01:48 +08:00
committed by GitHub
parent 24d622d8e4
commit fde64bd3df
10 changed files with 511 additions and 16 deletions

View File

@@ -13,6 +13,7 @@ import { WebLinksAddon } from "@xterm/addon-web-links";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { getCookie, isElectron } from "@/ui/main-axios.ts";
import { TOTPDialog } from "@/ui/components/TOTPDialog";
interface SSHTerminalProps {
hostConfig: any;
@@ -55,6 +56,8 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
const [isConnecting, setIsConnecting] = useState(false);
const [connectionError, setConnectionError] = useState<string | null>(null);
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [totpRequired, setTotpRequired] = useState(false);
const [totpPrompt, setTotpPrompt] = useState<string>("");
const isVisibleRef = useRef<boolean>(false);
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const reconnectAttempts = useRef(0);
@@ -102,6 +105,25 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
} catch (_) {}
}
function handleTotpSubmit(code: string) {
if (webSocketRef.current && code) {
webSocketRef.current.send(
JSON.stringify({
type: "totp_response",
data: { code },
}),
);
setTotpRequired(false);
setTotpPrompt("");
}
}
function handleTotpCancel() {
setTotpRequired(false);
setTotpPrompt("");
if (onClose) onClose();
}
function scheduleNotify(cols: number, rows: number) {
if (!(cols > 0 && rows > 0)) return;
pendingSizeRef.current = { cols, rows };
@@ -422,6 +444,9 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
if (onClose) {
onClose();
}
} else if (msg.type === "totp_required") {
setTotpRequired(true);
setTotpPrompt(msg.prompt || "Verification code:");
}
} catch (error) {
toast.error(t("terminal.messageParseError"));
@@ -729,6 +754,13 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
</div>
</div>
)}
<TOTPDialog
isOpen={totpRequired}
prompt={totpPrompt}
onSubmit={handleTotpSubmit}
onCancel={handleTotpCancel}
/>
</div>
);
});