illumio-pylo 0.3.10__py3-none-any.whl → 0.3.12__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- illumio_pylo/API/APIConnector.py +145 -103
- illumio_pylo/API/CredentialsManager.py +38 -0
- illumio_pylo/API/Explorer.py +44 -0
- illumio_pylo/API/JsonPayloadTypes.py +39 -0
- illumio_pylo/Helpers/exports.py +1 -1
- illumio_pylo/IPList.py +15 -8
- illumio_pylo/IPMap.py +9 -0
- illumio_pylo/__init__.py +1 -1
- illumio_pylo/cli/commands/__init__.py +1 -0
- illumio_pylo/cli/commands/credential_manager.py +379 -4
- illumio_pylo/cli/commands/label_delete_unused.py +79 -0
- illumio_pylo/cli/commands/ui/credential_manager_ui/app.js +449 -0
- illumio_pylo/cli/commands/ui/credential_manager_ui/index.html +168 -0
- illumio_pylo/cli/commands/ui/credential_manager_ui/styles.css +430 -0
- illumio_pylo/cli/commands/ven_duplicate_remover.py +145 -93
- illumio_pylo/utilities/cli.py +4 -1
- illumio_pylo/utilities/health_monitoring.py +5 -1
- {illumio_pylo-0.3.10.dist-info → illumio_pylo-0.3.12.dist-info}/METADATA +18 -11
- {illumio_pylo-0.3.10.dist-info → illumio_pylo-0.3.12.dist-info}/RECORD +22 -18
- {illumio_pylo-0.3.10.dist-info → illumio_pylo-0.3.12.dist-info}/WHEEL +1 -1
- {illumio_pylo-0.3.10.dist-info → illumio_pylo-0.3.12.dist-info/licenses}/LICENSE +0 -0
- {illumio_pylo-0.3.10.dist-info → illumio_pylo-0.3.12.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from typing import Optional, List
|
|
3
|
+
|
|
4
|
+
import illumio_pylo as pylo
|
|
5
|
+
|
|
6
|
+
from . import Command
|
|
7
|
+
from illumio_pylo.API.JsonPayloadTypes import LabelObjectJsonStructure
|
|
8
|
+
|
|
9
|
+
command_name = "label-delete-unused"
|
|
10
|
+
objects_load_filter = [] # No need to load any objects from PCE
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def fill_parser(parser: argparse.ArgumentParser):
|
|
14
|
+
parser.add_argument('--confirm', action='store_true',
|
|
15
|
+
help='No change will be implemented in the PCE until you use this function to confirm you\'re good with them after review')
|
|
16
|
+
parser.add_argument('--limit', type=int, required=False, default=None,
|
|
17
|
+
help='Maximum number of unused labels to delete (default: all found unused labels)')
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def __main(args, org: pylo.Organization = None, connector: pylo.APIConnector = None, config_data=None, **kwargs):
|
|
21
|
+
|
|
22
|
+
settings_confirmed_changes: bool = args['confirm']
|
|
23
|
+
settings_limit_deletions: Optional[int] = args['limit']
|
|
24
|
+
|
|
25
|
+
print("Fetching all Labels from the PCE... ", end='', flush=True)
|
|
26
|
+
# pylo.log_set_debug()
|
|
27
|
+
labels_json = connector.objects_label_get(max_results=199000, get_usage=True, async_mode=False)
|
|
28
|
+
print("OK!")
|
|
29
|
+
|
|
30
|
+
print(f"Analyzing {len(labels_json)} labels to find unused ones... ")
|
|
31
|
+
unused_labels: List[LabelObjectJsonStructure] = []
|
|
32
|
+
|
|
33
|
+
for label_json in labels_json:
|
|
34
|
+
usage_json = label_json.get('usage', {})
|
|
35
|
+
label_is_used = False
|
|
36
|
+
|
|
37
|
+
for usage_type, usage_confirmed in usage_json.items():
|
|
38
|
+
if usage_confirmed:
|
|
39
|
+
label_is_used = True
|
|
40
|
+
print(f"Label '{label_json.get('value')}' is used in '{usage_type}', skipping deletion.")
|
|
41
|
+
break
|
|
42
|
+
|
|
43
|
+
if not label_is_used:
|
|
44
|
+
print(f"Label '{label_json.get('value')}' is unused, marking for deletion.")
|
|
45
|
+
unused_labels.append(label_json)
|
|
46
|
+
|
|
47
|
+
print()
|
|
48
|
+
print(f"Found {len(unused_labels)} unused labels vs total of {len(labels_json)} labels.")
|
|
49
|
+
|
|
50
|
+
if len(unused_labels) > 0:
|
|
51
|
+
if not settings_confirmed_changes:
|
|
52
|
+
print("No change will be implemented in the PCE until you use the '--confirm' flag to confirm you're good with them after review.")
|
|
53
|
+
else:
|
|
54
|
+
print()
|
|
55
|
+
print(f"Proceeding to delete unused labels up to the limit of '{settings_limit_deletions if settings_limit_deletions is not None else 'all'}'...")
|
|
56
|
+
tracker = connector.new_tracker_for_label_multi_deletion()
|
|
57
|
+
|
|
58
|
+
if settings_limit_deletions is not None:
|
|
59
|
+
unused_labels = unused_labels[:settings_limit_deletions]
|
|
60
|
+
|
|
61
|
+
for label_json in unused_labels:
|
|
62
|
+
tracker.add_label(label_json['href'])
|
|
63
|
+
|
|
64
|
+
tracker.execute_deletion()
|
|
65
|
+
errors_count = tracker.get_errors_count()
|
|
66
|
+
success_count = len(unused_labels) - errors_count
|
|
67
|
+
|
|
68
|
+
for label_json in unused_labels:
|
|
69
|
+
error = tracker.get_error(label_json['href'])
|
|
70
|
+
if error is not None:
|
|
71
|
+
print(f" - ERROR deleting label '{label_json.get('value')}': {error}")
|
|
72
|
+
else:
|
|
73
|
+
print(f" - SUCCESS deleting label '{label_json.get('value')}'")
|
|
74
|
+
|
|
75
|
+
print()
|
|
76
|
+
print(f"Deletion completed: {success_count} labels deleted successfully, {errors_count} errors encountered.")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
command_object = Command(command_name, __main, fill_parser, skip_pce_config_loading=True, load_specific_objects_only=objects_load_filter)
|
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
// Credential Manager Web Editor - JavaScript Application
|
|
2
|
+
|
|
3
|
+
(function() {
|
|
4
|
+
'use strict';
|
|
5
|
+
|
|
6
|
+
// DOM Elements
|
|
7
|
+
const elements = {
|
|
8
|
+
// Credentials table
|
|
9
|
+
credentialsTable: document.getElementById('credentials-table'),
|
|
10
|
+
credentialsBody: document.getElementById('credentials-body'),
|
|
11
|
+
credentialsLoading: document.getElementById('credentials-loading'),
|
|
12
|
+
noCredentials: document.getElementById('no-credentials'),
|
|
13
|
+
|
|
14
|
+
// Notification
|
|
15
|
+
notification: document.getElementById('notification'),
|
|
16
|
+
|
|
17
|
+
// Buttons
|
|
18
|
+
btnNewCredential: document.getElementById('btn-new-credential'),
|
|
19
|
+
btnCloseModal: document.getElementById('btn-close-modal'),
|
|
20
|
+
btnCancel: document.getElementById('btn-cancel'),
|
|
21
|
+
btnSubmit: document.getElementById('btn-submit'),
|
|
22
|
+
btnCloseTestModal: document.getElementById('btn-close-test-modal'),
|
|
23
|
+
btnCloseTest: document.getElementById('btn-close-test'),
|
|
24
|
+
|
|
25
|
+
// Modal
|
|
26
|
+
credentialModal: document.getElementById('credential-modal'),
|
|
27
|
+
modalTitle: document.getElementById('modal-title'),
|
|
28
|
+
credentialForm: document.getElementById('credential-form'),
|
|
29
|
+
|
|
30
|
+
// Form fields
|
|
31
|
+
formMode: document.getElementById('form-mode'),
|
|
32
|
+
formOriginalName: document.getElementById('form-original-name'),
|
|
33
|
+
formName: document.getElementById('form-name'),
|
|
34
|
+
formFqdn: document.getElementById('form-fqdn'),
|
|
35
|
+
formPort: document.getElementById('form-port'),
|
|
36
|
+
formOrgId: document.getElementById('form-org-id'),
|
|
37
|
+
formApiUser: document.getElementById('form-api-user'),
|
|
38
|
+
formApiKey: document.getElementById('form-api-key'),
|
|
39
|
+
formVerifySsl: document.getElementById('form-verify-ssl'),
|
|
40
|
+
formEncrypt: document.getElementById('form-encrypt'),
|
|
41
|
+
formSshKey: document.getElementById('form-ssh-key'),
|
|
42
|
+
formUseWorkdir: document.getElementById('form-use-workdir'),
|
|
43
|
+
|
|
44
|
+
// Form sections
|
|
45
|
+
encryptionSection: document.getElementById('encryption-section'),
|
|
46
|
+
sshKeysSection: document.getElementById('ssh-keys-section'),
|
|
47
|
+
storageSection: document.getElementById('storage-section'),
|
|
48
|
+
apiKeyRequired: document.getElementById('api-key-required'),
|
|
49
|
+
apiKeyHint: document.getElementById('api-key-hint'),
|
|
50
|
+
|
|
51
|
+
// Test modal
|
|
52
|
+
testModal: document.getElementById('test-modal'),
|
|
53
|
+
testResult: document.getElementById('test-result'),
|
|
54
|
+
|
|
55
|
+
// Delete modal
|
|
56
|
+
deleteModal: document.getElementById('delete-modal'),
|
|
57
|
+
deleteCredentialName: document.getElementById('delete-credential-name'),
|
|
58
|
+
btnCloseDeleteModal: document.getElementById('btn-close-delete-modal'),
|
|
59
|
+
btnCancelDelete: document.getElementById('btn-cancel-delete'),
|
|
60
|
+
btnConfirmDelete: document.getElementById('btn-confirm-delete')
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// State
|
|
64
|
+
let sshKeys = [];
|
|
65
|
+
let encryptionAvailable = false;
|
|
66
|
+
let credentialToDelete = null;
|
|
67
|
+
|
|
68
|
+
// Initialize the application
|
|
69
|
+
async function init() {
|
|
70
|
+
await loadCredentials();
|
|
71
|
+
await checkEncryptionStatus();
|
|
72
|
+
setupEventListeners();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Setup event listeners
|
|
76
|
+
function setupEventListeners() {
|
|
77
|
+
// New credential button
|
|
78
|
+
elements.btnNewCredential.addEventListener('click', () => openCreateModal());
|
|
79
|
+
|
|
80
|
+
// Modal close buttons
|
|
81
|
+
elements.btnCloseModal.addEventListener('click', closeModal);
|
|
82
|
+
elements.btnCancel.addEventListener('click', closeModal);
|
|
83
|
+
|
|
84
|
+
// Form submission
|
|
85
|
+
elements.credentialForm.addEventListener('submit', handleFormSubmit);
|
|
86
|
+
|
|
87
|
+
// Encryption checkbox
|
|
88
|
+
elements.formEncrypt.addEventListener('change', toggleSshKeySelection);
|
|
89
|
+
|
|
90
|
+
// Test modal close
|
|
91
|
+
elements.btnCloseTestModal.addEventListener('click', closeTestModal);
|
|
92
|
+
elements.btnCloseTest.addEventListener('click', closeTestModal);
|
|
93
|
+
|
|
94
|
+
// Delete modal buttons
|
|
95
|
+
elements.btnCloseDeleteModal.addEventListener('click', closeDeleteModal);
|
|
96
|
+
elements.btnCancelDelete.addEventListener('click', closeDeleteModal);
|
|
97
|
+
elements.btnConfirmDelete.addEventListener('click', confirmDelete);
|
|
98
|
+
|
|
99
|
+
// Close modals on background click
|
|
100
|
+
elements.credentialModal.addEventListener('click', (e) => {
|
|
101
|
+
if (e.target === elements.credentialModal) closeModal();
|
|
102
|
+
});
|
|
103
|
+
elements.testModal.addEventListener('click', (e) => {
|
|
104
|
+
if (e.target === elements.testModal) closeTestModal();
|
|
105
|
+
});
|
|
106
|
+
elements.deleteModal.addEventListener('click', (e) => {
|
|
107
|
+
if (e.target === elements.deleteModal) closeDeleteModal();
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// API Functions
|
|
112
|
+
async function apiCall(url, options = {}) {
|
|
113
|
+
try {
|
|
114
|
+
const response = await fetch(url, {
|
|
115
|
+
headers: {
|
|
116
|
+
'Content-Type': 'application/json',
|
|
117
|
+
...options.headers
|
|
118
|
+
},
|
|
119
|
+
...options
|
|
120
|
+
});
|
|
121
|
+
const data = await response.json();
|
|
122
|
+
if (!response.ok) {
|
|
123
|
+
throw new Error(data.error || 'An error occurred');
|
|
124
|
+
}
|
|
125
|
+
return data;
|
|
126
|
+
} catch (error) {
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Load all credentials
|
|
132
|
+
async function loadCredentials() {
|
|
133
|
+
elements.credentialsLoading.classList.remove('hidden');
|
|
134
|
+
elements.credentialsTable.classList.add('hidden');
|
|
135
|
+
elements.noCredentials.classList.add('hidden');
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const credentials = await apiCall('/api/credentials');
|
|
139
|
+
renderCredentials(credentials);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
showNotification('Failed to load credentials: ' + error.message, 'error');
|
|
142
|
+
elements.credentialsLoading.classList.add('hidden');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Render credentials table
|
|
147
|
+
function renderCredentials(credentials) {
|
|
148
|
+
elements.credentialsLoading.classList.add('hidden');
|
|
149
|
+
|
|
150
|
+
if (credentials.length === 0) {
|
|
151
|
+
elements.noCredentials.classList.remove('hidden');
|
|
152
|
+
elements.credentialsTable.classList.add('hidden');
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
elements.credentialsTable.classList.remove('hidden');
|
|
157
|
+
elements.credentialsBody.innerHTML = '';
|
|
158
|
+
|
|
159
|
+
for (const cred of credentials) {
|
|
160
|
+
const row = document.createElement('tr');
|
|
161
|
+
row.innerHTML = `
|
|
162
|
+
<td><strong>${escapeHtml(cred.name)}</strong></td>
|
|
163
|
+
<td>${escapeHtml(cred.fqdn)}</td>
|
|
164
|
+
<td>${cred.port}</td>
|
|
165
|
+
<td>${cred.org_id}</td>
|
|
166
|
+
<td>${escapeHtml(cred.api_user)}</td>
|
|
167
|
+
<td class="${cred.verify_ssl ? 'status-yes' : 'status-no'}">${cred.verify_ssl ? 'Yes' : 'No'}</td>
|
|
168
|
+
<td title="${escapeHtml(cred.originating_file)}">${truncatePath(cred.originating_file)}</td>
|
|
169
|
+
<td class="actions-cell">
|
|
170
|
+
<button class="btn btn-small btn-success btn-test" data-name="${escapeHtml(cred.name)}">Test</button>
|
|
171
|
+
<button class="btn btn-small btn-secondary btn-edit" data-name="${escapeHtml(cred.name)}">Edit</button>
|
|
172
|
+
<button class="btn btn-small btn-danger btn-delete" data-name="${escapeHtml(cred.name)}">Delete</button>
|
|
173
|
+
</td>
|
|
174
|
+
`;
|
|
175
|
+
elements.credentialsBody.appendChild(row);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Add event listeners to buttons
|
|
179
|
+
document.querySelectorAll('.btn-test').forEach(btn => {
|
|
180
|
+
btn.addEventListener('click', () => testCredential(btn.dataset.name));
|
|
181
|
+
});
|
|
182
|
+
document.querySelectorAll('.btn-edit').forEach(btn => {
|
|
183
|
+
btn.addEventListener('click', () => openEditModal(btn.dataset.name));
|
|
184
|
+
});
|
|
185
|
+
document.querySelectorAll('.btn-delete').forEach(btn => {
|
|
186
|
+
btn.addEventListener('click', () => openDeleteModal(btn.dataset.name));
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Check encryption availability and load SSH keys
|
|
191
|
+
async function checkEncryptionStatus() {
|
|
192
|
+
try {
|
|
193
|
+
const status = await apiCall('/api/encryption-status');
|
|
194
|
+
encryptionAvailable = status.available;
|
|
195
|
+
|
|
196
|
+
if (encryptionAvailable) {
|
|
197
|
+
const keysData = await apiCall('/api/ssh-keys');
|
|
198
|
+
sshKeys = keysData.keys || [];
|
|
199
|
+
|
|
200
|
+
if (sshKeys.length > 0) {
|
|
201
|
+
elements.encryptionSection.classList.remove('hidden');
|
|
202
|
+
populateSshKeySelect();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
} catch (error) {
|
|
206
|
+
console.log('Encryption not available:', error.message);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Populate SSH key select dropdown
|
|
211
|
+
function populateSshKeySelect() {
|
|
212
|
+
elements.formSshKey.innerHTML = '';
|
|
213
|
+
for (const key of sshKeys) {
|
|
214
|
+
const option = document.createElement('option');
|
|
215
|
+
option.value = key.index;
|
|
216
|
+
option.textContent = `${key.type} | ${key.fingerprint.substring(0, 16)}... | ${key.comment || 'No comment'}`;
|
|
217
|
+
elements.formSshKey.appendChild(option);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Toggle SSH key selection visibility
|
|
222
|
+
function toggleSshKeySelection() {
|
|
223
|
+
if (elements.formEncrypt.checked && sshKeys.length > 0) {
|
|
224
|
+
elements.sshKeysSection.classList.remove('hidden');
|
|
225
|
+
} else {
|
|
226
|
+
elements.sshKeysSection.classList.add('hidden');
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Open modal for creating new credential
|
|
231
|
+
function openCreateModal() {
|
|
232
|
+
resetForm();
|
|
233
|
+
elements.formMode.value = 'create';
|
|
234
|
+
elements.modalTitle.textContent = 'New Credential';
|
|
235
|
+
elements.btnSubmit.textContent = 'Create';
|
|
236
|
+
elements.formName.removeAttribute('readonly');
|
|
237
|
+
elements.formApiKey.setAttribute('required', 'required');
|
|
238
|
+
elements.apiKeyRequired.classList.remove('hidden');
|
|
239
|
+
elements.apiKeyHint.classList.add('hidden');
|
|
240
|
+
elements.storageSection.classList.remove('hidden');
|
|
241
|
+
elements.credentialModal.classList.remove('hidden');
|
|
242
|
+
elements.formName.focus();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Open modal for editing existing credential
|
|
246
|
+
async function openEditModal(name) {
|
|
247
|
+
resetForm();
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
const credential = await apiCall(`/api/credentials/${encodeURIComponent(name)}`);
|
|
251
|
+
|
|
252
|
+
elements.formMode.value = 'edit';
|
|
253
|
+
elements.formOriginalName.value = credential.name;
|
|
254
|
+
elements.modalTitle.textContent = 'Edit Credential';
|
|
255
|
+
elements.btnSubmit.textContent = 'Update';
|
|
256
|
+
|
|
257
|
+
// Populate form
|
|
258
|
+
elements.formName.value = credential.name;
|
|
259
|
+
elements.formName.setAttribute('readonly', 'readonly');
|
|
260
|
+
elements.formFqdn.value = credential.fqdn;
|
|
261
|
+
elements.formPort.value = credential.port;
|
|
262
|
+
elements.formOrgId.value = credential.org_id;
|
|
263
|
+
elements.formApiUser.value = credential.api_user;
|
|
264
|
+
elements.formVerifySsl.checked = credential.verify_ssl;
|
|
265
|
+
|
|
266
|
+
// API key is optional for updates
|
|
267
|
+
elements.formApiKey.removeAttribute('required');
|
|
268
|
+
elements.apiKeyRequired.classList.add('hidden');
|
|
269
|
+
elements.apiKeyHint.classList.remove('hidden');
|
|
270
|
+
|
|
271
|
+
// Hide storage section for edits
|
|
272
|
+
elements.storageSection.classList.add('hidden');
|
|
273
|
+
|
|
274
|
+
elements.credentialModal.classList.remove('hidden');
|
|
275
|
+
elements.formFqdn.focus();
|
|
276
|
+
} catch (error) {
|
|
277
|
+
showNotification('Failed to load credential: ' + error.message, 'error');
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Open modal for deleting credential
|
|
282
|
+
function openDeleteModal(name) {
|
|
283
|
+
credentialToDelete = name;
|
|
284
|
+
elements.deleteCredentialName.textContent = name;
|
|
285
|
+
elements.deleteModal.classList.remove('hidden');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Close modal
|
|
289
|
+
function closeModal() {
|
|
290
|
+
elements.credentialModal.classList.add('hidden');
|
|
291
|
+
resetForm();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Close delete modal
|
|
295
|
+
function closeDeleteModal() {
|
|
296
|
+
elements.deleteModal.classList.add('hidden');
|
|
297
|
+
credentialToDelete = null;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Reset form
|
|
301
|
+
function resetForm() {
|
|
302
|
+
elements.credentialForm.reset();
|
|
303
|
+
elements.formMode.value = 'create';
|
|
304
|
+
elements.formOriginalName.value = '';
|
|
305
|
+
elements.formPort.value = '8443';
|
|
306
|
+
elements.formOrgId.value = '1';
|
|
307
|
+
elements.formVerifySsl.checked = true;
|
|
308
|
+
elements.formEncrypt.checked = false;
|
|
309
|
+
elements.sshKeysSection.classList.add('hidden');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Handle form submission
|
|
313
|
+
async function handleFormSubmit(e) {
|
|
314
|
+
e.preventDefault();
|
|
315
|
+
|
|
316
|
+
const mode = elements.formMode.value;
|
|
317
|
+
const data = {
|
|
318
|
+
name: elements.formName.value.trim(),
|
|
319
|
+
fqdn: elements.formFqdn.value.trim(),
|
|
320
|
+
port: parseInt(elements.formPort.value),
|
|
321
|
+
org_id: parseInt(elements.formOrgId.value),
|
|
322
|
+
api_user: elements.formApiUser.value.trim(),
|
|
323
|
+
verify_ssl: elements.formVerifySsl.checked
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
// Add API key if provided
|
|
327
|
+
const apiKey = elements.formApiKey.value;
|
|
328
|
+
if (apiKey) {
|
|
329
|
+
data.api_key = apiKey;
|
|
330
|
+
} else if (mode === 'create') {
|
|
331
|
+
showNotification('API key is required', 'error');
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Add encryption settings
|
|
336
|
+
if (elements.formEncrypt.checked && sshKeys.length > 0) {
|
|
337
|
+
data.encrypt = true;
|
|
338
|
+
data.ssh_key_index = parseInt(elements.formSshKey.value);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Add storage location for create
|
|
342
|
+
if (mode === 'create') {
|
|
343
|
+
data.use_current_workdir = elements.formUseWorkdir.checked;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
elements.btnSubmit.disabled = true;
|
|
347
|
+
elements.btnSubmit.textContent = mode === 'create' ? 'Creating...' : 'Updating...';
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
if (mode === 'create') {
|
|
351
|
+
await apiCall('/api/credentials', {
|
|
352
|
+
method: 'POST',
|
|
353
|
+
body: JSON.stringify(data)
|
|
354
|
+
});
|
|
355
|
+
showNotification('Credential created successfully!', 'success');
|
|
356
|
+
} else {
|
|
357
|
+
const originalName = elements.formOriginalName.value;
|
|
358
|
+
await apiCall(`/api/credentials/${encodeURIComponent(originalName)}`, {
|
|
359
|
+
method: 'PUT',
|
|
360
|
+
body: JSON.stringify(data)
|
|
361
|
+
});
|
|
362
|
+
showNotification('Credential updated successfully!', 'success');
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
closeModal();
|
|
366
|
+
await loadCredentials();
|
|
367
|
+
} catch (error) {
|
|
368
|
+
showNotification('Failed to save credential: ' + error.message, 'error');
|
|
369
|
+
} finally {
|
|
370
|
+
elements.btnSubmit.disabled = false;
|
|
371
|
+
elements.btnSubmit.textContent = mode === 'create' ? 'Create' : 'Update';
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Confirm credential deletion
|
|
376
|
+
async function confirmDelete() {
|
|
377
|
+
if (!credentialToDelete) return;
|
|
378
|
+
|
|
379
|
+
elements.btnConfirmDelete.disabled = true;
|
|
380
|
+
elements.btnConfirmDelete.textContent = 'Deleting...';
|
|
381
|
+
|
|
382
|
+
try {
|
|
383
|
+
await apiCall(`/api/credentials/${encodeURIComponent(credentialToDelete)}`, {
|
|
384
|
+
method: 'DELETE'
|
|
385
|
+
});
|
|
386
|
+
showNotification('Credential deleted successfully!', 'success');
|
|
387
|
+
closeDeleteModal();
|
|
388
|
+
await loadCredentials();
|
|
389
|
+
} catch (error) {
|
|
390
|
+
showNotification('Failed to delete credential: ' + error.message, 'error');
|
|
391
|
+
} finally {
|
|
392
|
+
elements.btnConfirmDelete.disabled = false;
|
|
393
|
+
elements.btnConfirmDelete.textContent = 'Delete';
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Test credential connection
|
|
398
|
+
async function testCredential(name) {
|
|
399
|
+
elements.testResult.className = 'test-result loading';
|
|
400
|
+
elements.testResult.textContent = 'Testing connection...';
|
|
401
|
+
elements.testModal.classList.remove('hidden');
|
|
402
|
+
|
|
403
|
+
try {
|
|
404
|
+
const result = await apiCall(`/api/credentials/${encodeURIComponent(name)}/test`, {
|
|
405
|
+
method: 'POST'
|
|
406
|
+
});
|
|
407
|
+
elements.testResult.className = 'test-result success';
|
|
408
|
+
elements.testResult.textContent = `Connection to "${name}" successful!`;
|
|
409
|
+
} catch (error) {
|
|
410
|
+
elements.testResult.className = 'test-result error';
|
|
411
|
+
elements.testResult.textContent = `Connection failed: ${error.message}`;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Close test modal
|
|
416
|
+
function closeTestModal() {
|
|
417
|
+
elements.testModal.classList.add('hidden');
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Show notification
|
|
421
|
+
function showNotification(message, type = 'info') {
|
|
422
|
+
elements.notification.textContent = message;
|
|
423
|
+
elements.notification.className = `notification ${type}`;
|
|
424
|
+
elements.notification.classList.remove('hidden');
|
|
425
|
+
|
|
426
|
+
// Auto-hide after 5 seconds
|
|
427
|
+
setTimeout(() => {
|
|
428
|
+
elements.notification.classList.add('hidden');
|
|
429
|
+
}, 5000);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Utility: Escape HTML
|
|
433
|
+
function escapeHtml(text) {
|
|
434
|
+
const div = document.createElement('div');
|
|
435
|
+
div.textContent = text;
|
|
436
|
+
return div.innerHTML;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Utility: Truncate file path for display
|
|
440
|
+
function truncatePath(path) {
|
|
441
|
+
if (path.length > 30) {
|
|
442
|
+
return '...' + path.slice(-27);
|
|
443
|
+
}
|
|
444
|
+
return path;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Start the application
|
|
448
|
+
document.addEventListener('DOMContentLoaded', init);
|
|
449
|
+
})();
|