Add SSH key generation and deployment features (#234)

* Fix SSH key upload and credential editing issues

Fixed two major credential management issues:

1. Fix SSH key upload button not responding (Issue #232)
   - Error handling was silently swallowing exceptions
   - Added proper error propagation in axios functions
   - Improved error display to show specific error messages
   - Users now see actual error details instead of generic messages

2. Improve credential editing to show actual content
   - Both "Upload File" and "Paste Key" modes now display existing data
   - Upload mode: shows current key content in read-only preview area
   - Paste mode: shows editable key content in textarea
   - Smart input method switching preserves existing data
   - Enhanced button labels and status indicators

Key changes:
- Fixed handleApiError propagation in main-axios.ts credential functions
- Enhanced CredentialEditor.tsx with key content preview
- Improved error handling with console logging for debugging
- Better UX with clear status indicators and preserved data

These fixes resolve the "Add Credential button does nothing" issue
and provide full visibility of credential content during editing.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Add comprehensive SSH key management and validation features

- Add support for both private and public key storage
- Implement automatic SSH key type detection for all major formats (RSA, Ed25519, ECDSA, DSA)
- Add real-time key pair validation to verify private/public key correspondence
- Enhance credential editor UI with unified key input interface supporting upload/paste
- Improve file format support including extensionless files (id_rsa, id_ed25519, etc.)
- Add comprehensive fallback detection for OpenSSH format keys
- Implement debounced API calls for better UX during real-time validation
- Update database schema with backward compatibility for existing credentials
- Add API endpoints for key detection and pair validation
- Fix SSH2 module integration issues in TypeScript environment

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Optimize credentials interface and add i18n improvements

- Merge upload/paste tabs into unified SSH key input interface
- Remove manual key type selection dropdown (rely on auto-detection)
- Add public key generation from private key functionality
- Complete key pair validation removal to fix errors
- Add missing translation keys for better internationalization
- Improve UX with streamlined credential editing workflow

* Implement direct SSH key generation with ssh2 native API

- Replace complex PEM-to-SSH conversion logic with ssh2's generateKeyPairSync
- Add three key generation buttons: Ed25519, ECDSA P-256, and RSA
- Generate keys directly in SSH format (ssh-ed25519, ecdsa-sha2-nistp256, ssh-rsa)
- Fix ECDSA parameter bug: use bits (256) instead of curve for ssh2 API
- Enhance generate-public-key endpoint with SSH format conversion
- Add comprehensive key type detection and parsing fallbacks
- Add internationalization support for key generation UI
- Simplify codebase from 300+ lines to ~80 lines of clean SSH generation

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Add passphrase support for SSH key generation

- Add optional passphrase input field in key generation container
- Implement AES-128-CBC encryption for protected private keys
- Auto-fill key password field when passphrase is provided
- Support passphrase protection for all key types (Ed25519, ECDSA, RSA)
- Enhance user experience with automatic form field population

* 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>

---------

Co-authored-by: ZacharyZcR <zacharyzcr1984@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
This commit was merged in pull request #234.
This commit is contained in:
ZacharyZcR
2025-09-15 10:29:32 +08:00
committed by GitHub
parent 0f75cd4d16
commit a4feb67c08
12 changed files with 2338 additions and 276 deletions

View File

@@ -1506,7 +1506,7 @@ export async function getCredentials(): Promise<any> {
const response = await authApi.get("/credentials");
return response.data;
} catch (error) {
handleApiError(error, "fetch credentials");
throw handleApiError(error, "fetch credentials");
}
}
@@ -1515,7 +1515,7 @@ export async function getCredentialDetails(credentialId: number): Promise<any> {
const response = await authApi.get(`/credentials/${credentialId}`);
return response.data;
} catch (error) {
handleApiError(error, "fetch credential details");
throw handleApiError(error, "fetch credential details");
}
}
@@ -1524,7 +1524,7 @@ export async function createCredential(credentialData: any): Promise<any> {
const response = await authApi.post("/credentials", credentialData);
return response.data;
} catch (error) {
handleApiError(error, "create credential");
throw handleApiError(error, "create credential");
}
}
@@ -1539,7 +1539,7 @@ export async function updateCredential(
);
return response.data;
} catch (error) {
handleApiError(error, "update credential");
throw handleApiError(error, "update credential");
}
}
@@ -1548,7 +1548,7 @@ export async function deleteCredential(credentialId: number): Promise<any> {
const response = await authApi.delete(`/credentials/${credentialId}`);
return response.data;
} catch (error) {
handleApiError(error, "delete credential");
throw handleApiError(error, "delete credential");
}
}
@@ -1594,7 +1594,7 @@ export async function applyCredentialToHost(
);
return response.data;
} catch (error) {
handleApiError(error, "apply credential to host");
throw handleApiError(error, "apply credential to host");
}
}
@@ -1604,7 +1604,7 @@ export async function removeCredentialFromHost(hostId: number): Promise<any> {
const response = await sshHostApi.delete(`/db/host/${hostId}/credential`);
return response.data;
} catch (error) {
handleApiError(error, "remove credential from host");
throw handleApiError(error, "remove credential from host");
}
}
@@ -1620,7 +1620,7 @@ export async function migrateHostToCredential(
);
return response.data;
} catch (error) {
handleApiError(error, "migrate host to credential");
throw handleApiError(error, "migrate host to credential");
}
}
@@ -1663,6 +1663,98 @@ export async function renameCredentialFolder(
});
return response.data;
} catch (error) {
handleApiError(error, "rename credential folder");
throw handleApiError(error, "rename credential folder");
}
}
export async function detectKeyType(
privateKey: string,
keyPassword?: string,
): Promise<any> {
try {
const response = await authApi.post("/credentials/detect-key-type", {
privateKey,
keyPassword,
});
return response.data;
} catch (error) {
throw handleApiError(error, "detect key type");
}
}
export async function detectPublicKeyType(
publicKey: string,
): Promise<any> {
try {
const response = await authApi.post("/credentials/detect-public-key-type", {
publicKey,
});
return response.data;
} catch (error) {
throw handleApiError(error, "detect public key type");
}
}
export async function validateKeyPair(
privateKey: string,
publicKey: string,
keyPassword?: string,
): Promise<any> {
try {
const response = await authApi.post("/credentials/validate-key-pair", {
privateKey,
publicKey,
keyPassword,
});
return response.data;
} catch (error) {
throw handleApiError(error, "validate key pair");
}
}
export async function generatePublicKeyFromPrivate(
privateKey: string,
keyPassword?: string,
): Promise<any> {
try {
const response = await authApi.post("/credentials/generate-public-key", {
privateKey,
keyPassword,
});
return response.data;
} catch (error) {
throw handleApiError(error, "generate public key from private key");
}
}
export async function generateKeyPair(
keyType: 'ssh-ed25519' | 'ssh-rsa' | 'ecdsa-sha2-nistp256',
keySize?: number,
passphrase?: string,
): Promise<any> {
try {
const response = await authApi.post("/credentials/generate-key-pair", {
keyType,
keySize,
passphrase,
});
return response.data;
} catch (error) {
throw handleApiError(error, "generate SSH key pair");
}
}
export async function deployCredentialToHost(
credentialId: number,
targetHostId: number,
): Promise<any> {
try {
const response = await authApi.post(
`/credentials/${credentialId}/deploy-to-host`,
{ targetHostId }
);
return response.data;
} catch (error) {
throw handleApiError(error, "deploy credential to host");
}
}