Cleanup files and improve file manager.
This commit is contained in:
@@ -50,11 +50,13 @@ export function CredentialEditor({
|
||||
const [keyDetectionLoading, setKeyDetectionLoading] = useState(false);
|
||||
const keyDetectionTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const [detectedPublicKeyType, setDetectedPublicKeyType] = useState<string | null>(null);
|
||||
const [publicKeyDetectionLoading, setPublicKeyDetectionLoading] = useState(false);
|
||||
const [detectedPublicKeyType, setDetectedPublicKeyType] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [publicKeyDetectionLoading, setPublicKeyDetectionLoading] =
|
||||
useState(false);
|
||||
const publicKeyDetectionTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
@@ -230,8 +232,11 @@ export function CredentialEditor({
|
||||
}, []);
|
||||
|
||||
// Detect key type function
|
||||
const handleKeyTypeDetection = async (keyValue: string, keyPassword?: string) => {
|
||||
if (!keyValue || keyValue.trim() === '') {
|
||||
const handleKeyTypeDetection = async (
|
||||
keyValue: string,
|
||||
keyPassword?: string,
|
||||
) => {
|
||||
if (!keyValue || keyValue.trim() === "") {
|
||||
setDetectedKeyType(null);
|
||||
return;
|
||||
}
|
||||
@@ -242,12 +247,12 @@ export function CredentialEditor({
|
||||
if (result.success) {
|
||||
setDetectedKeyType(result.keyType);
|
||||
} else {
|
||||
setDetectedKeyType('invalid');
|
||||
console.warn('Key detection failed:', result.error);
|
||||
setDetectedKeyType("invalid");
|
||||
console.warn("Key detection failed:", result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
setDetectedKeyType('error');
|
||||
console.error('Key type detection error:', error);
|
||||
setDetectedKeyType("error");
|
||||
console.error("Key type detection error:", error);
|
||||
} finally {
|
||||
setKeyDetectionLoading(false);
|
||||
}
|
||||
@@ -265,7 +270,7 @@ export function CredentialEditor({
|
||||
|
||||
// Detect public key type function
|
||||
const handlePublicKeyTypeDetection = async (publicKeyValue: string) => {
|
||||
if (!publicKeyValue || publicKeyValue.trim() === '') {
|
||||
if (!publicKeyValue || publicKeyValue.trim() === "") {
|
||||
setDetectedPublicKeyType(null);
|
||||
return;
|
||||
}
|
||||
@@ -276,12 +281,12 @@ export function CredentialEditor({
|
||||
if (result.success) {
|
||||
setDetectedPublicKeyType(result.keyType);
|
||||
} else {
|
||||
setDetectedPublicKeyType('invalid');
|
||||
console.warn('Public key detection failed:', result.error);
|
||||
setDetectedPublicKeyType("invalid");
|
||||
console.warn("Public key detection failed:", result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
setDetectedPublicKeyType('error');
|
||||
console.error('Public key type detection error:', error);
|
||||
setDetectedPublicKeyType("error");
|
||||
console.error("Public key type detection error:", error);
|
||||
} finally {
|
||||
setPublicKeyDetectionLoading(false);
|
||||
}
|
||||
@@ -297,20 +302,19 @@ export function CredentialEditor({
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
|
||||
const getFriendlyKeyTypeName = (keyType: string): string => {
|
||||
const keyTypeMap: Record<string, string> = {
|
||||
'ssh-rsa': 'RSA (SSH)',
|
||||
'ssh-ed25519': 'Ed25519 (SSH)',
|
||||
'ecdsa-sha2-nistp256': 'ECDSA P-256 (SSH)',
|
||||
'ecdsa-sha2-nistp384': 'ECDSA P-384 (SSH)',
|
||||
'ecdsa-sha2-nistp521': 'ECDSA P-521 (SSH)',
|
||||
'ssh-dss': 'DSA (SSH)',
|
||||
'rsa-sha2-256': 'RSA-SHA2-256',
|
||||
'rsa-sha2-512': 'RSA-SHA2-512',
|
||||
'invalid': 'Invalid Key',
|
||||
'error': 'Detection Error',
|
||||
'unknown': 'Unknown'
|
||||
"ssh-rsa": "RSA (SSH)",
|
||||
"ssh-ed25519": "Ed25519 (SSH)",
|
||||
"ecdsa-sha2-nistp256": "ECDSA P-256 (SSH)",
|
||||
"ecdsa-sha2-nistp384": "ECDSA P-384 (SSH)",
|
||||
"ecdsa-sha2-nistp521": "ECDSA P-521 (SSH)",
|
||||
"ssh-dss": "DSA (SSH)",
|
||||
"rsa-sha2-256": "RSA-SHA2-256",
|
||||
"rsa-sha2-512": "RSA-SHA2-512",
|
||||
invalid: "Invalid Key",
|
||||
error: "Detection Error",
|
||||
unknown: "Unknown",
|
||||
};
|
||||
return keyTypeMap[keyType] || keyType;
|
||||
};
|
||||
@@ -418,7 +422,6 @@ export function CredentialEditor({
|
||||
};
|
||||
}, [folderDropdownOpen]);
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex-1 flex flex-col h-full min-h-0 w-full"
|
||||
@@ -680,12 +683,15 @@ export function CredentialEditor({
|
||||
{/* Key Generation Passphrase Input */}
|
||||
<div className="mb-3">
|
||||
<FormLabel className="text-sm mb-2 block">
|
||||
{t("credentials.keyPassword")} ({t("credentials.optional")})
|
||||
{t("credentials.keyPassword")} (
|
||||
{t("credentials.optional")})
|
||||
</FormLabel>
|
||||
<PasswordInput
|
||||
placeholder={t("placeholders.keyPassword")}
|
||||
value={keyGenerationPassphrase}
|
||||
onChange={(e) => setKeyGenerationPassphrase(e.target.value)}
|
||||
onChange={(e) =>
|
||||
setKeyGenerationPassphrase(e.target.value)
|
||||
}
|
||||
className="max-w-xs"
|
||||
/>
|
||||
<div className="text-xs text-muted-foreground mt-1">
|
||||
@@ -700,24 +706,47 @@ export function CredentialEditor({
|
||||
size="sm"
|
||||
onClick={async () => {
|
||||
try {
|
||||
const result = await generateKeyPair('ssh-ed25519', undefined, keyGenerationPassphrase);
|
||||
const result = await generateKeyPair(
|
||||
"ssh-ed25519",
|
||||
undefined,
|
||||
keyGenerationPassphrase,
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
form.setValue("key", result.privateKey);
|
||||
form.setValue("publicKey", result.publicKey);
|
||||
// Auto-fill the key password field if passphrase was used
|
||||
if (keyGenerationPassphrase) {
|
||||
form.setValue("keyPassword", keyGenerationPassphrase);
|
||||
form.setValue(
|
||||
"keyPassword",
|
||||
keyGenerationPassphrase,
|
||||
);
|
||||
}
|
||||
debouncedKeyDetection(result.privateKey, keyGenerationPassphrase);
|
||||
debouncedKeyDetection(
|
||||
result.privateKey,
|
||||
keyGenerationPassphrase,
|
||||
);
|
||||
debouncedPublicKeyDetection(result.publicKey);
|
||||
toast.success(t("credentials.keyPairGeneratedSuccessfully", { keyType: "Ed25519" }));
|
||||
toast.success(
|
||||
t(
|
||||
"credentials.keyPairGeneratedSuccessfully",
|
||||
{ keyType: "Ed25519" },
|
||||
),
|
||||
);
|
||||
} else {
|
||||
toast.error(result.error || t("credentials.failedToGenerateKeyPair"));
|
||||
toast.error(
|
||||
result.error ||
|
||||
t("credentials.failedToGenerateKeyPair"),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to generate Ed25519 key pair:', error);
|
||||
toast.error(t("credentials.failedToGenerateKeyPair"));
|
||||
console.error(
|
||||
"Failed to generate Ed25519 key pair:",
|
||||
error,
|
||||
);
|
||||
toast.error(
|
||||
t("credentials.failedToGenerateKeyPair"),
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -729,24 +758,47 @@ export function CredentialEditor({
|
||||
size="sm"
|
||||
onClick={async () => {
|
||||
try {
|
||||
const result = await generateKeyPair('ecdsa-sha2-nistp256', undefined, keyGenerationPassphrase);
|
||||
const result = await generateKeyPair(
|
||||
"ecdsa-sha2-nistp256",
|
||||
undefined,
|
||||
keyGenerationPassphrase,
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
form.setValue("key", result.privateKey);
|
||||
form.setValue("publicKey", result.publicKey);
|
||||
// Auto-fill the key password field if passphrase was used
|
||||
if (keyGenerationPassphrase) {
|
||||
form.setValue("keyPassword", keyGenerationPassphrase);
|
||||
form.setValue(
|
||||
"keyPassword",
|
||||
keyGenerationPassphrase,
|
||||
);
|
||||
}
|
||||
debouncedKeyDetection(result.privateKey, keyGenerationPassphrase);
|
||||
debouncedKeyDetection(
|
||||
result.privateKey,
|
||||
keyGenerationPassphrase,
|
||||
);
|
||||
debouncedPublicKeyDetection(result.publicKey);
|
||||
toast.success(t("credentials.keyPairGeneratedSuccessfully", { keyType: "ECDSA" }));
|
||||
toast.success(
|
||||
t(
|
||||
"credentials.keyPairGeneratedSuccessfully",
|
||||
{ keyType: "ECDSA" },
|
||||
),
|
||||
);
|
||||
} else {
|
||||
toast.error(result.error || t("credentials.failedToGenerateKeyPair"));
|
||||
toast.error(
|
||||
result.error ||
|
||||
t("credentials.failedToGenerateKeyPair"),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to generate ECDSA key pair:', error);
|
||||
toast.error(t("credentials.failedToGenerateKeyPair"));
|
||||
console.error(
|
||||
"Failed to generate ECDSA key pair:",
|
||||
error,
|
||||
);
|
||||
toast.error(
|
||||
t("credentials.failedToGenerateKeyPair"),
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -758,24 +810,47 @@ export function CredentialEditor({
|
||||
size="sm"
|
||||
onClick={async () => {
|
||||
try {
|
||||
const result = await generateKeyPair('ssh-rsa', 2048, keyGenerationPassphrase);
|
||||
const result = await generateKeyPair(
|
||||
"ssh-rsa",
|
||||
2048,
|
||||
keyGenerationPassphrase,
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
form.setValue("key", result.privateKey);
|
||||
form.setValue("publicKey", result.publicKey);
|
||||
// Auto-fill the key password field if passphrase was used
|
||||
if (keyGenerationPassphrase) {
|
||||
form.setValue("keyPassword", keyGenerationPassphrase);
|
||||
form.setValue(
|
||||
"keyPassword",
|
||||
keyGenerationPassphrase,
|
||||
);
|
||||
}
|
||||
debouncedKeyDetection(result.privateKey, keyGenerationPassphrase);
|
||||
debouncedKeyDetection(
|
||||
result.privateKey,
|
||||
keyGenerationPassphrase,
|
||||
);
|
||||
debouncedPublicKeyDetection(result.publicKey);
|
||||
toast.success(t("credentials.keyPairGeneratedSuccessfully", { keyType: "RSA" }));
|
||||
toast.success(
|
||||
t(
|
||||
"credentials.keyPairGeneratedSuccessfully",
|
||||
{ keyType: "RSA" },
|
||||
),
|
||||
);
|
||||
} else {
|
||||
toast.error(result.error || t("credentials.failedToGenerateKeyPair"));
|
||||
toast.error(
|
||||
result.error ||
|
||||
t("credentials.failedToGenerateKeyPair"),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to generate RSA key pair:', error);
|
||||
toast.error(t("credentials.failedToGenerateKeyPair"));
|
||||
console.error(
|
||||
"Failed to generate RSA key pair:",
|
||||
error,
|
||||
);
|
||||
toast.error(
|
||||
t("credentials.failedToGenerateKeyPair"),
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -786,207 +861,267 @@ export function CredentialEditor({
|
||||
{t("credentials.generateKeyPairNote")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4 items-start">
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="key"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mb-4 flex flex-col">
|
||||
<FormLabel className="mb-2 min-h-[20px]">
|
||||
{t("credentials.sshPrivateKey")}
|
||||
</FormLabel>
|
||||
<div className="mb-2">
|
||||
<div className="relative inline-block w-full">
|
||||
<input
|
||||
id="key-upload"
|
||||
type="file"
|
||||
accept="*,.pem,.key,.txt,.ppk"
|
||||
onChange={async (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
try {
|
||||
const fileContent = await file.text();
|
||||
field.onChange(fileContent);
|
||||
debouncedKeyDetection(fileContent, form.watch("keyPassword"));
|
||||
} catch (error) {
|
||||
console.error('Failed to read uploaded file:', error);
|
||||
}
|
||||
<div className="grid grid-cols-2 gap-4 items-start">
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="key"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mb-4 flex flex-col">
|
||||
<FormLabel className="mb-2 min-h-[20px]">
|
||||
{t("credentials.sshPrivateKey")}
|
||||
</FormLabel>
|
||||
<div className="mb-2">
|
||||
<div className="relative inline-block w-full">
|
||||
<input
|
||||
id="key-upload"
|
||||
type="file"
|
||||
accept="*,.pem,.key,.txt,.ppk"
|
||||
onChange={async (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
try {
|
||||
const fileContent = await file.text();
|
||||
field.onChange(fileContent);
|
||||
debouncedKeyDetection(
|
||||
fileContent,
|
||||
form.watch("keyPassword"),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"Failed to read uploaded file:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
}}
|
||||
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="w-full justify-start text-left"
|
||||
>
|
||||
<span className="truncate">
|
||||
{t("credentials.uploadPrivateKeyFile")}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<FormControl>
|
||||
<textarea
|
||||
placeholder={t(
|
||||
"placeholders.pastePrivateKey",
|
||||
)}
|
||||
className="flex min-h-[120px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
value={
|
||||
typeof field.value === "string"
|
||||
? field.value
|
||||
: ""
|
||||
}
|
||||
onChange={(e) => {
|
||||
field.onChange(e.target.value);
|
||||
debouncedKeyDetection(e.target.value, form.watch("keyPassword"));
|
||||
}
|
||||
}}
|
||||
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
|
||||
/>
|
||||
</FormControl>
|
||||
{detectedKeyType && (
|
||||
<div className="text-sm mt-2">
|
||||
<span className="text-muted-foreground">{t("credentials.detectedKeyType")}: </span>
|
||||
<span className={`font-medium ${
|
||||
detectedKeyType === 'invalid' || detectedKeyType === 'error'
|
||||
? 'text-destructive'
|
||||
: 'text-green-600'
|
||||
}`}>
|
||||
{getFriendlyKeyTypeName(detectedKeyType)}
|
||||
</span>
|
||||
{keyDetectionLoading && (
|
||||
<span className="ml-2 text-muted-foreground">({t("credentials.detectingKeyType")})</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="publicKey"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mb-4 flex flex-col">
|
||||
<FormLabel className="mb-2 min-h-[20px]">
|
||||
{t("credentials.sshPublicKey")} ({t("credentials.optional")})
|
||||
</FormLabel>
|
||||
<div className="mb-2 flex gap-2">
|
||||
<div className="relative inline-block flex-1">
|
||||
<input
|
||||
id="public-key-upload"
|
||||
type="file"
|
||||
accept="*,.pub,.txt"
|
||||
onChange={async (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
try {
|
||||
const fileContent = await file.text();
|
||||
field.onChange(fileContent);
|
||||
debouncedPublicKeyDetection(fileContent);
|
||||
} catch (error) {
|
||||
console.error('Failed to read uploaded public key file:', error);
|
||||
}
|
||||
}
|
||||
}}
|
||||
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="w-full justify-start text-left"
|
||||
>
|
||||
<span className="truncate">
|
||||
{t("credentials.uploadPublicKeyFile")}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="flex-shrink-0"
|
||||
onClick={async () => {
|
||||
const privateKey = form.watch("key");
|
||||
if (!privateKey || typeof privateKey !== "string" || !privateKey.trim()) {
|
||||
toast.error(t("credentials.privateKeyRequiredForGeneration"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const keyPassword = form.watch("keyPassword");
|
||||
const result = await generatePublicKeyFromPrivate(privateKey, keyPassword);
|
||||
|
||||
if (result.success && result.publicKey) {
|
||||
// Set the generated public key
|
||||
field.onChange(result.publicKey);
|
||||
// Trigger public key detection
|
||||
debouncedPublicKeyDetection(result.publicKey);
|
||||
|
||||
toast.success(t("credentials.publicKeyGeneratedSuccessfully"));
|
||||
} else {
|
||||
toast.error(result.error || t("credentials.failedToGeneratePublicKey"));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to generate public key:', error);
|
||||
toast.error(t("credentials.failedToGeneratePublicKey"));
|
||||
}
|
||||
}}
|
||||
className="w-full justify-start text-left"
|
||||
>
|
||||
{t("credentials.generatePublicKey")}
|
||||
<span className="truncate">
|
||||
{t("credentials.uploadPrivateKeyFile")}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
<FormControl>
|
||||
<textarea
|
||||
placeholder={t(
|
||||
"placeholders.pastePublicKey",
|
||||
)}
|
||||
className="flex min-h-[120px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
value={field.value || ""}
|
||||
onChange={(e) => {
|
||||
field.onChange(e.target.value);
|
||||
debouncedPublicKeyDetection(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="text-xs text-muted-foreground mt-1">
|
||||
{t("credentials.publicKeyNote")}
|
||||
</div>
|
||||
{detectedPublicKeyType && field.value && (
|
||||
<div className="text-sm mt-2">
|
||||
<span className="text-muted-foreground">{t("credentials.detectedKeyType")}: </span>
|
||||
<span className={`font-medium ${
|
||||
detectedPublicKeyType === 'invalid' || detectedPublicKeyType === 'error'
|
||||
? 'text-destructive'
|
||||
: 'text-green-600'
|
||||
}`}>
|
||||
{getFriendlyKeyTypeName(detectedPublicKeyType)}
|
||||
</div>
|
||||
<FormControl>
|
||||
<textarea
|
||||
placeholder={t(
|
||||
"placeholders.pastePrivateKey",
|
||||
)}
|
||||
className="flex min-h-[120px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
value={
|
||||
typeof field.value === "string"
|
||||
? field.value
|
||||
: ""
|
||||
}
|
||||
onChange={(e) => {
|
||||
field.onChange(e.target.value);
|
||||
debouncedKeyDetection(
|
||||
e.target.value,
|
||||
form.watch("keyPassword"),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
{detectedKeyType && (
|
||||
<div className="text-sm mt-2">
|
||||
<span className="text-muted-foreground">
|
||||
{t("credentials.detectedKeyType")}:{" "}
|
||||
</span>
|
||||
<span
|
||||
className={`font-medium ${
|
||||
detectedKeyType === "invalid" ||
|
||||
detectedKeyType === "error"
|
||||
? "text-destructive"
|
||||
: "text-green-600"
|
||||
}`}
|
||||
>
|
||||
{getFriendlyKeyTypeName(detectedKeyType)}
|
||||
</span>
|
||||
{keyDetectionLoading && (
|
||||
<span className="ml-2 text-muted-foreground">
|
||||
({t("credentials.detectingKeyType")})
|
||||
</span>
|
||||
{publicKeyDetectionLoading && (
|
||||
<span className="ml-2 text-muted-foreground">({t("credentials.detectingKeyType")})</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-8 gap-4 mt-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="keyPassword"
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-8">
|
||||
<FormLabel>
|
||||
{t("credentials.keyPassword")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<PasswordInput
|
||||
placeholder={t("placeholders.keyPassword")}
|
||||
{...field}
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="publicKey"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mb-4 flex flex-col">
|
||||
<FormLabel className="mb-2 min-h-[20px]">
|
||||
{t("credentials.sshPublicKey")} (
|
||||
{t("credentials.optional")})
|
||||
</FormLabel>
|
||||
<div className="mb-2 flex gap-2">
|
||||
<div className="relative inline-block flex-1">
|
||||
<input
|
||||
id="public-key-upload"
|
||||
type="file"
|
||||
accept="*,.pub,.txt"
|
||||
onChange={async (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
try {
|
||||
const fileContent = await file.text();
|
||||
field.onChange(fileContent);
|
||||
debouncedPublicKeyDetection(
|
||||
fileContent,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"Failed to read uploaded public key file:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
}}
|
||||
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="w-full justify-start text-left"
|
||||
>
|
||||
<span className="truncate">
|
||||
{t("credentials.uploadPublicKeyFile")}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="flex-shrink-0"
|
||||
onClick={async () => {
|
||||
const privateKey = form.watch("key");
|
||||
if (
|
||||
!privateKey ||
|
||||
typeof privateKey !== "string" ||
|
||||
!privateKey.trim()
|
||||
) {
|
||||
toast.error(
|
||||
t(
|
||||
"credentials.privateKeyRequiredForGeneration",
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const keyPassword =
|
||||
form.watch("keyPassword");
|
||||
const result =
|
||||
await generatePublicKeyFromPrivate(
|
||||
privateKey,
|
||||
keyPassword,
|
||||
);
|
||||
|
||||
if (result.success && result.publicKey) {
|
||||
// Set the generated public key
|
||||
field.onChange(result.publicKey);
|
||||
// Trigger public key detection
|
||||
debouncedPublicKeyDetection(
|
||||
result.publicKey,
|
||||
);
|
||||
|
||||
toast.success(
|
||||
t(
|
||||
"credentials.publicKeyGeneratedSuccessfully",
|
||||
),
|
||||
);
|
||||
} else {
|
||||
toast.error(
|
||||
result.error ||
|
||||
t(
|
||||
"credentials.failedToGeneratePublicKey",
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"Failed to generate public key:",
|
||||
error,
|
||||
);
|
||||
toast.error(
|
||||
t(
|
||||
"credentials.failedToGeneratePublicKey",
|
||||
),
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("credentials.generatePublicKey")}
|
||||
</Button>
|
||||
</div>
|
||||
<FormControl>
|
||||
<textarea
|
||||
placeholder={t("placeholders.pastePublicKey")}
|
||||
className="flex min-h-[120px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
value={field.value || ""}
|
||||
onChange={(e) => {
|
||||
field.onChange(e.target.value);
|
||||
debouncedPublicKeyDetection(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="text-xs text-muted-foreground mt-1">
|
||||
{t("credentials.publicKeyNote")}
|
||||
</div>
|
||||
{detectedPublicKeyType && field.value && (
|
||||
<div className="text-sm mt-2">
|
||||
<span className="text-muted-foreground">
|
||||
{t("credentials.detectedKeyType")}:{" "}
|
||||
</span>
|
||||
<span
|
||||
className={`font-medium ${
|
||||
detectedPublicKeyType === "invalid" ||
|
||||
detectedPublicKeyType === "error"
|
||||
? "text-destructive"
|
||||
: "text-green-600"
|
||||
}`}
|
||||
>
|
||||
{getFriendlyKeyTypeName(
|
||||
detectedPublicKeyType,
|
||||
)}
|
||||
</span>
|
||||
{publicKeyDetectionLoading && (
|
||||
<span className="ml-2 text-muted-foreground">
|
||||
({t("credentials.detectingKeyType")})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-8 gap-4 mt-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="keyPassword"
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-8">
|
||||
<FormLabel>
|
||||
{t("credentials.keyPassword")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<PasswordInput
|
||||
placeholder={t("placeholders.keyPassword")}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
Reference in New Issue
Block a user