Spaces:
Running
Running
Update index.html
Browse files- index.html +268 -103
index.html
CHANGED
@@ -303,8 +303,127 @@
|
|
303 |
</div>
|
304 |
|
305 |
<script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
306 |
// Server management
|
307 |
-
let servers = JSON.parse(localStorage.getItem('sshProxyServers')) || [];
|
308 |
let currentServerId = null;
|
309 |
let term;
|
310 |
let socket;
|
@@ -384,9 +503,16 @@
|
|
384 |
}
|
385 |
|
386 |
// Initialize the app
|
387 |
-
document.addEventListener('DOMContentLoaded', () => {
|
388 |
-
|
389 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
390 |
});
|
391 |
|
392 |
// Tab switching
|
@@ -739,34 +865,35 @@
|
|
739 |
}
|
740 |
|
741 |
// Delete server
|
742 |
-
function deleteServer() {
|
743 |
if (!currentServerId) return;
|
744 |
|
745 |
const server = servers.find(s => s.id === currentServerId);
|
746 |
if (!server) return;
|
747 |
|
748 |
-
// Show confirmation dialog
|
749 |
if (confirm(`Are you sure you want to delete the server "${server.name}"?`)) {
|
750 |
-
|
751 |
-
|
752 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
753 |
}
|
754 |
-
|
755 |
-
// Remove server from array
|
756 |
-
servers = servers.filter(s => s.id !== currentServerId);
|
757 |
-
saveServers();
|
758 |
-
renderServers();
|
759 |
-
|
760 |
-
// Close modal
|
761 |
-
closeModal();
|
762 |
-
|
763 |
-
// Show notification
|
764 |
-
showNotification('Server configuration deleted', 'green');
|
765 |
}
|
766 |
}
|
767 |
|
768 |
// Save server
|
769 |
-
function saveServer() {
|
770 |
const mode = this.dataset.mode;
|
771 |
const serverId = this.dataset.id;
|
772 |
|
@@ -787,42 +914,47 @@
|
|
787 |
|
788 |
// Validate required fields
|
789 |
if (!server.name || !server.sshHost || !server.sshUsername || !server.proxyHost) {
|
790 |
-
|
791 |
return;
|
792 |
}
|
793 |
-
|
794 |
-
|
795 |
-
|
796 |
-
|
797 |
-
|
798 |
-
|
799 |
-
|
800 |
-
|
801 |
-
|
802 |
-
|
803 |
-
|
804 |
-
|
805 |
-
const index = servers.findIndex(s => s.id === serverId);
|
806 |
-
if (index !== -1) {
|
807 |
server.id = serverId;
|
808 |
-
|
809 |
-
|
810 |
}
|
811 |
-
|
812 |
-
//
|
813 |
-
|
814 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
815 |
}
|
816 |
-
|
817 |
-
saveServers();
|
818 |
-
renderServers();
|
819 |
-
document.getElementById('servers-tab').click();
|
820 |
-
|
821 |
-
// Show notification
|
822 |
-
showNotification(`Server ${mode === 'edit' ? 'updated' : 'added'} successfully`, 'green');
|
823 |
-
|
824 |
-
// Reset form
|
825 |
-
resetForm();
|
826 |
}
|
827 |
|
828 |
// Reset form
|
@@ -883,69 +1015,70 @@
|
|
883 |
}
|
884 |
|
885 |
// Update server status
|
886 |
-
function updateConnectionState(serverId, state, error = null) {
|
887 |
-
|
888 |
-
|
889 |
-
|
890 |
-
|
891 |
-
|
892 |
-
|
893 |
renderServers();
|
894 |
updateUIForConnectionState(state, error);
|
|
|
|
|
|
|
895 |
}
|
896 |
}
|
897 |
|
898 |
// Export servers
|
899 |
-
function exportServers() {
|
900 |
-
|
901 |
-
|
902 |
-
|
903 |
-
|
904 |
-
|
905 |
-
|
906 |
-
|
907 |
-
|
908 |
-
|
909 |
-
|
910 |
-
|
911 |
-
|
|
|
|
|
|
|
|
|
|
|
912 |
}
|
913 |
|
914 |
// Import servers
|
915 |
-
function importServers(event) {
|
916 |
const file = event.target.files[0];
|
917 |
if (!file) return;
|
918 |
|
919 |
const reader = new FileReader();
|
920 |
-
reader.onload = (e) => {
|
921 |
try {
|
922 |
const importedServers = JSON.parse(e.target.result);
|
923 |
if (Array.isArray(importedServers) && importedServers.length > 0) {
|
924 |
-
//
|
925 |
-
const
|
926 |
-
|
927 |
-
|
928 |
-
if (newServers.length > 0) {
|
929 |
-
servers = [...servers, ...newServers];
|
930 |
-
saveServers();
|
931 |
-
renderServers();
|
932 |
-
|
933 |
-
// Show notification
|
934 |
-
showNotification(`Imported ${newServers.length} server configuration(s)`, 'green');
|
935 |
-
} else {
|
936 |
-
// Show notification
|
937 |
-
showNotification('No new server configurations to import', 'yellow');
|
938 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
939 |
} else {
|
940 |
-
|
941 |
-
showNotification('Invalid server configurations file', 'red');
|
942 |
}
|
943 |
} catch (error) {
|
944 |
-
|
945 |
-
showNotification('
|
946 |
}
|
947 |
|
948 |
-
// Reset file input
|
949 |
event.target.value = '';
|
950 |
};
|
951 |
reader.readAsText(file);
|
@@ -1051,10 +1184,35 @@
|
|
1051 |
term.setOption('theme', themeConfig);
|
1052 |
}
|
1053 |
|
1054 |
-
//
|
1055 |
-
function encryptCredentials(data) {
|
1056 |
-
|
1057 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1058 |
}
|
1059 |
|
1060 |
// Enhanced error handling
|
@@ -1096,12 +1254,19 @@
|
|
1096 |
};
|
1097 |
}
|
1098 |
|
1099 |
-
const debouncedSearch = debounce((searchTerm) => {
|
1100 |
-
|
1101 |
-
|
1102 |
-
const
|
1103 |
-
|
1104 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1105 |
}, 300);
|
1106 |
|
1107 |
// Add accessibility
|
|
|
303 |
</div>
|
304 |
|
305 |
<script>
|
306 |
+
// IndexedDB Database Manager
|
307 |
+
class DatabaseManager {
|
308 |
+
constructor() {
|
309 |
+
this.dbName = 'SSHProxyManager';
|
310 |
+
this.dbVersion = 1;
|
311 |
+
this.storeName = 'servers';
|
312 |
+
this.db = null;
|
313 |
+
}
|
314 |
+
|
315 |
+
async init() {
|
316 |
+
return new Promise((resolve, reject) => {
|
317 |
+
const request = indexedDB.open(this.dbName, this.dbVersion);
|
318 |
+
|
319 |
+
request.onerror = (event) => {
|
320 |
+
console.error('Database error:', event.target.error);
|
321 |
+
reject(event.target.error);
|
322 |
+
};
|
323 |
+
|
324 |
+
request.onsuccess = (event) => {
|
325 |
+
this.db = event.target.result;
|
326 |
+
resolve();
|
327 |
+
};
|
328 |
+
|
329 |
+
request.onupgradeneeded = (event) => {
|
330 |
+
const db = event.target.result;
|
331 |
+
if (!db.objectStoreNames.contains(this.storeName)) {
|
332 |
+
const store = db.createObjectStore(this.storeName, { keyPath: 'id' });
|
333 |
+
// Create indexes for searching
|
334 |
+
store.createIndex('name', 'name', { unique: false });
|
335 |
+
store.createIndex('sshHost', 'sshHost', { unique: false });
|
336 |
+
store.createIndex('status', 'status', { unique: false });
|
337 |
+
}
|
338 |
+
};
|
339 |
+
});
|
340 |
+
}
|
341 |
+
|
342 |
+
async getAllServers() {
|
343 |
+
return new Promise((resolve, reject) => {
|
344 |
+
if (!this.db) {
|
345 |
+
reject(new Error('Database not initialized'));
|
346 |
+
return;
|
347 |
+
}
|
348 |
+
|
349 |
+
const transaction = this.db.transaction([this.storeName], 'readonly');
|
350 |
+
const store = transaction.objectStore(this.storeName);
|
351 |
+
const request = store.getAll();
|
352 |
+
|
353 |
+
request.onsuccess = () => resolve(request.result);
|
354 |
+
request.onerror = () => reject(request.error);
|
355 |
+
});
|
356 |
+
}
|
357 |
+
|
358 |
+
async saveServer(server) {
|
359 |
+
return new Promise((resolve, reject) => {
|
360 |
+
const transaction = this.db.transaction([this.storeName], 'readwrite');
|
361 |
+
const store = transaction.objectStore(this.storeName);
|
362 |
+
const request = store.put(server);
|
363 |
+
|
364 |
+
request.onsuccess = () => resolve(request.result);
|
365 |
+
request.onerror = () => reject(request.error);
|
366 |
+
});
|
367 |
+
}
|
368 |
+
|
369 |
+
async deleteServer(serverId) {
|
370 |
+
return new Promise((resolve, reject) => {
|
371 |
+
const transaction = this.db.transaction([this.storeName], 'readwrite');
|
372 |
+
const store = transaction.objectStore(this.storeName);
|
373 |
+
const request = store.delete(serverId);
|
374 |
+
|
375 |
+
request.onsuccess = () => resolve();
|
376 |
+
request.onerror = () => reject(request.error);
|
377 |
+
});
|
378 |
+
}
|
379 |
+
|
380 |
+
async searchServers(query) {
|
381 |
+
return new Promise((resolve, reject) => {
|
382 |
+
const transaction = this.db.transaction([this.storeName], 'readonly');
|
383 |
+
const store = transaction.objectStore(this.storeName);
|
384 |
+
const nameIndex = store.index('name');
|
385 |
+
const request = nameIndex.getAll();
|
386 |
+
|
387 |
+
request.onsuccess = () => {
|
388 |
+
const servers = request.result;
|
389 |
+
const results = servers.filter(server =>
|
390 |
+
server.name.toLowerCase().includes(query.toLowerCase()) ||
|
391 |
+
server.sshHost.toLowerCase().includes(query.toLowerCase())
|
392 |
+
);
|
393 |
+
resolve(results);
|
394 |
+
};
|
395 |
+
request.onerror = () => reject(request.error);
|
396 |
+
});
|
397 |
+
}
|
398 |
+
|
399 |
+
async updateServerStatus(serverId, status) {
|
400 |
+
return new Promise((resolve, reject) => {
|
401 |
+
const transaction = this.db.transaction([this.storeName], 'readwrite');
|
402 |
+
const store = transaction.objectStore(this.storeName);
|
403 |
+
const request = store.get(serverId);
|
404 |
+
|
405 |
+
request.onsuccess = () => {
|
406 |
+
const server = request.result;
|
407 |
+
if (server) {
|
408 |
+
server.status = status;
|
409 |
+
server.lastStateChange = new Date().toISOString();
|
410 |
+
const updateRequest = store.put(server);
|
411 |
+
updateRequest.onsuccess = () => resolve(server);
|
412 |
+
updateRequest.onerror = () => reject(updateRequest.error);
|
413 |
+
} else {
|
414 |
+
reject(new Error('Server not found'));
|
415 |
+
}
|
416 |
+
};
|
417 |
+
request.onerror = () => reject(request.error);
|
418 |
+
});
|
419 |
+
}
|
420 |
+
}
|
421 |
+
|
422 |
+
// Initialize database manager
|
423 |
+
const dbManager = new DatabaseManager();
|
424 |
+
let servers = []; // Keep this for compatibility with existing code
|
425 |
+
|
426 |
// Server management
|
|
|
427 |
let currentServerId = null;
|
428 |
let term;
|
429 |
let socket;
|
|
|
503 |
}
|
504 |
|
505 |
// Initialize the app
|
506 |
+
document.addEventListener('DOMContentLoaded', async () => {
|
507 |
+
try {
|
508 |
+
await dbManager.init();
|
509 |
+
servers = await dbManager.getAllServers();
|
510 |
+
renderServers();
|
511 |
+
setupEventListeners();
|
512 |
+
} catch (error) {
|
513 |
+
console.error('Failed to initialize database:', error);
|
514 |
+
showNotification('Failed to load server configurations', 'red');
|
515 |
+
}
|
516 |
});
|
517 |
|
518 |
// Tab switching
|
|
|
865 |
}
|
866 |
|
867 |
// Delete server
|
868 |
+
async function deleteServer() {
|
869 |
if (!currentServerId) return;
|
870 |
|
871 |
const server = servers.find(s => s.id === currentServerId);
|
872 |
if (!server) return;
|
873 |
|
|
|
874 |
if (confirm(`Are you sure you want to delete the server "${server.name}"?`)) {
|
875 |
+
try {
|
876 |
+
if (server.status === 'connected') {
|
877 |
+
disconnectFromServer(server.id);
|
878 |
+
}
|
879 |
+
|
880 |
+
// Delete from IndexedDB
|
881 |
+
await dbManager.deleteServer(currentServerId);
|
882 |
+
|
883 |
+
// Update local array
|
884 |
+
servers = servers.filter(s => s.id !== currentServerId);
|
885 |
+
renderServers();
|
886 |
+
closeModal();
|
887 |
+
showNotification('Server configuration deleted', 'green');
|
888 |
+
} catch (error) {
|
889 |
+
console.error('Failed to delete server:', error);
|
890 |
+
showNotification('Failed to delete server configuration', 'red');
|
891 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
892 |
}
|
893 |
}
|
894 |
|
895 |
// Save server
|
896 |
+
async function saveServer() {
|
897 |
const mode = this.dataset.mode;
|
898 |
const serverId = this.dataset.id;
|
899 |
|
|
|
914 |
|
915 |
// Validate required fields
|
916 |
if (!server.name || !server.sshHost || !server.sshUsername || !server.proxyHost) {
|
917 |
+
showNotification('Please fill in all required fields', 'red');
|
918 |
return;
|
919 |
}
|
920 |
+
|
921 |
+
try {
|
922 |
+
// Encrypt sensitive data
|
923 |
+
const sensitiveData = {
|
924 |
+
sshPassword: server.sshPassword,
|
925 |
+
proxyPassword: server.proxyPassword
|
926 |
+
};
|
927 |
+
server.encryptedCredentials = await encryptCredentials(sensitiveData);
|
928 |
+
delete server.sshPassword;
|
929 |
+
delete server.proxyPassword;
|
930 |
+
|
931 |
+
if (mode === 'edit') {
|
|
|
|
|
932 |
server.id = serverId;
|
933 |
+
} else {
|
934 |
+
server.id = Date.now().toString();
|
935 |
}
|
936 |
+
|
937 |
+
// Save to IndexedDB
|
938 |
+
await dbManager.saveServer(server);
|
939 |
+
|
940 |
+
// Update local array
|
941 |
+
if (mode === 'edit') {
|
942 |
+
const index = servers.findIndex(s => s.id === serverId);
|
943 |
+
if (index !== -1) {
|
944 |
+
servers[index] = server;
|
945 |
+
}
|
946 |
+
} else {
|
947 |
+
servers.push(server);
|
948 |
+
}
|
949 |
+
|
950 |
+
renderServers();
|
951 |
+
document.getElementById('servers-tab').click();
|
952 |
+
showNotification(`Server ${mode === 'edit' ? 'updated' : 'added'} successfully`, 'green');
|
953 |
+
resetForm();
|
954 |
+
} catch (error) {
|
955 |
+
console.error('Failed to save server:', error);
|
956 |
+
showNotification('Failed to save server configuration', 'red');
|
957 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
958 |
}
|
959 |
|
960 |
// Reset form
|
|
|
1015 |
}
|
1016 |
|
1017 |
// Update server status
|
1018 |
+
async function updateConnectionState(serverId, state, error = null) {
|
1019 |
+
try {
|
1020 |
+
const updatedServer = await dbManager.updateServerStatus(serverId, state);
|
1021 |
+
const index = servers.findIndex(s => s.id === serverId);
|
1022 |
+
if (index !== -1) {
|
1023 |
+
servers[index] = updatedServer;
|
1024 |
+
}
|
1025 |
renderServers();
|
1026 |
updateUIForConnectionState(state, error);
|
1027 |
+
} catch (error) {
|
1028 |
+
console.error('Failed to update server status:', error);
|
1029 |
+
showNotification('Failed to update server status', 'red');
|
1030 |
}
|
1031 |
}
|
1032 |
|
1033 |
// Export servers
|
1034 |
+
async function exportServers() {
|
1035 |
+
try {
|
1036 |
+
const allServers = await dbManager.getAllServers();
|
1037 |
+
const dataStr = JSON.stringify(allServers, null, 2);
|
1038 |
+
const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
|
1039 |
+
|
1040 |
+
const exportName = 'ssh-proxy-servers-' + new Date().toISOString().slice(0, 10) + '.json';
|
1041 |
+
|
1042 |
+
const linkElement = document.createElement('a');
|
1043 |
+
linkElement.setAttribute('href', dataUri);
|
1044 |
+
linkElement.setAttribute('download', exportName);
|
1045 |
+
linkElement.click();
|
1046 |
+
|
1047 |
+
showNotification('Server configurations exported successfully', 'green');
|
1048 |
+
} catch (error) {
|
1049 |
+
console.error('Failed to export servers:', error);
|
1050 |
+
showNotification('Failed to export server configurations', 'red');
|
1051 |
+
}
|
1052 |
}
|
1053 |
|
1054 |
// Import servers
|
1055 |
+
async function importServers(event) {
|
1056 |
const file = event.target.files[0];
|
1057 |
if (!file) return;
|
1058 |
|
1059 |
const reader = new FileReader();
|
1060 |
+
reader.onload = async (e) => {
|
1061 |
try {
|
1062 |
const importedServers = JSON.parse(e.target.result);
|
1063 |
if (Array.isArray(importedServers) && importedServers.length > 0) {
|
1064 |
+
// Save each server to IndexedDB
|
1065 |
+
for (const server of importedServers) {
|
1066 |
+
await dbManager.saveServer(server);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1067 |
}
|
1068 |
+
|
1069 |
+
// Update local array
|
1070 |
+
servers = await dbManager.getAllServers();
|
1071 |
+
renderServers();
|
1072 |
+
|
1073 |
+
showNotification(`Imported ${importedServers.length} server configuration(s)`, 'green');
|
1074 |
} else {
|
1075 |
+
showNotification('No valid server configurations found', 'yellow');
|
|
|
1076 |
}
|
1077 |
} catch (error) {
|
1078 |
+
console.error('Failed to import servers:', error);
|
1079 |
+
showNotification('Failed to import server configurations', 'red');
|
1080 |
}
|
1081 |
|
|
|
1082 |
event.target.value = '';
|
1083 |
};
|
1084 |
reader.readAsText(file);
|
|
|
1184 |
term.setOption('theme', themeConfig);
|
1185 |
}
|
1186 |
|
1187 |
+
// Enhanced encryption using WebCrypto API
|
1188 |
+
async function encryptCredentials(data) {
|
1189 |
+
const encoder = new TextEncoder();
|
1190 |
+
const key = await window.crypto.subtle.generateKey(
|
1191 |
+
{
|
1192 |
+
name: "AES-GCM",
|
1193 |
+
length: 256
|
1194 |
+
},
|
1195 |
+
true,
|
1196 |
+
["encrypt", "decrypt"]
|
1197 |
+
);
|
1198 |
+
|
1199 |
+
const iv = window.crypto.getRandomValues(new Uint8Array(12));
|
1200 |
+
const encoded = encoder.encode(JSON.stringify(data));
|
1201 |
+
|
1202 |
+
const encrypted = await window.crypto.subtle.encrypt(
|
1203 |
+
{
|
1204 |
+
name: "AES-GCM",
|
1205 |
+
iv: iv
|
1206 |
+
},
|
1207 |
+
key,
|
1208 |
+
encoded
|
1209 |
+
);
|
1210 |
+
|
1211 |
+
return {
|
1212 |
+
encrypted: Array.from(new Uint8Array(encrypted)),
|
1213 |
+
iv: Array.from(iv),
|
1214 |
+
key: await exportKey(key)
|
1215 |
+
};
|
1216 |
}
|
1217 |
|
1218 |
// Enhanced error handling
|
|
|
1254 |
};
|
1255 |
}
|
1256 |
|
1257 |
+
const debouncedSearch = debounce(async (searchTerm) => {
|
1258 |
+
try {
|
1259 |
+
const results = await dbManager.searchServers(searchTerm);
|
1260 |
+
const serverCards = document.querySelectorAll('.server-card');
|
1261 |
+
serverCards.forEach(card => {
|
1262 |
+
const serverId = card.dataset.id;
|
1263 |
+
const found = results.some(server => server.id === serverId);
|
1264 |
+
card.classList.toggle('hidden', !found);
|
1265 |
+
});
|
1266 |
+
} catch (error) {
|
1267 |
+
console.error('Failed to search servers:', error);
|
1268 |
+
showNotification('Failed to search servers', 'red');
|
1269 |
+
}
|
1270 |
}, 300);
|
1271 |
|
1272 |
// Add accessibility
|