feat: added quick connection system (ad-hoc)
This commit is contained in:
@@ -201,6 +201,18 @@ http {
|
|||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location /ssh/quick-connect {
|
||||||
|
proxy_pass http://127.0.0.1:30001;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection 'upgrade';
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
location /ssh/ {
|
location /ssh/ {
|
||||||
proxy_pass http://127.0.0.1:30001;
|
proxy_pass http://127.0.0.1:30001;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
|
|||||||
@@ -190,6 +190,18 @@ http {
|
|||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location /ssh/quick-connect {
|
||||||
|
proxy_pass http://127.0.0.1:30001;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection 'upgrade';
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
location /ssh/ {
|
location /ssh/ {
|
||||||
proxy_pass http://127.0.0.1:30001;
|
proxy_pass http://127.0.0.1:30001;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
|
|||||||
@@ -543,6 +543,195 @@ router.post(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /ssh/quick-connect:
|
||||||
|
* post:
|
||||||
|
* summary: Create a temporary SSH connection without saving to database
|
||||||
|
* description: Returns a temporary host configuration for immediate use
|
||||||
|
* tags:
|
||||||
|
* - SSH
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* required:
|
||||||
|
* - ip
|
||||||
|
* - port
|
||||||
|
* - username
|
||||||
|
* - authType
|
||||||
|
* properties:
|
||||||
|
* ip:
|
||||||
|
* type: string
|
||||||
|
* description: SSH server IP or hostname
|
||||||
|
* port:
|
||||||
|
* type: number
|
||||||
|
* description: SSH server port
|
||||||
|
* username:
|
||||||
|
* type: string
|
||||||
|
* description: SSH username
|
||||||
|
* authType:
|
||||||
|
* type: string
|
||||||
|
* enum: [password, key, credential]
|
||||||
|
* description: Authentication method
|
||||||
|
* password:
|
||||||
|
* type: string
|
||||||
|
* description: Password (required if authType is password)
|
||||||
|
* key:
|
||||||
|
* type: string
|
||||||
|
* description: SSH private key (required if authType is key)
|
||||||
|
* keyPassword:
|
||||||
|
* type: string
|
||||||
|
* description: SSH key password (optional)
|
||||||
|
* keyType:
|
||||||
|
* type: string
|
||||||
|
* description: SSH key type
|
||||||
|
* credentialId:
|
||||||
|
* type: number
|
||||||
|
* description: Credential ID (required if authType is credential)
|
||||||
|
* overrideCredentialUsername:
|
||||||
|
* type: boolean
|
||||||
|
* description: Use provided username instead of credential username
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Temporary host configuration created successfully
|
||||||
|
* 400:
|
||||||
|
* description: Invalid request data
|
||||||
|
* 401:
|
||||||
|
* description: Unauthorized
|
||||||
|
* 403:
|
||||||
|
* description: Forbidden
|
||||||
|
* 404:
|
||||||
|
* description: Credential not found
|
||||||
|
* 500:
|
||||||
|
* description: Server error
|
||||||
|
*/
|
||||||
|
router.post(
|
||||||
|
"/quick-connect",
|
||||||
|
authenticateJWT,
|
||||||
|
requireDataAccess,
|
||||||
|
async (req: AuthenticatedRequest, res: Response) => {
|
||||||
|
const userId = req.userId;
|
||||||
|
const {
|
||||||
|
ip,
|
||||||
|
port,
|
||||||
|
username,
|
||||||
|
authType,
|
||||||
|
password,
|
||||||
|
key,
|
||||||
|
keyPassword,
|
||||||
|
keyType,
|
||||||
|
credentialId,
|
||||||
|
overrideCredentialUsername,
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!isNonEmptyString(ip) ||
|
||||||
|
!isValidPort(port) ||
|
||||||
|
!isNonEmptyString(username) ||
|
||||||
|
!authType
|
||||||
|
) {
|
||||||
|
return res.status(400).json({ error: "Missing required fields" });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let resolvedPassword = password;
|
||||||
|
let resolvedKey = key;
|
||||||
|
let resolvedKeyPassword = keyPassword;
|
||||||
|
let resolvedKeyType = keyType;
|
||||||
|
let resolvedAuthType = authType;
|
||||||
|
let resolvedUsername = username;
|
||||||
|
|
||||||
|
if (authType === "credential" && credentialId) {
|
||||||
|
const credentials = await SimpleDBOps.select(
|
||||||
|
db
|
||||||
|
.select()
|
||||||
|
.from(sshCredentials)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(sshCredentials.id, credentialId),
|
||||||
|
eq(sshCredentials.userId, userId),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
"ssh_credentials",
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!credentials || credentials.length === 0) {
|
||||||
|
return res.status(404).json({ error: "Credential not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const cred = credentials[0];
|
||||||
|
|
||||||
|
resolvedPassword = cred.password as string | undefined;
|
||||||
|
resolvedKey = (cred.private_key || cred.privateKey || cred.key) as
|
||||||
|
| string
|
||||||
|
| undefined;
|
||||||
|
resolvedKeyPassword = (cred.key_password || cred.keyPassword) as
|
||||||
|
| string
|
||||||
|
| undefined;
|
||||||
|
resolvedKeyType = (cred.key_type || cred.keyType) as string | undefined;
|
||||||
|
resolvedAuthType = (cred.auth_type || cred.authType) as
|
||||||
|
| string
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
if (!overrideCredentialUsername) {
|
||||||
|
resolvedUsername = cred.username as string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tempHost: Record<string, unknown> = {
|
||||||
|
id: -Date.now(),
|
||||||
|
userId: userId,
|
||||||
|
name: `${resolvedUsername}@${ip}:${port}`,
|
||||||
|
ip,
|
||||||
|
port: Number(port),
|
||||||
|
username: resolvedUsername,
|
||||||
|
folder: "",
|
||||||
|
tags: [],
|
||||||
|
pin: false,
|
||||||
|
authType: resolvedAuthType || authType,
|
||||||
|
password: resolvedPassword,
|
||||||
|
key: resolvedKey,
|
||||||
|
keyPassword: resolvedKeyPassword,
|
||||||
|
keyType: resolvedKeyType,
|
||||||
|
enableTerminal: true,
|
||||||
|
enableTunnel: false,
|
||||||
|
enableFileManager: true,
|
||||||
|
enableDocker: false,
|
||||||
|
showTerminalInSidebar: true,
|
||||||
|
showFileManagerInSidebar: false,
|
||||||
|
showTunnelInSidebar: false,
|
||||||
|
showDockerInSidebar: false,
|
||||||
|
showServerStatsInSidebar: false,
|
||||||
|
defaultPath: "/",
|
||||||
|
tunnelConnections: [],
|
||||||
|
jumpHosts: [],
|
||||||
|
quickActions: [],
|
||||||
|
statsConfig: {},
|
||||||
|
notes: "",
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
return res.status(200).json(tempHost);
|
||||||
|
} catch (error) {
|
||||||
|
sshLogger.error("Quick connect failed", error, {
|
||||||
|
operation: "quick_connect",
|
||||||
|
userId,
|
||||||
|
ip,
|
||||||
|
port,
|
||||||
|
authType,
|
||||||
|
});
|
||||||
|
return res
|
||||||
|
.status(500)
|
||||||
|
.json({ error: "Failed to create quick connection" });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @openapi
|
* @openapi
|
||||||
* /ssh/db/host/{id}:
|
* /ssh/db/host/{id}:
|
||||||
|
|||||||
@@ -186,6 +186,32 @@
|
|||||||
"renameFolder": "Rename folder",
|
"renameFolder": "Rename folder",
|
||||||
"idLabel": "ID:"
|
"idLabel": "ID:"
|
||||||
},
|
},
|
||||||
|
"quickConnect": {
|
||||||
|
"title": "Quick Connect",
|
||||||
|
"description": "Connect directly to a terminal or file manager session without saving a host configuration",
|
||||||
|
"ipAddress": "IP Address or Hostname",
|
||||||
|
"port": "Port",
|
||||||
|
"username": "Username",
|
||||||
|
"password": "Password",
|
||||||
|
"key": "SSH Private Key",
|
||||||
|
"keyPassword": "Key Password (Optional)",
|
||||||
|
"keyType": "Key Type",
|
||||||
|
"uploadFile": "Upload File",
|
||||||
|
"pasteKey": "Paste Key",
|
||||||
|
"credential": "Credential",
|
||||||
|
"overrideUsername": "Override Credential Username",
|
||||||
|
"overrideUsernameDesc": "Use a different username than the one stored in the credential",
|
||||||
|
"connectTerminal": "Connect to Terminal",
|
||||||
|
"connectFileManager": "Connect to File Manager",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"passwordRequired": "Password is required",
|
||||||
|
"keyRequired": "SSH key is required",
|
||||||
|
"credentialRequired": "Credential selection is required",
|
||||||
|
"connectionEstablished": "Connection established successfully",
|
||||||
|
"connectionFailed": "Failed to establish connection",
|
||||||
|
"autoDetect": "Auto Detect",
|
||||||
|
"authentication": "Authentication"
|
||||||
|
},
|
||||||
"dragIndicator": {
|
"dragIndicator": {
|
||||||
"error": "Error: {{error}}",
|
"error": "Error: {{error}}",
|
||||||
"dragging": "Dragging {{fileName}}",
|
"dragging": "Dragging {{fileName}}",
|
||||||
|
|||||||
664
src/ui/desktop/navigation/QuickConnectDialog.tsx
Normal file
664
src/ui/desktop/navigation/QuickConnectDialog.tsx
Normal file
@@ -0,0 +1,664 @@
|
|||||||
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
|
import { useForm, Controller } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog.tsx";
|
||||||
|
import { Button } from "@/components/ui/button.tsx";
|
||||||
|
import { Input } from "@/components/ui/input.tsx";
|
||||||
|
import { PasswordInput } from "@/components/ui/password-input.tsx";
|
||||||
|
import { Label } from "@/components/ui/label.tsx";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
} from "@/components/ui/form.tsx";
|
||||||
|
import {
|
||||||
|
Tabs,
|
||||||
|
TabsContent,
|
||||||
|
TabsList,
|
||||||
|
TabsTrigger,
|
||||||
|
} from "@/components/ui/tabs.tsx";
|
||||||
|
import { Switch } from "@/components/ui/switch.tsx";
|
||||||
|
import { CredentialSelector } from "@/ui/desktop/apps/host-manager/credentials/CredentialSelector.tsx";
|
||||||
|
import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext.tsx";
|
||||||
|
import { quickConnect, getCredentials } from "@/ui/main-axios.ts";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import CodeMirror from "@uiw/react-codemirror";
|
||||||
|
import { oneDark } from "@codemirror/theme-one-dark";
|
||||||
|
import { githubLight } from "@uiw/codemirror-theme-github";
|
||||||
|
import { EditorView } from "@codemirror/view";
|
||||||
|
import { useTheme } from "@/components/theme-provider.tsx";
|
||||||
|
import type { SSHHost, Credential } from "@/types";
|
||||||
|
|
||||||
|
interface QuickConnectDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyTypeOptions = [
|
||||||
|
{ value: "auto", label: "Auto Detect" },
|
||||||
|
{ value: "ssh-rsa", label: "RSA" },
|
||||||
|
{ value: "ssh-ed25519", label: "Ed25519" },
|
||||||
|
{ value: "ecdsa-sha2-nistp256", label: "ECDSA NIST P-256" },
|
||||||
|
{ value: "ecdsa-sha2-nistp384", label: "ECDSA NIST P-384" },
|
||||||
|
{ value: "ecdsa-sha2-nistp521", label: "ECDSA NIST P-521" },
|
||||||
|
{ value: "ssh-dss", label: "DSA" },
|
||||||
|
{ value: "ssh-rsa-sha2-256", label: "RSA SHA2-256" },
|
||||||
|
{ value: "ssh-rsa-sha2-512", label: "RSA SHA2-512" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function QuickConnectDialog({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
}: QuickConnectDialogProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { theme: appTheme } = useTheme();
|
||||||
|
const { addTab, setCurrentTab } = useTabs();
|
||||||
|
const [authTab, setAuthTab] = useState<"password" | "key" | "credential">(
|
||||||
|
"password",
|
||||||
|
);
|
||||||
|
const [keyInputMethod, setKeyInputMethod] = useState<"upload" | "paste">(
|
||||||
|
"upload",
|
||||||
|
);
|
||||||
|
const [credentials, setCredentials] = useState<Credential[]>([]);
|
||||||
|
const [keyTypeDropdownOpen, setKeyTypeDropdownOpen] = useState(false);
|
||||||
|
const keyTypeButtonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const keyTypeDropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [isConnecting, setIsConnecting] = useState(false);
|
||||||
|
|
||||||
|
const isDarkMode =
|
||||||
|
appTheme === "dark" ||
|
||||||
|
(appTheme === "system" &&
|
||||||
|
window.matchMedia("(prefers-color-scheme: dark)").matches);
|
||||||
|
const editorTheme = isDarkMode ? oneDark : githubLight;
|
||||||
|
|
||||||
|
const formSchema = z
|
||||||
|
.object({
|
||||||
|
ip: z.string().min(1, t("quickConnect.ipAddress")),
|
||||||
|
port: z.coerce.number().min(1).max(65535).default(22),
|
||||||
|
username: z.string().min(1, t("quickConnect.username")),
|
||||||
|
authType: z.enum(["password", "key", "credential"]),
|
||||||
|
password: z.string().optional(),
|
||||||
|
key: z.any().optional(),
|
||||||
|
keyPassword: z.string().optional(),
|
||||||
|
keyType: z
|
||||||
|
.enum([
|
||||||
|
"auto",
|
||||||
|
"ssh-rsa",
|
||||||
|
"ssh-ed25519",
|
||||||
|
"ecdsa-sha2-nistp256",
|
||||||
|
"ecdsa-sha2-nistp384",
|
||||||
|
"ecdsa-sha2-nistp521",
|
||||||
|
"ssh-dss",
|
||||||
|
"ssh-rsa-sha2-256",
|
||||||
|
"ssh-rsa-sha2-512",
|
||||||
|
])
|
||||||
|
.optional(),
|
||||||
|
credentialId: z.number().optional().nullable(),
|
||||||
|
overrideCredentialUsername: z.boolean().optional(),
|
||||||
|
})
|
||||||
|
.superRefine((data, ctx) => {
|
||||||
|
if (data.authType === "password") {
|
||||||
|
if (
|
||||||
|
!data.password ||
|
||||||
|
(typeof data.password === "string" && data.password.trim() === "")
|
||||||
|
) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: t("quickConnect.passwordRequired"),
|
||||||
|
path: ["password"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (data.authType === "key") {
|
||||||
|
if (
|
||||||
|
!data.key ||
|
||||||
|
(typeof data.key === "string" && data.key.trim() === "")
|
||||||
|
) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: t("quickConnect.keyRequired"),
|
||||||
|
path: ["key"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!data.keyType) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: t("hosts.keyTypeRequired"),
|
||||||
|
path: ["keyType"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (data.authType === "credential") {
|
||||||
|
if (!data.credentialId) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: t("quickConnect.credentialRequired"),
|
||||||
|
path: ["credentialId"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormData = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
const form = useForm<FormData>({
|
||||||
|
resolver: zodResolver(formSchema) as any,
|
||||||
|
mode: "all",
|
||||||
|
defaultValues: {
|
||||||
|
ip: "",
|
||||||
|
port: 22,
|
||||||
|
username: "",
|
||||||
|
authType: "password" as const,
|
||||||
|
password: "",
|
||||||
|
key: null,
|
||||||
|
keyPassword: "",
|
||||||
|
keyType: "auto" as const,
|
||||||
|
credentialId: null,
|
||||||
|
overrideCredentialUsername: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchCredentials = async () => {
|
||||||
|
try {
|
||||||
|
const data = await getCredentials();
|
||||||
|
setCredentials((data as Credential[]) || []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch credentials:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (open) {
|
||||||
|
fetchCredentials();
|
||||||
|
}
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
form.setValue("authType", authTab, { shouldValidate: true });
|
||||||
|
|
||||||
|
if (authTab === "password") {
|
||||||
|
form.setValue("key", null, { shouldValidate: true });
|
||||||
|
form.setValue("keyPassword", "", { shouldValidate: true });
|
||||||
|
form.setValue("keyType", "auto", { shouldValidate: true });
|
||||||
|
form.setValue("credentialId", null, { shouldValidate: true });
|
||||||
|
} else if (authTab === "key") {
|
||||||
|
form.setValue("password", "", { shouldValidate: true });
|
||||||
|
form.setValue("credentialId", null, { shouldValidate: true });
|
||||||
|
} else if (authTab === "credential") {
|
||||||
|
form.setValue("password", "", { shouldValidate: true });
|
||||||
|
form.setValue("key", null, { shouldValidate: true });
|
||||||
|
form.setValue("keyPassword", "", { shouldValidate: true });
|
||||||
|
form.setValue("keyType", "auto", { shouldValidate: true });
|
||||||
|
}
|
||||||
|
}, [authTab, form]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function onClickOutside(event: MouseEvent) {
|
||||||
|
if (
|
||||||
|
keyTypeDropdownOpen &&
|
||||||
|
keyTypeDropdownRef.current &&
|
||||||
|
!keyTypeDropdownRef.current.contains(event.target as Node) &&
|
||||||
|
keyTypeButtonRef.current &&
|
||||||
|
!keyTypeButtonRef.current.contains(event.target as Node)
|
||||||
|
) {
|
||||||
|
setKeyTypeDropdownOpen(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("mousedown", onClickOutside);
|
||||||
|
return () => document.removeEventListener("mousedown", onClickOutside);
|
||||||
|
}, [keyTypeDropdownOpen]);
|
||||||
|
|
||||||
|
const handleConnect = async (connectionType: "terminal" | "file_manager") => {
|
||||||
|
const formData = form.getValues();
|
||||||
|
|
||||||
|
const isValid = await form.trigger();
|
||||||
|
if (!isValid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsConnecting(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
let keyContent: string | undefined;
|
||||||
|
if (formData.authType === "key" && formData.key) {
|
||||||
|
if (formData.key instanceof File) {
|
||||||
|
keyContent = await formData.key.text();
|
||||||
|
} else if (typeof formData.key === "string") {
|
||||||
|
keyContent = formData.key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hostConfig = await quickConnect({
|
||||||
|
ip: formData.ip,
|
||||||
|
port: formData.port,
|
||||||
|
username: formData.username,
|
||||||
|
authType: formData.authType,
|
||||||
|
password:
|
||||||
|
formData.authType === "password" ? formData.password : undefined,
|
||||||
|
key: formData.authType === "key" ? keyContent : undefined,
|
||||||
|
keyPassword:
|
||||||
|
formData.authType === "key" ? formData.keyPassword : undefined,
|
||||||
|
keyType: formData.authType === "key" ? formData.keyType : undefined,
|
||||||
|
credentialId:
|
||||||
|
formData.authType === "credential"
|
||||||
|
? formData.credentialId
|
||||||
|
: undefined,
|
||||||
|
overrideCredentialUsername:
|
||||||
|
formData.authType === "credential"
|
||||||
|
? formData.overrideCredentialUsername
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const tabId = addTab({
|
||||||
|
type: connectionType,
|
||||||
|
title: `${formData.username}@${formData.ip}:${formData.port}`,
|
||||||
|
hostConfig: hostConfig as SSHHost,
|
||||||
|
});
|
||||||
|
setCurrentTab(tabId);
|
||||||
|
|
||||||
|
form.reset();
|
||||||
|
setAuthTab("password");
|
||||||
|
setKeyInputMethod("upload");
|
||||||
|
onOpenChange(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Quick connect failed:", error);
|
||||||
|
toast.error(
|
||||||
|
t("quickConnect.connectionFailed") +
|
||||||
|
": " +
|
||||||
|
(error instanceof Error ? error.message : String(error)),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsConnecting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="sm:max-w-[600px] bg-canvas border-2 border-edge max-h-[90vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{t("quickConnect.title")}</DialogTitle>
|
||||||
|
<DialogDescription className="text-muted-foreground">
|
||||||
|
{t("quickConnect.description")}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<Form {...form}>
|
||||||
|
<div className="space-y-4 py-4">
|
||||||
|
<div className="grid grid-cols-12 gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="ip"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="col-span-8">
|
||||||
|
<FormLabel className="text-base font-semibold text-foreground">
|
||||||
|
{t("quickConnect.ipAddress")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder={t("placeholders.ipAddress")}
|
||||||
|
{...field}
|
||||||
|
onBlur={(e) => {
|
||||||
|
field.onChange(e.target.value.trim());
|
||||||
|
field.onBlur();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="port"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="col-span-4">
|
||||||
|
<FormLabel className="text-base font-semibold text-foreground">
|
||||||
|
{t("quickConnect.port")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder={t("placeholders.port")}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="username"
|
||||||
|
render={({ field }) => {
|
||||||
|
const isCredentialAuth = authTab === "credential";
|
||||||
|
const hasCredential = !!form.watch("credentialId");
|
||||||
|
const overrideEnabled = !!form.watch(
|
||||||
|
"overrideCredentialUsername",
|
||||||
|
);
|
||||||
|
const shouldDisable =
|
||||||
|
isCredentialAuth && hasCredential && !overrideEnabled;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel className="text-base font-semibold text-foreground">
|
||||||
|
{t("quickConnect.username")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder={t("placeholders.username")}
|
||||||
|
disabled={shouldDisable}
|
||||||
|
{...field}
|
||||||
|
onBlur={(e) => {
|
||||||
|
field.onChange(e.target.value.trim());
|
||||||
|
field.onBlur();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Label className="text-base font-semibold text-foreground">
|
||||||
|
{t("quickConnect.authentication")}
|
||||||
|
</Label>
|
||||||
|
<Tabs
|
||||||
|
value={authTab}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setAuthTab(value as "password" | "key" | "credential")
|
||||||
|
}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<TabsList className="inline-flex items-center justify-center rounded-md bg-muted p-1 text-muted-foreground">
|
||||||
|
<TabsTrigger value="password">
|
||||||
|
{t("hosts.password")}
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="key">{t("hosts.key")}</TabsTrigger>
|
||||||
|
<TabsTrigger value="credential">
|
||||||
|
{t("quickConnect.credential")}
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="password" className="mt-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="password"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t("quickConnect.password")}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<PasswordInput
|
||||||
|
placeholder={t("placeholders.password")}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="key" className="mt-4">
|
||||||
|
<Tabs
|
||||||
|
value={keyInputMethod}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setKeyInputMethod(value as "upload" | "paste")
|
||||||
|
}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<TabsList className="inline-flex items-center justify-center rounded-md bg-muted p-1 text-muted-foreground">
|
||||||
|
<TabsTrigger value="upload">
|
||||||
|
{t("quickConnect.uploadFile")}
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="paste">
|
||||||
|
{t("quickConnect.pasteKey")}
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="upload" className="mt-4">
|
||||||
|
<Controller
|
||||||
|
control={form.control}
|
||||||
|
name="key"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="mb-4">
|
||||||
|
<FormLabel>{t("quickConnect.key")}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<div className="relative inline-block">
|
||||||
|
<input
|
||||||
|
id="key-upload"
|
||||||
|
type="file"
|
||||||
|
accept=".pem,.key,.txt,.ppk"
|
||||||
|
onChange={(e) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
field.onChange(file || null);
|
||||||
|
}}
|
||||||
|
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
className="justify-start text-left"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="truncate"
|
||||||
|
title={
|
||||||
|
(field.value as File)?.name ||
|
||||||
|
t("hosts.upload")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{field.value
|
||||||
|
? (field.value as File).name
|
||||||
|
: t("hosts.upload")}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="paste" className="mt-4">
|
||||||
|
<Controller
|
||||||
|
control={form.control}
|
||||||
|
name="key"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="mb-4">
|
||||||
|
<FormLabel>{t("quickConnect.key")}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<CodeMirror
|
||||||
|
value={
|
||||||
|
typeof field.value === "string"
|
||||||
|
? field.value
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
onChange={(value) => field.onChange(value)}
|
||||||
|
placeholder={t("placeholders.pastePrivateKey")}
|
||||||
|
theme={editorTheme}
|
||||||
|
className="border border-input rounded-md overflow-hidden"
|
||||||
|
minHeight="120px"
|
||||||
|
basicSetup={{
|
||||||
|
lineNumbers: true,
|
||||||
|
foldGutter: false,
|
||||||
|
dropCursor: false,
|
||||||
|
allowMultipleSelections: false,
|
||||||
|
highlightSelectionMatches: false,
|
||||||
|
}}
|
||||||
|
extensions={[
|
||||||
|
EditorView.theme({
|
||||||
|
".cm-scroller": {
|
||||||
|
overflow: "auto",
|
||||||
|
scrollbarWidth: "thin",
|
||||||
|
scrollbarColor:
|
||||||
|
"var(--scrollbar-thumb) var(--scrollbar-track)",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4 mt-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="keyPassword"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t("quickConnect.keyPassword")}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<PasswordInput
|
||||||
|
placeholder={t("placeholders.keyPassword")}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="keyType"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="relative">
|
||||||
|
<FormLabel>{t("quickConnect.keyType")}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<div className="relative">
|
||||||
|
<Button
|
||||||
|
ref={keyTypeButtonRef}
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
className="w-full justify-start text-left rounded-md px-2 py-2 bg-canvas border border-input text-foreground"
|
||||||
|
onClick={() =>
|
||||||
|
setKeyTypeDropdownOpen((open) => !open)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{keyTypeOptions.find(
|
||||||
|
(opt) => opt.value === field.value,
|
||||||
|
)?.label || t("quickConnect.autoDetect")}
|
||||||
|
</Button>
|
||||||
|
{keyTypeDropdownOpen && (
|
||||||
|
<div
|
||||||
|
ref={keyTypeDropdownRef}
|
||||||
|
className="absolute bottom-full left-0 z-50 mb-1 w-full bg-canvas border border-input rounded-md shadow-lg max-h-40 overflow-y-auto thin-scrollbar p-1"
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-1 gap-1 p-0">
|
||||||
|
{keyTypeOptions.map((opt) => (
|
||||||
|
<Button
|
||||||
|
key={opt.value}
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="w-full justify-start text-left rounded-md px-2 py-1.5 bg-canvas text-foreground hover:bg-surface-hover focus:bg-surface-hover focus:outline-none"
|
||||||
|
onClick={() => {
|
||||||
|
field.onChange(opt.value);
|
||||||
|
setKeyTypeDropdownOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{opt.label}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="credential" className="mt-4">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="credentialId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<CredentialSelector
|
||||||
|
value={field.value}
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
onCredentialSelect={(credential) => {
|
||||||
|
if (
|
||||||
|
credential &&
|
||||||
|
!form.getValues("overrideCredentialUsername")
|
||||||
|
) {
|
||||||
|
form.setValue("username", credential.username);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{form.watch("credentialId") && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="overrideCredentialUsername"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 bg-elevated dark:bg-input/30">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>
|
||||||
|
{t("quickConnect.overrideUsername")}
|
||||||
|
</FormLabel>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{t("quickConnect.overrideUsernameDesc")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
disabled={isConnecting}
|
||||||
|
>
|
||||||
|
{t("common.cancel")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => handleConnect("terminal")}
|
||||||
|
disabled={isConnecting}
|
||||||
|
>
|
||||||
|
{t("quickConnect.connectTerminal")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => handleConnect("file_manager")}
|
||||||
|
disabled={isConnecting}
|
||||||
|
>
|
||||||
|
{t("quickConnect.connectFileManager")}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,13 +2,14 @@ import React, { useState } from "react";
|
|||||||
import { flushSync } from "react-dom";
|
import { flushSync } from "react-dom";
|
||||||
import { useSidebar } from "@/components/ui/sidebar.tsx";
|
import { useSidebar } from "@/components/ui/sidebar.tsx";
|
||||||
import { Button } from "@/components/ui/button.tsx";
|
import { Button } from "@/components/ui/button.tsx";
|
||||||
import { ChevronDown, ChevronUpIcon, Hammer } from "lucide-react";
|
import { ChevronDown, ChevronUpIcon, Hammer, Zap } from "lucide-react";
|
||||||
import { Tab } from "@/ui/desktop/navigation/tabs/Tab.tsx";
|
import { Tab } from "@/ui/desktop/navigation/tabs/Tab.tsx";
|
||||||
import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext.tsx";
|
import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext.tsx";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { TabDropdown } from "@/ui/desktop/navigation/tabs/TabDropdown.tsx";
|
import { TabDropdown } from "@/ui/desktop/navigation/tabs/TabDropdown.tsx";
|
||||||
import { SSHToolsSidebar } from "@/ui/desktop/apps/tools/SSHToolsSidebar.tsx";
|
import { SSHToolsSidebar } from "@/ui/desktop/apps/tools/SSHToolsSidebar.tsx";
|
||||||
import { useCommandHistory } from "@/ui/desktop/apps/features/terminal/command-history/CommandHistoryContext.tsx";
|
import { useCommandHistory } from "@/ui/desktop/apps/features/terminal/command-history/CommandHistoryContext.tsx";
|
||||||
|
import { QuickConnectDialog } from "@/ui/desktop/navigation/QuickConnectDialog.tsx";
|
||||||
|
|
||||||
interface TabData {
|
interface TabData {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -61,6 +62,7 @@ export function TopNavbar({
|
|||||||
const [toolsSidebarOpen, setToolsSidebarOpen] = useState(false);
|
const [toolsSidebarOpen, setToolsSidebarOpen] = useState(false);
|
||||||
const [commandHistoryTabActive, setCommandHistoryTabActive] = useState(false);
|
const [commandHistoryTabActive, setCommandHistoryTabActive] = useState(false);
|
||||||
const [splitScreenTabActive, setSplitScreenTabActive] = useState(false);
|
const [splitScreenTabActive, setSplitScreenTabActive] = useState(false);
|
||||||
|
const [quickConnectOpen, setQuickConnectOpen] = useState(false);
|
||||||
const [rightSidebarWidth, setRightSidebarWidth] = useState<number>(() => {
|
const [rightSidebarWidth, setRightSidebarWidth] = useState<number>(() => {
|
||||||
const saved = localStorage.getItem("rightSidebarWidth");
|
const saved = localStorage.getItem("rightSidebarWidth");
|
||||||
const defaultWidth = 400;
|
const defaultWidth = 400;
|
||||||
@@ -536,6 +538,15 @@ export function TopNavbar({
|
|||||||
<Hammer className="h-4 w-4" />
|
<Hammer className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setQuickConnectOpen(true)}
|
||||||
|
className="w-[30px] h-[30px] border-edge"
|
||||||
|
title={t("quickConnect.title")}
|
||||||
|
>
|
||||||
|
<Zap className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setIsTopbarOpen(false)}
|
onClick={() => setIsTopbarOpen(false)}
|
||||||
@@ -582,6 +593,11 @@ export function TopNavbar({
|
|||||||
setSplitScreenTabActive(false);
|
setSplitScreenTabActive(false);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<QuickConnectDialog
|
||||||
|
open={quickConnectOpen}
|
||||||
|
onOpenChange={setQuickConnectOpen}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1439,6 +1439,83 @@ export async function verifySSHTOTP(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /ssh/quick-connect:
|
||||||
|
* post:
|
||||||
|
* summary: Create a temporary SSH connection without saving to database
|
||||||
|
* description: Returns a temporary host configuration for immediate use
|
||||||
|
* tags:
|
||||||
|
* - SSH
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* required:
|
||||||
|
* - ip
|
||||||
|
* - port
|
||||||
|
* - username
|
||||||
|
* - authType
|
||||||
|
* properties:
|
||||||
|
* ip:
|
||||||
|
* type: string
|
||||||
|
* description: SSH server IP or hostname
|
||||||
|
* port:
|
||||||
|
* type: number
|
||||||
|
* description: SSH server port
|
||||||
|
* username:
|
||||||
|
* type: string
|
||||||
|
* description: SSH username
|
||||||
|
* authType:
|
||||||
|
* type: string
|
||||||
|
* enum: [password, key, credential]
|
||||||
|
* description: Authentication method
|
||||||
|
* password:
|
||||||
|
* type: string
|
||||||
|
* description: Password (required if authType is password)
|
||||||
|
* key:
|
||||||
|
* type: string
|
||||||
|
* description: SSH private key (required if authType is key)
|
||||||
|
* keyPassword:
|
||||||
|
* type: string
|
||||||
|
* description: SSH key password (optional)
|
||||||
|
* keyType:
|
||||||
|
* type: string
|
||||||
|
* description: SSH key type
|
||||||
|
* credentialId:
|
||||||
|
* type: number
|
||||||
|
* description: Credential ID (required if authType is credential)
|
||||||
|
* overrideCredentialUsername:
|
||||||
|
* type: boolean
|
||||||
|
* description: Use provided username instead of credential username
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Temporary host configuration created successfully
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* description: SSHHost object
|
||||||
|
* 400:
|
||||||
|
* description: Invalid request data
|
||||||
|
* 401:
|
||||||
|
* description: Unauthorized
|
||||||
|
* 500:
|
||||||
|
* description: Server error
|
||||||
|
*/
|
||||||
|
export async function quickConnect(
|
||||||
|
data: Record<string, unknown>,
|
||||||
|
): Promise<SSHHost> {
|
||||||
|
try {
|
||||||
|
const response = await authApi.post("/ssh/quick-connect", data);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
throw handleApiError(error, "quick connect");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function getSSHStatus(
|
export async function getSSHStatus(
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
): Promise<{ connected: boolean }> {
|
): Promise<{ connected: boolean }> {
|
||||||
|
|||||||
Reference in New Issue
Block a user