Implement SSH key deployment feature with credential resolution
- Add SSH key deployment endpoint supporting all authentication types - Implement automatic credential resolution for credential-based hosts - Add deployment UI with host selection and progress tracking - Support password, key, and credential authentication methods - Include deployment verification and error handling - Add public key field to credential types and API responses - Implement secure SSH connection handling with proper timeout 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,21 @@ import {
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "@/components/ui/sheet";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@@ -29,12 +44,17 @@ import {
|
||||
Pencil,
|
||||
X,
|
||||
Check,
|
||||
Upload,
|
||||
Server,
|
||||
User,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
getCredentials,
|
||||
deleteCredential,
|
||||
updateCredential,
|
||||
renameCredentialFolder,
|
||||
deployCredentialToHost,
|
||||
getSSHHosts,
|
||||
} from "@/ui/main-axios";
|
||||
import { toast } from "sonner";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -65,12 +85,27 @@ export function CredentialsManager({
|
||||
const [editingFolder, setEditingFolder] = useState<string | null>(null);
|
||||
const [editingFolderName, setEditingFolderName] = useState("");
|
||||
const [operationLoading, setOperationLoading] = useState(false);
|
||||
const [showDeployDialog, setShowDeployDialog] = useState(false);
|
||||
const [deployingCredential, setDeployingCredential] = useState<Credential | null>(null);
|
||||
const [availableHosts, setAvailableHosts] = useState<any[]>([]);
|
||||
const [selectedHostId, setSelectedHostId] = useState<string>("");
|
||||
const [deployLoading, setDeployLoading] = useState(false);
|
||||
const dragCounter = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
fetchCredentials();
|
||||
fetchHosts();
|
||||
}, []);
|
||||
|
||||
const fetchHosts = async () => {
|
||||
try {
|
||||
const hosts = await getSSHHosts();
|
||||
setAvailableHosts(hosts);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch hosts:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchCredentials = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
@@ -90,6 +125,49 @@ export function CredentialsManager({
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeploy = (credential: Credential) => {
|
||||
if (credential.authType !== 'key') {
|
||||
toast.error("Only SSH key-based credentials can be deployed");
|
||||
return;
|
||||
}
|
||||
if (!credential.publicKey) {
|
||||
toast.error("Public key is required for deployment");
|
||||
return;
|
||||
}
|
||||
setDeployingCredential(credential);
|
||||
setSelectedHostId("");
|
||||
setShowDeployDialog(true);
|
||||
};
|
||||
|
||||
const performDeploy = async () => {
|
||||
if (!deployingCredential || !selectedHostId) {
|
||||
toast.error("Please select a target host");
|
||||
return;
|
||||
}
|
||||
|
||||
setDeployLoading(true);
|
||||
try {
|
||||
const result = await deployCredentialToHost(
|
||||
deployingCredential.id,
|
||||
parseInt(selectedHostId)
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
toast.success(result.message || "SSH key deployed successfully");
|
||||
setShowDeployDialog(false);
|
||||
setDeployingCredential(null);
|
||||
setSelectedHostId("");
|
||||
} else {
|
||||
toast.error(result.error || "Deployment failed");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Deployment error:', error);
|
||||
toast.error("Failed to deploy SSH key");
|
||||
} finally {
|
||||
setDeployLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (credentialId: number, credentialName: string) => {
|
||||
confirmWithToast(
|
||||
t("credentials.confirmDeleteCredential", { name: credentialName }),
|
||||
@@ -577,6 +655,26 @@ export function CredentialsManager({
|
||||
<p>Edit credential</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{credential.authType === 'key' && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDeploy(credential);
|
||||
}}
|
||||
className="h-5 w-5 p-0 text-green-600 hover:text-green-700 hover:bg-green-500/10"
|
||||
>
|
||||
<Upload className="h-3 w-3" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Deploy SSH key to host</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
@@ -687,6 +785,145 @@ export function CredentialsManager({
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Sheet open={showDeployDialog} onOpenChange={setShowDeployDialog}>
|
||||
<SheetContent className="w-[500px] max-w-[50vw] overflow-y-auto">
|
||||
<SheetHeader className="space-y-6 pb-8">
|
||||
<SheetTitle className="flex items-center space-x-4">
|
||||
<div className="p-2 rounded-lg bg-zinc-100 dark:bg-zinc-800">
|
||||
<Upload className="h-5 w-5 text-green-600" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="text-xl font-semibold">Deploy SSH Key</div>
|
||||
<div className="text-sm font-normal text-zinc-600 dark:text-zinc-400 mt-1">
|
||||
Deploy public key to target server
|
||||
</div>
|
||||
</div>
|
||||
</SheetTitle>
|
||||
</SheetHeader>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Credential Information Card */}
|
||||
{deployingCredential && (
|
||||
<div className="border border-zinc-200 dark:border-zinc-700 rounded-lg p-4 bg-zinc-50 dark:bg-zinc-900/50">
|
||||
<h4 className="text-sm font-semibold text-zinc-800 dark:text-zinc-200 mb-3 flex items-center">
|
||||
<Key className="h-4 w-4 mr-2 text-zinc-500" />
|
||||
Source Credential
|
||||
</h4>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="p-1.5 rounded-md bg-zinc-100 dark:bg-zinc-800">
|
||||
<User className="h-3 w-3 text-zinc-500 dark:text-zinc-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-zinc-500 dark:text-zinc-400">Name</div>
|
||||
<div className="text-sm font-medium text-zinc-800 dark:text-zinc-200">
|
||||
{deployingCredential.name || deployingCredential.username}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="p-1.5 rounded-md bg-zinc-100 dark:bg-zinc-800">
|
||||
<User className="h-3 w-3 text-zinc-500 dark:text-zinc-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-zinc-500 dark:text-zinc-400">Username</div>
|
||||
<div className="text-sm font-medium text-zinc-800 dark:text-zinc-200">
|
||||
{deployingCredential.username}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="p-1.5 rounded-md bg-zinc-100 dark:bg-zinc-800">
|
||||
<Key className="h-3 w-3 text-zinc-500 dark:text-zinc-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-zinc-500 dark:text-zinc-400">Key Type</div>
|
||||
<div className="text-sm font-medium text-zinc-800 dark:text-zinc-200">
|
||||
{deployingCredential.keyType || 'SSH Key'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Target Host Selection */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-semibold text-zinc-800 dark:text-zinc-200 flex items-center">
|
||||
<Server className="h-4 w-4 mr-2 text-zinc-500" />
|
||||
Target Host
|
||||
</label>
|
||||
<Select value={selectedHostId} onValueChange={setSelectedHostId}>
|
||||
<SelectTrigger className="h-12 border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-900/50">
|
||||
<SelectValue placeholder="Choose a host to deploy to..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableHosts.map((host) => (
|
||||
<SelectItem key={host.id} value={host.id.toString()}>
|
||||
<div className="flex items-center gap-3 py-1">
|
||||
<div className="p-1.5 rounded-md bg-zinc-100 dark:bg-zinc-800">
|
||||
<Server className="h-3 w-3 text-zinc-500 dark:text-zinc-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium text-zinc-800 dark:text-zinc-200">
|
||||
{host.name || host.ip}
|
||||
</div>
|
||||
<div className="text-xs text-zinc-500 dark:text-zinc-400">
|
||||
{host.username}@{host.ip}:{host.port}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Information Note */}
|
||||
<div className="border border-blue-200 dark:border-blue-800 rounded-lg p-4 bg-blue-50 dark:bg-blue-900/20">
|
||||
<div className="flex items-start space-x-3">
|
||||
<Info className="h-4 w-4 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" />
|
||||
<div className="text-sm text-blue-800 dark:text-blue-200">
|
||||
<p className="font-medium mb-1">Deployment Process</p>
|
||||
<p className="text-blue-700 dark:text-blue-300">
|
||||
This will safely add the public key to the target host's ~/.ssh/authorized_keys file
|
||||
without overwriting existing keys. The operation is reversible.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SheetFooter className="mt-8 flex space-x-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowDeployDialog(false)}
|
||||
disabled={deployLoading}
|
||||
className="flex-1"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={performDeploy}
|
||||
disabled={!selectedHostId || deployLoading}
|
||||
className="flex-1 bg-green-600 hover:bg-green-700 text-white"
|
||||
>
|
||||
{deployLoading ? (
|
||||
<div className="flex items-center">
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-2 border-white border-t-transparent mr-2"></div>
|
||||
Deploying...
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center">
|
||||
<Upload className="h-4 w-4 mr-2" />
|
||||
Deploy SSH Key
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user