Add SSH connection validation for settings
This commit is contained in:
@@ -34,6 +34,7 @@ import type {
|
||||
ResolvedPath,
|
||||
ResetLocalDatabaseInput,
|
||||
ResetLocalDatabaseResponse,
|
||||
SSHValidationResult,
|
||||
DashboardShortcut,
|
||||
DashboardShortcutInput,
|
||||
} from "../types";
|
||||
@@ -330,6 +331,8 @@ export const saveMonitoringMachine = (machine: MonitoringMachineInput) =>
|
||||
}
|
||||
return response.json() as Promise<MonitoringMachine>;
|
||||
});
|
||||
export const testMonitoringMachineSSH = (machine: MonitoringMachineInput) =>
|
||||
post<SSHValidationResult>("/api/settings/machines/test-ssh", machine);
|
||||
export const deleteMonitoringMachine = (machineId: string) =>
|
||||
del<{ status: string }>(
|
||||
`/api/settings/machines/${encodeURIComponent(machineId)}`,
|
||||
|
||||
@@ -13,12 +13,14 @@ import {
|
||||
saveTask,
|
||||
deleteTask,
|
||||
runTask,
|
||||
testMonitoringMachineSSH,
|
||||
} from "../api/client";
|
||||
import type {
|
||||
MonitoringMachineInput,
|
||||
ResetLocalDatabaseInput,
|
||||
SavedTaskInput,
|
||||
SSHKeyInput,
|
||||
SSHValidationResult,
|
||||
} from "../types";
|
||||
|
||||
export function useMonitoringSettings() {
|
||||
@@ -134,6 +136,12 @@ export function useSaveMonitoringMachine() {
|
||||
});
|
||||
}
|
||||
|
||||
export function useTestMonitoringMachineSSH() {
|
||||
return useMutation<SSHValidationResult, Error, MonitoringMachineInput>({
|
||||
mutationFn: testMonitoringMachineSSH,
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteMonitoringMachine() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
useSSHKeys,
|
||||
useSaveMonitoringMachine,
|
||||
useSaveSSHKey,
|
||||
useTestMonitoringMachineSSH,
|
||||
} from "../hooks/useSettings";
|
||||
import { DialogFooter } from "../components/DialogFooter";
|
||||
import { HoverEditButton } from "../components/HoverEditButton";
|
||||
@@ -76,6 +77,10 @@ function MachineEditor({
|
||||
machine,
|
||||
sshKeys,
|
||||
onChange,
|
||||
onValidateSSH,
|
||||
isValidatingSSH,
|
||||
sshValidationMessage,
|
||||
sshValidationError,
|
||||
}: {
|
||||
title: string;
|
||||
hint?: string;
|
||||
@@ -86,6 +91,10 @@ function MachineEditor({
|
||||
| MonitoringMachineInput
|
||||
| ((current: MonitoringMachineInput) => MonitoringMachineInput),
|
||||
) => void;
|
||||
onValidateSSH: () => void;
|
||||
isValidatingSSH: boolean;
|
||||
sshValidationMessage: string;
|
||||
sshValidationError: string;
|
||||
}) {
|
||||
const draft = machine;
|
||||
const setDraft = onChange;
|
||||
@@ -417,6 +426,35 @@ function MachineEditor({
|
||||
SSH machines usually need monitoring or files enabled.
|
||||
</Alert>
|
||||
)}
|
||||
{!isLocal && (
|
||||
<Stack spacing={1}>
|
||||
<Alert severity="info">
|
||||
Validate SSH before saving: this records the first trusted host key
|
||||
in the backend-managed known_hosts file, then checks SSH auth.
|
||||
</Alert>
|
||||
<Stack direction="row" spacing={1} sx={{ flexWrap: "wrap" }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={onValidateSSH}
|
||||
disabled={
|
||||
isValidatingSSH ||
|
||||
!draft.host.trim() ||
|
||||
!draft.username.trim()
|
||||
}
|
||||
>
|
||||
{isValidatingSSH
|
||||
? "Validating SSH..."
|
||||
: "Validate SSH + trust host"}
|
||||
</Button>
|
||||
</Stack>
|
||||
{sshValidationMessage && (
|
||||
<Alert severity="success">{sshValidationMessage}</Alert>
|
||||
)}
|
||||
{sshValidationError && (
|
||||
<Alert severity="error">{sshValidationError}</Alert>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
{hasJellyseerr && !draft.jellyseerr_url && (
|
||||
<Alert severity="info">
|
||||
Jellyseerr is enabled, but no URL is configured yet.
|
||||
@@ -828,7 +866,10 @@ export function Settings() {
|
||||
const { data: sshKeys = [] } = useSSHKeys();
|
||||
const saveMachine = useSaveMonitoringMachine();
|
||||
const deleteMachine = useDeleteMonitoringMachine();
|
||||
const testMachineSSH = useTestMonitoringMachineSSH();
|
||||
const [tab, setTab] = useState<SettingsTab>("machines");
|
||||
const [sshValidationMessage, setSSHValidationMessage] = useState("");
|
||||
const [sshValidationError, setSSHValidationError] = useState("");
|
||||
const [machineDialogOpen, setMachineDialogOpen] = useState(false);
|
||||
const [machineDraft, setMachineDraft] = useState<MonitoringMachineInput>(
|
||||
emptyMachine(),
|
||||
@@ -848,26 +889,54 @@ export function Settings() {
|
||||
const sshMachines = orderedMachines.filter(
|
||||
(machine) => machine.mode === "ssh",
|
||||
);
|
||||
const clearSSHValidation = () => {
|
||||
setSSHValidationMessage("");
|
||||
setSSHValidationError("");
|
||||
};
|
||||
const beginLocal = () => {
|
||||
clearSSHValidation();
|
||||
setMachineDraft(emptyMachine("local"));
|
||||
setMachineDialogOpen(true);
|
||||
};
|
||||
const beginRemote = () => {
|
||||
clearSSHValidation();
|
||||
setMachineDraft(emptyMachine("ssh"));
|
||||
setMachineDialogOpen(true);
|
||||
};
|
||||
const openEditMachine = (machine: MonitoringMachineInput) => {
|
||||
clearSSHValidation();
|
||||
setMachineDraft(machine);
|
||||
setMachineDialogOpen(true);
|
||||
};
|
||||
const closeMachineDialog = () => {
|
||||
clearSSHValidation();
|
||||
setMachineDialogOpen(false);
|
||||
};
|
||||
const updateMachineDraft = (
|
||||
draft:
|
||||
| MonitoringMachineInput
|
||||
| ((current: MonitoringMachineInput) => MonitoringMachineInput),
|
||||
) => {
|
||||
clearSSHValidation();
|
||||
setMachineDraft(draft);
|
||||
};
|
||||
const saveMachineDraft = async (draft: MonitoringMachineInput) => {
|
||||
clearSSHValidation();
|
||||
await saveMachine.mutateAsync(draft);
|
||||
setMachineDialogOpen(false);
|
||||
setMachineDraft(emptyMachine(draft.mode));
|
||||
};
|
||||
const validateMachineSSH = async () => {
|
||||
clearSSHValidation();
|
||||
try {
|
||||
const result = await testMachineSSH.mutateAsync(machineDraft);
|
||||
setSSHValidationMessage(result.message);
|
||||
} catch (error) {
|
||||
setSSHValidationError(
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Stack spacing={2.25}>
|
||||
<Stack spacing={0.5}>
|
||||
@@ -1209,7 +1278,11 @@ export function Settings() {
|
||||
}
|
||||
machine={machineDraft}
|
||||
sshKeys={sshKeys}
|
||||
onChange={setMachineDraft}
|
||||
onChange={updateMachineDraft}
|
||||
onValidateSSH={validateMachineSSH}
|
||||
isValidatingSSH={testMachineSSH.isPending}
|
||||
sshValidationMessage={sshValidationMessage}
|
||||
sshValidationError={sshValidationError}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogFooter
|
||||
|
||||
@@ -283,6 +283,14 @@ export interface ResetLocalDatabaseResponse {
|
||||
media_index_files: string[];
|
||||
}
|
||||
|
||||
export interface SSHValidationResult {
|
||||
status: string;
|
||||
message: string;
|
||||
host: string;
|
||||
port: number;
|
||||
known_hosts_updated: boolean;
|
||||
}
|
||||
|
||||
export interface MonitoringPollerStatus {
|
||||
worker_running: boolean;
|
||||
stop_requested: boolean;
|
||||
|
||||
Reference in New Issue
Block a user