Feature Sudo password auto-fill;

This commit is contained in:
Nunzio Marfè
2025-12-05 18:08:33 +01:00
parent f0647dc7c1
commit 8d52bef631
11 changed files with 1585 additions and 1422 deletions

View File

@@ -705,6 +705,7 @@ export const DEFAULT_TERMINAL_CONFIG = {
startupSnippetId: null as number | null, startupSnippetId: null as number | null,
autoMosh: false, autoMosh: false,
moshCommand: "mosh-server new -s -l LANG=en_US.UTF-8", moshCommand: "mosh-server new -s -l LANG=en_US.UTF-8",
sudoPasswordAutoFill: false,
}; };
export type TerminalConfigType = typeof DEFAULT_TERMINAL_CONFIG; export type TerminalConfigType = typeof DEFAULT_TERMINAL_CONFIG;

View File

@@ -799,7 +799,9 @@
"searchServers": "Server durchsuchen...", "searchServers": "Server durchsuchen...",
"noServerFound": "Kein Server gefunden", "noServerFound": "Kein Server gefunden",
"jumpHostsOrder": "Verbindungen werden in dieser Reihenfolge hergestellt: Jump-Host 1 → Jump-Host 2 → ... → Ziel-Server", "jumpHostsOrder": "Verbindungen werden in dieser Reihenfolge hergestellt: Jump-Host 1 → Jump-Host 2 → ... → Ziel-Server",
"advancedAuthSettings": "Erweiterte Authentifizierungseinstellungen" "advancedAuthSettings": "Erweiterte Authentifizierungseinstellungen",
"sudoPasswordAutoFill": "Sudo-Passwort automatisch ausfüllen",
"sudoPasswordAutoFillDesc": "Popup anzeigen, um das Passwort bei sudo-Befehlen automatisch einzufügen"
}, },
"terminal": { "terminal": {
"title": "Terminal", "title": "Terminal",
@@ -833,7 +835,11 @@
"connectionTimeout": "Zeitüberschreitung der Verbindung", "connectionTimeout": "Zeitüberschreitung der Verbindung",
"terminalTitle": "Terminal - {{host}}", "terminalTitle": "Terminal - {{host}}",
"terminalWithPath": "Terminal - {{host}} : {{path}}", "terminalWithPath": "Terminal - {{host}} : {{path}}",
"runTitle": "Ausführen von {{command}} {{host}}" "runTitle": "Ausführen von {{command}} {{host}}",
"sudoPasswordPopupTitle": "Passwort einfügen?",
"sudoPasswordPopupHint": "Drücken Sie Enter zum Einfügen, Esc zum Abbrechen",
"sudoPasswordPopupConfirm": "Einfügen",
"sudoPasswordPopupDismiss": "Abbrechen"
}, },
"fileManager": { "fileManager": {
"title": "Dateimanager", "title": "Dateimanager",

View File

@@ -908,7 +908,9 @@
"quickActionName": "Action name", "quickActionName": "Action name",
"noSnippetFound": "No snippet found", "noSnippetFound": "No snippet found",
"quickActionsOrder": "Quick action buttons will appear in the order listed above on the Server Stats page", "quickActionsOrder": "Quick action buttons will appear in the order listed above on the Server Stats page",
"advancedAuthSettings": "Advanced Authentication Settings" "advancedAuthSettings": "Advanced Authentication Settings",
"sudoPasswordAutoFill": "Sudo Password Auto-Fill",
"sudoPasswordAutoFillDesc": "Automatically offer to insert SSH password when sudo prompts for password"
}, },
"terminal": { "terminal": {
"title": "Terminal", "title": "Terminal",
@@ -946,7 +948,11 @@
"totpRequired": "Two-Factor Authentication Required", "totpRequired": "Two-Factor Authentication Required",
"totpCodeLabel": "Verification Code", "totpCodeLabel": "Verification Code",
"totpPlaceholder": "000000", "totpPlaceholder": "000000",
"totpVerify": "Verify" "totpVerify": "Verify",
"sudoPasswordPopupTitle": "Insert Password?",
"sudoPasswordPopupHint": "Press Enter to insert, Esc to dismiss",
"sudoPasswordPopupConfirm": "Insert",
"sudoPasswordPopupDismiss": "Dismiss"
}, },
"fileManager": { "fileManager": {
"title": "File Manager", "title": "File Manager",

View File

@@ -790,7 +790,9 @@
"searchServers": "Rechercher des serveurs...", "searchServers": "Rechercher des serveurs...",
"noServerFound": "Aucun serveur trouvé", "noServerFound": "Aucun serveur trouvé",
"jumpHostsOrder": "Les connexions seront établies dans l'ordre : Serveur de rebond 1 → Serveur de rebond 2 → ... → Serveur cible", "jumpHostsOrder": "Les connexions seront établies dans l'ordre : Serveur de rebond 1 → Serveur de rebond 2 → ... → Serveur cible",
"advancedAuthSettings": "Paramètres d'authentification avancés" "advancedAuthSettings": "Paramètres d'authentification avancés",
"sudoPasswordAutoFill": "Remplissage automatique du mot de passe sudo",
"sudoPasswordAutoFillDesc": "Proposer automatiquement dinsérer le mot de passe SSH lorsque sudo demande un mot de passe"
}, },
"terminal": { "terminal": {
"title": "Terminal", "title": "Terminal",
@@ -828,7 +830,11 @@
"totpRequired": "Authentification à deux facteurs requise", "totpRequired": "Authentification à deux facteurs requise",
"totpCodeLabel": "Code de vérification", "totpCodeLabel": "Code de vérification",
"totpPlaceholder": "000000", "totpPlaceholder": "000000",
"totpVerify": "Vérifier" "totpVerify": "Vérifier",
"sudoPasswordPopupTitle": "Insérer le mot de passe ?",
"sudoPasswordPopupHint": "Appuyez sur Entrée pour insérer, Échap pour annuler",
"sudoPasswordPopupConfirm": "Insérer",
"sudoPasswordPopupDismiss": "Annuler"
}, },
"fileManager": { "fileManager": {
"title": "Gestionnaire de fichiers", "title": "Gestionnaire de fichiers",

View File

@@ -745,7 +745,9 @@
"searchServers": "Pesquisar servidores...", "searchServers": "Pesquisar servidores...",
"noServerFound": "Nenhum servidor encontrado", "noServerFound": "Nenhum servidor encontrado",
"jumpHostsOrder": "As conexões serão feitas na ordem: Host de Salto 1 → Host de Salto 2 → ... → Servidor de Destino", "jumpHostsOrder": "As conexões serão feitas na ordem: Host de Salto 1 → Host de Salto 2 → ... → Servidor de Destino",
"advancedAuthSettings": "Configurações Avançadas de Autenticação" "advancedAuthSettings": "Configurações Avançadas de Autenticação",
"sudoPasswordAutoFill": "Preenchimento automático da senha do sudo",
"sudoPasswordAutoFillDesc": "Oferecer automaticamente inserir a senha SSH quando o sudo solicitar uma senha"
}, },
"terminal": { "terminal": {
"title": "Terminal", "title": "Terminal",
@@ -779,7 +781,11 @@
"connectionTimeout": "Tempo limite de conexão esgotado", "connectionTimeout": "Tempo limite de conexão esgotado",
"terminalTitle": "Terminal - {{host}}", "terminalTitle": "Terminal - {{host}}",
"terminalWithPath": "Terminal - {{host}}:{{path}}", "terminalWithPath": "Terminal - {{host}}:{{path}}",
"runTitle": "Executando {{command}} - {{host}}" "runTitle": "Executando {{command}} - {{host}}",
"sudoPasswordPopupTitle": "Inserir Senha?",
"sudoPasswordPopupHint": "Pressione Enter para inserir, Esc para cancelar",
"sudoPasswordPopupConfirm": "Inserir",
"sudoPasswordPopupDismiss": "Cancelar"
}, },
"fileManager": { "fileManager": {
"title": "Gerenciador de Arquivos", "title": "Gerenciador de Arquivos",
@@ -1034,7 +1040,6 @@
"fileSavedSuccessfully": "Arquivo salvo com sucesso", "fileSavedSuccessfully": "Arquivo salvo com sucesso",
"autoSaveFailed": "Falha no salvamento automático", "autoSaveFailed": "Falha no salvamento automático",
"fileAutoSaved": "Arquivo salvo automaticamente", "fileAutoSaved": "Arquivo salvo automaticamente",
"moveFileFailed": "Falha ao mover {{name}}", "moveFileFailed": "Falha ao mover {{name}}",
"moveOperationFailed": "Falha na operação de mover", "moveOperationFailed": "Falha na operação de mover",
"canOnlyCompareFiles": "Só é possível comparar dois arquivos", "canOnlyCompareFiles": "Só é possível comparar dois arquivos",

View File

@@ -858,7 +858,9 @@
"searchServers": "Поиск серверов...", "searchServers": "Поиск серверов...",
"noServerFound": "Сервер не найден", "noServerFound": "Сервер не найден",
"jumpHostsOrder": "Подключения будут выполнены в порядке: Промежуточный хост 1 → Промежуточный хост 2 → ... → Целевой сервер", "jumpHostsOrder": "Подключения будут выполнены в порядке: Промежуточный хост 1 → Промежуточный хост 2 → ... → Целевой сервер",
"advancedAuthSettings": "Расширенные настройки аутентификации" "advancedAuthSettings": "Расширенные настройки аутентификации",
"sudoPasswordAutoFill": "Автозаполнение пароля sudo",
"sudoPasswordAutoFillDesc": "Показывать всплывающее окно для автоматического ввода пароля при выполнении команд sudo",
}, },
"terminal": { "terminal": {
"title": "Терминал", "title": "Терминал",
@@ -896,7 +898,11 @@
"totpRequired": "Требуется двухфакторная аутентификация", "totpRequired": "Требуется двухфакторная аутентификация",
"totpCodeLabel": "Код проверки", "totpCodeLabel": "Код проверки",
"totpPlaceholder": "000000", "totpPlaceholder": "000000",
"totpVerify": "Проверить" "totpVerify": "Проверить",
"sudoPasswordPopupTitle": "Вставить пароль?",
"sudoPasswordPopupHint": "Нажмите Enter для вставки, Esc для отмены",
"sudoPasswordPopupConfirm": "Вставить",
"sudoPasswordPopupDismiss": "Отмена"
}, },
"fileManager": { "fileManager": {
"title": "Файловый менеджер", "title": "Файловый менеджер",

View File

@@ -893,7 +893,9 @@
"searchServers": "搜索服务器...", "searchServers": "搜索服务器...",
"noServerFound": "未找到服务器", "noServerFound": "未找到服务器",
"jumpHostsOrder": "连接将按顺序进行:跳板主机 1 → 跳板主机 2 → ... → 目标服务器", "jumpHostsOrder": "连接将按顺序进行:跳板主机 1 → 跳板主机 2 → ... → 目标服务器",
"advancedAuthSettings": "高级身份验证设置" "advancedAuthSettings": "高级身份验证设置",
"sudoPasswordAutoFill": "Sudo 密码自动填充",
"sudoPasswordAutoFillDesc": "在 sudo 命令时显示弹窗以自动输入密码"
}, },
"terminal": { "terminal": {
"title": "终端", "title": "终端",
@@ -931,7 +933,11 @@
"reconnecting": "重新连接中... ({{attempt}}/{{max}})", "reconnecting": "重新连接中... ({{attempt}}/{{max}})",
"reconnected": "重新连接成功", "reconnected": "重新连接成功",
"maxReconnectAttemptsReached": "已达到最大重连尝试次数", "maxReconnectAttemptsReached": "已达到最大重连尝试次数",
"connectionTimeout": "连接超时" "connectionTimeout": "连接超时",
"sudoPasswordPopupTitle": "插入密码?",
"sudoPasswordPopupHint": "按 Enter 插入Esc 取消",
"sudoPasswordPopupConfirm": "插入",
"sudoPasswordPopupDismiss": "取消"
}, },
"fileManager": { "fileManager": {
"title": "文件管理器", "title": "文件管理器",

View File

@@ -307,6 +307,7 @@ export interface TerminalConfig {
startupSnippetId: number | null; startupSnippetId: number | null;
autoMosh: boolean; autoMosh: boolean;
moshCommand: string; moshCommand: string;
sudoPasswordAutoFill: boolean;
} }
// ============================================================================ // ============================================================================

View File

@@ -547,6 +547,7 @@ export function HostManagerEditor({
startupSnippetId: z.number().nullable(), startupSnippetId: z.number().nullable(),
autoMosh: z.boolean(), autoMosh: z.boolean(),
moshCommand: z.string(), moshCommand: z.string(),
sudoPasswordAutoFill: z.boolean(),
}) })
.optional(), .optional(),
forceKeyboardInteractive: z.boolean().optional(), forceKeyboardInteractive: z.boolean().optional(),
@@ -631,7 +632,7 @@ export function HostManagerEditor({
type FormData = z.infer<typeof formSchema>; type FormData = z.infer<typeof formSchema>;
const form = useForm<FormData>({ const form = useForm<FormData>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema) as any,
defaultValues: { defaultValues: {
name: "", name: "",
ip: "", ip: "",
@@ -2443,6 +2444,29 @@ export function HostManagerEditor({
/> />
)} )}
<FormField
control={form.control}
name="terminalConfig.sudoPasswordAutoFill"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3">
<div className="space-y-0.5">
<FormLabel>
{t("hosts.sudoPasswordAutoFill")}
</FormLabel>
<FormDescription>
{t("hosts.sudoPasswordAutoFillDesc")}
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium"> <label className="text-sm font-medium">
Environment Variables Environment Variables

View File

@@ -0,0 +1,82 @@
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { KeyRound } from "lucide-react";
import { Button } from "@/components/ui/button.tsx";
interface SudoPasswordPopupProps {
isOpen: boolean;
hostPassword: string;
backgroundColor: string;
onConfirm: (password: string) => void;
onDismiss: () => void;
}
export function SudoPasswordPopup({
isOpen,
hostPassword,
backgroundColor,
onConfirm,
onDismiss
}: SudoPasswordPopupProps) {
const { t } = useTranslation();
useEffect(() => {
if (!isOpen) return;
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Enter") {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
onConfirm(hostPassword);
} else if (e.key === "Escape") {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
onDismiss();
}
};
window.addEventListener("keydown", handleKeyDown, true);
return () => window.removeEventListener("keydown", handleKeyDown, true);
}, [isOpen, onConfirm, onDismiss, hostPassword]);
if (!isOpen) return null;
return (
<div
className="absolute bottom-4 right-4 z-50 backdrop-blur-sm border border-border rounded-lg shadow-lg p-4 min-w-[280px]"
style={{ backgroundColor: backgroundColor }}
>
<div className="flex items-center gap-3 mb-3">
<div className="p-2 bg-primary/10 rounded-full">
<KeyRound className="h-5 w-5 text-primary" />
</div>
<div>
<h4 className="font-medium text-sm">
{t("terminal.sudoPasswordPopupTitle", "Insert password?")}
</h4>
<p className="text-xs text-muted-foreground">
{t("terminal.sudoPasswordPopupHint", "Press Enter to insert, Esc to dismiss")}
</p>
</div>
</div>
<div className="flex gap-2 justify-end">
<Button
variant="ghost"
size="sm"
onClick={onDismiss}
>
{t("terminal.sudoPasswordPopupDismiss", "Dismiss")}
</Button>
<Button
variant="default"
size="sm"
onClick={() => onConfirm(hostPassword)}
>
{t("terminal.sudoPasswordPopupConfirm", "Insert")}
</Button>
</div>
</div>
);
}

View File

@@ -32,6 +32,7 @@ import { useCommandHistory as useCommandHistoryHook } from "@/ui/hooks/useComman
import { useCommandHistory } from "@/ui/desktop/apps/terminal/command-history/CommandHistoryContext.tsx"; import { useCommandHistory } from "@/ui/desktop/apps/terminal/command-history/CommandHistoryContext.tsx";
import { CommandAutocomplete } from "./command-history/CommandAutocomplete.tsx"; import { CommandAutocomplete } from "./command-history/CommandAutocomplete.tsx";
import { SimpleLoader } from "@/ui/desktop/navigation/animations/SimpleLoader.tsx"; import { SimpleLoader } from "@/ui/desktop/navigation/animations/SimpleLoader.tsx";
import { SudoPasswordPopup } from "./SudoPasswordPopup.tsx";
interface HostConfig { interface HostConfig {
id?: number; id?: number;
@@ -173,6 +174,7 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
const [showHistoryDialog, setShowHistoryDialog] = useState(false); const [showHistoryDialog, setShowHistoryDialog] = useState(false);
const [commandHistory, setCommandHistory] = useState<string[]>([]); const [commandHistory, setCommandHistory] = useState<string[]>([]);
const [isLoadingHistory, setIsLoadingHistory] = useState(false); const [isLoadingHistory, setIsLoadingHistory] = useState(false);
const [showSudoPasswordPopup, setShowSudoPasswordPopup] = useState(false);
const setIsLoadingRef = useRef(commandHistoryContext.setIsLoading); const setIsLoadingRef = useRef(commandHistoryContext.setIsLoading);
const setCommandHistoryContextRef = useRef( const setCommandHistoryContextRef = useRef(
@@ -660,6 +662,11 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
if (msg.type === "data") { if (msg.type === "data") {
if (typeof msg.data === "string") { if (typeof msg.data === "string") {
terminal.write(msg.data); terminal.write(msg.data);
// Sudo password prompt detection
const sudoPasswordPattern = /(?:\[sudo\] password for \S+:|sudo: a password is required)/;
if (config.sudoPasswordAutoFill && sudoPasswordPattern.test(msg.data)) {
setShowSudoPasswordPopup(true);
}
} else { } else {
terminal.write(String(msg.data)); terminal.write(String(msg.data));
} }
@@ -1500,6 +1507,19 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
onSelect={handleAutocompleteSelect} onSelect={handleAutocompleteSelect}
/> />
<SudoPasswordPopup
isOpen={showSudoPasswordPopup}
hostPassword={hostConfig.password || ""}
backgroundColor={backgroundColor}
onConfirm={(password) => {
setShowSudoPasswordPopup(false);
if (webSocketRef.current && webSocketRef.current.readyState === WebSocket.OPEN) {
webSocketRef.current.send(JSON.stringify({ type: "input", data: password + "\n" }));
}
}}
onDismiss={() => setShowSudoPasswordPopup(false)}
/>
<SimpleLoader <SimpleLoader
visible={isConnecting} visible={isConnecting}
message={t("terminal.connecting")} message={t("terminal.connecting")}