termites 1.0.37 → 1.0.39
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.
- package/package.json +1 -1
- package/server.js +244 -16
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -113,6 +113,11 @@ class TermitesServer {
|
|
|
113
113
|
return cookies;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
isAuthenticated(req) {
|
|
117
|
+
const cookies = this.parseCookies(req.headers.cookie || '');
|
|
118
|
+
return cookies.session === this.sessionToken;
|
|
119
|
+
}
|
|
120
|
+
|
|
116
121
|
// Create a new local terminal
|
|
117
122
|
createTerminal() {
|
|
118
123
|
const terminalId = crypto.randomUUID();
|
|
@@ -416,6 +421,81 @@ class TermitesServer {
|
|
|
416
421
|
return;
|
|
417
422
|
}
|
|
418
423
|
|
|
424
|
+
// Change password API
|
|
425
|
+
if (url === '/api/change-password' && req.method === 'POST') {
|
|
426
|
+
if (!this.isAuthenticated(req)) {
|
|
427
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
428
|
+
res.end(JSON.stringify({ success: false, error: 'Not authenticated' }));
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
let body = '';
|
|
432
|
+
req.on('data', chunk => body += chunk);
|
|
433
|
+
req.on('end', () => {
|
|
434
|
+
try {
|
|
435
|
+
const { currentPassword, newPassword } = JSON.parse(body);
|
|
436
|
+
if (!verifyPassword(currentPassword, this.config.passwordHash)) {
|
|
437
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
438
|
+
res.end(JSON.stringify({ success: false, error: 'Current password is incorrect' }));
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
if (!newPassword || newPassword.length < 4) {
|
|
442
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
443
|
+
res.end(JSON.stringify({ success: false, error: 'New password must be at least 4 characters' }));
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
this.config.passwordHash = hashPassword(newPassword);
|
|
447
|
+
this.config.sessionToken = crypto.randomBytes(32).toString('hex');
|
|
448
|
+
this.sessionToken = this.config.sessionToken;
|
|
449
|
+
saveConfig(this.config);
|
|
450
|
+
res.writeHead(200, {
|
|
451
|
+
'Content-Type': 'application/json',
|
|
452
|
+
'Set-Cookie': `session=${this.sessionToken}; Path=/; HttpOnly; SameSite=Strict`
|
|
453
|
+
});
|
|
454
|
+
res.end(JSON.stringify({ success: true }));
|
|
455
|
+
} catch (e) {
|
|
456
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
457
|
+
res.end(JSON.stringify({ success: false, error: 'Invalid request' }));
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Get settings API
|
|
464
|
+
if (url === '/api/settings' && req.method === 'GET') {
|
|
465
|
+
if (!this.isAuthenticated(req)) {
|
|
466
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
467
|
+
res.end(JSON.stringify({ success: false, error: 'Not authenticated' }));
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
471
|
+
res.end(JSON.stringify({ success: true, settings: this.config.settings || {} }));
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Save settings API
|
|
476
|
+
if (url === '/api/settings' && req.method === 'POST') {
|
|
477
|
+
if (!this.isAuthenticated(req)) {
|
|
478
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
479
|
+
res.end(JSON.stringify({ success: false, error: 'Not authenticated' }));
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
let body = '';
|
|
483
|
+
req.on('data', chunk => body += chunk);
|
|
484
|
+
req.on('end', () => {
|
|
485
|
+
try {
|
|
486
|
+
const settings = JSON.parse(body);
|
|
487
|
+
this.config.settings = settings;
|
|
488
|
+
saveConfig(this.config);
|
|
489
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
490
|
+
res.end(JSON.stringify({ success: true }));
|
|
491
|
+
} catch (e) {
|
|
492
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
493
|
+
res.end(JSON.stringify({ success: false, error: 'Invalid request' }));
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
|
|
419
499
|
// No password set - show setup page
|
|
420
500
|
if (!this.config.passwordHash) {
|
|
421
501
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
@@ -682,8 +762,23 @@ class TermitesServer {
|
|
|
682
762
|
.client-item.selected { font-weight: bold; }
|
|
683
763
|
.client-item .client-name { font-size: 13px; margin-bottom: 3px; display: flex; align-items: center; }
|
|
684
764
|
.client-item .client-info { font-size: 11px; opacity: 0.6; padding-left: 14px; }
|
|
765
|
+
.client-item { display: flex; align-items: center; }
|
|
766
|
+
.client-item .client-main { flex: 1; cursor: pointer; }
|
|
767
|
+
.close-terminal-btn {
|
|
768
|
+
background: none; border: none; color: inherit; opacity: 0.4;
|
|
769
|
+
font-size: 18px; padding: 4px 8px; cursor: pointer; margin-right: 8px;
|
|
770
|
+
}
|
|
771
|
+
.close-terminal-btn:hover { opacity: 1; color: #ef4444; }
|
|
685
772
|
.status-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; flex-shrink: 0; }
|
|
686
773
|
.status-dot.online { background: #22c55e; }
|
|
774
|
+
.unread-badge {
|
|
775
|
+
display: inline-block; width: 8px; height: 8px; border-radius: 50%;
|
|
776
|
+
background: #f59e0b; margin-left: 6px; animation: pulse 1.5s infinite;
|
|
777
|
+
}
|
|
778
|
+
@keyframes pulse {
|
|
779
|
+
0%, 100% { opacity: 1; }
|
|
780
|
+
50% { opacity: 0.5; }
|
|
781
|
+
}
|
|
687
782
|
.settings-section { padding: 12px 16px; }
|
|
688
783
|
.setting-group { margin-bottom: 16px; }
|
|
689
784
|
.setting-group:last-child { margin-bottom: 0; }
|
|
@@ -701,6 +796,33 @@ class TermitesServer {
|
|
|
701
796
|
background: transparent; color: inherit; cursor: pointer; font-size: 11px;
|
|
702
797
|
}
|
|
703
798
|
.add-btn:hover { opacity: 0.8; }
|
|
799
|
+
.change-pwd-btn {
|
|
800
|
+
width: 100%; padding: 8px; border: 1px solid; border-radius: 4px;
|
|
801
|
+
background: transparent; color: inherit; cursor: pointer; font-size: 13px;
|
|
802
|
+
}
|
|
803
|
+
.change-pwd-btn:hover { opacity: 0.8; }
|
|
804
|
+
.pwd-modal {
|
|
805
|
+
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
|
806
|
+
background: rgba(0,0,0,0.6); z-index: 200; display: none;
|
|
807
|
+
align-items: center; justify-content: center;
|
|
808
|
+
}
|
|
809
|
+
.pwd-modal.show { display: flex; }
|
|
810
|
+
.pwd-modal-content {
|
|
811
|
+
background: var(--modal-bg, #1a1a2e); padding: 24px; border-radius: 12px;
|
|
812
|
+
width: 320px; max-width: 90vw;
|
|
813
|
+
}
|
|
814
|
+
.pwd-modal h3 { margin-bottom: 16px; font-size: 16px; }
|
|
815
|
+
.pwd-modal input {
|
|
816
|
+
width: 100%; padding: 10px; border: 1px solid; border-radius: 6px;
|
|
817
|
+
background: transparent; color: inherit; margin-bottom: 12px; font-size: 14px;
|
|
818
|
+
}
|
|
819
|
+
.pwd-modal-btns { display: flex; gap: 8px; margin-top: 8px; }
|
|
820
|
+
.pwd-modal-btns button {
|
|
821
|
+
flex: 1; padding: 10px; border: none; border-radius: 6px; cursor: pointer; font-size: 14px;
|
|
822
|
+
}
|
|
823
|
+
.pwd-modal-btns .cancel-btn { background: #666; color: #fff; }
|
|
824
|
+
.pwd-modal-btns .save-btn { background: #22c55e; color: #fff; }
|
|
825
|
+
.pwd-error { color: #ef4444; font-size: 12px; margin-top: 8px; display: none; }
|
|
704
826
|
/* Mobile toolbar */
|
|
705
827
|
.mobile-toolbar {
|
|
706
828
|
display: none; flex-shrink: 0; padding: 6px 8px; gap: 6px;
|
|
@@ -802,6 +924,10 @@ class TermitesServer {
|
|
|
802
924
|
<option value="hide">Always hide</option>
|
|
803
925
|
</select>
|
|
804
926
|
</div>
|
|
927
|
+
<div class="setting-group">
|
|
928
|
+
<label>Password</label>
|
|
929
|
+
<button id="change-password-btn" class="change-pwd-btn">Change Password</button>
|
|
930
|
+
</div>
|
|
805
931
|
</div>
|
|
806
932
|
</div>
|
|
807
933
|
<div class="drawer-section">
|
|
@@ -842,6 +968,19 @@ class TermitesServer {
|
|
|
842
968
|
</div>
|
|
843
969
|
<div class="history-content" id="history-content"></div>
|
|
844
970
|
</div>
|
|
971
|
+
<div class="pwd-modal" id="pwd-modal">
|
|
972
|
+
<div class="pwd-modal-content">
|
|
973
|
+
<h3>Change Password</h3>
|
|
974
|
+
<input type="password" id="current-pwd" placeholder="Current password">
|
|
975
|
+
<input type="password" id="new-pwd" placeholder="New password (min 4 chars)">
|
|
976
|
+
<input type="password" id="confirm-pwd" placeholder="Confirm new password">
|
|
977
|
+
<div class="pwd-error" id="pwd-error"></div>
|
|
978
|
+
<div class="pwd-modal-btns">
|
|
979
|
+
<button class="cancel-btn" id="pwd-cancel">Cancel</button>
|
|
980
|
+
<button class="save-btn" id="pwd-save">Save</button>
|
|
981
|
+
</div>
|
|
982
|
+
</div>
|
|
983
|
+
</div>
|
|
845
984
|
|
|
846
985
|
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js"></script>
|
|
847
986
|
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.min.js"></script>
|
|
@@ -850,6 +989,7 @@ class TermitesServer {
|
|
|
850
989
|
let ws, term, fitAddon;
|
|
851
990
|
let clients = [];
|
|
852
991
|
let selectedClientId = null;
|
|
992
|
+
let unreadTerminals = new Set(); // Track terminals with unread output
|
|
853
993
|
// History buffer for iTerm-like scrolling (stores raw output)
|
|
854
994
|
let historyBuffer = [];
|
|
855
995
|
const MAX_HISTORY_SIZE = 100000; // Max characters to store
|
|
@@ -935,14 +1075,28 @@ class TermitesServer {
|
|
|
935
1075
|
let currentToolbar = 'auto';
|
|
936
1076
|
let modifiers = { ctrl: false, alt: false };
|
|
937
1077
|
|
|
938
|
-
function loadSettings() {
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
const
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
1078
|
+
async function loadSettings() {
|
|
1079
|
+
// Try to load from server first
|
|
1080
|
+
try {
|
|
1081
|
+
const res = await fetch('/api/settings');
|
|
1082
|
+
const data = await res.json();
|
|
1083
|
+
if (data.success && data.settings) {
|
|
1084
|
+
const s = data.settings;
|
|
1085
|
+
currentTheme = s.theme || currentTheme;
|
|
1086
|
+
currentFont = s.font || currentFont;
|
|
1087
|
+
currentFontSize = s.fontSize || currentFontSize;
|
|
1088
|
+
currentToolbar = s.toolbar || currentToolbar;
|
|
1089
|
+
}
|
|
1090
|
+
} catch (e) {
|
|
1091
|
+
// Fallback to localStorage
|
|
1092
|
+
const saved = localStorage.getItem('webshell-settings');
|
|
1093
|
+
if (saved) {
|
|
1094
|
+
const s = JSON.parse(saved);
|
|
1095
|
+
currentTheme = s.theme || currentTheme;
|
|
1096
|
+
currentFont = s.font || currentFont;
|
|
1097
|
+
currentFontSize = s.fontSize || currentFontSize;
|
|
1098
|
+
currentToolbar = s.toolbar || currentToolbar;
|
|
1099
|
+
}
|
|
946
1100
|
}
|
|
947
1101
|
document.getElementById('theme-select').value = currentTheme;
|
|
948
1102
|
document.getElementById('font-select').value = currentFont;
|
|
@@ -952,9 +1106,17 @@ class TermitesServer {
|
|
|
952
1106
|
}
|
|
953
1107
|
|
|
954
1108
|
function saveSettings() {
|
|
955
|
-
|
|
1109
|
+
const settings = {
|
|
956
1110
|
theme: currentTheme, font: currentFont, fontSize: currentFontSize, toolbar: currentToolbar
|
|
957
|
-
}
|
|
1111
|
+
};
|
|
1112
|
+
// Save to localStorage as backup
|
|
1113
|
+
localStorage.setItem('webshell-settings', JSON.stringify(settings));
|
|
1114
|
+
// Save to server
|
|
1115
|
+
fetch('/api/settings', {
|
|
1116
|
+
method: 'POST',
|
|
1117
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1118
|
+
body: JSON.stringify(settings)
|
|
1119
|
+
}).catch(() => {});
|
|
958
1120
|
}
|
|
959
1121
|
|
|
960
1122
|
function applyTheme(themeName) {
|
|
@@ -1076,6 +1238,53 @@ class TermitesServer {
|
|
|
1076
1238
|
document.getElementById('font-select').onchange = e => applyFont(e.target.value);
|
|
1077
1239
|
document.getElementById('font-size').oninput = e => applyFontSize(parseInt(e.target.value));
|
|
1078
1240
|
document.getElementById('toolbar-select').onchange = e => applyToolbar(e.target.value);
|
|
1241
|
+
|
|
1242
|
+
// Change password modal
|
|
1243
|
+
const pwdModal = document.getElementById('pwd-modal');
|
|
1244
|
+
const pwdError = document.getElementById('pwd-error');
|
|
1245
|
+
document.getElementById('change-password-btn').onclick = () => {
|
|
1246
|
+
pwdModal.classList.add('show');
|
|
1247
|
+
document.getElementById('current-pwd').value = '';
|
|
1248
|
+
document.getElementById('new-pwd').value = '';
|
|
1249
|
+
document.getElementById('confirm-pwd').value = '';
|
|
1250
|
+
pwdError.style.display = 'none';
|
|
1251
|
+
};
|
|
1252
|
+
document.getElementById('pwd-cancel').onclick = () => pwdModal.classList.remove('show');
|
|
1253
|
+
document.getElementById('pwd-save').onclick = async () => {
|
|
1254
|
+
const currentPwd = document.getElementById('current-pwd').value;
|
|
1255
|
+
const newPwd = document.getElementById('new-pwd').value;
|
|
1256
|
+
const confirmPwd = document.getElementById('confirm-pwd').value;
|
|
1257
|
+
pwdError.style.display = 'none';
|
|
1258
|
+
|
|
1259
|
+
if (newPwd.length < 4) {
|
|
1260
|
+
pwdError.textContent = 'New password must be at least 4 characters';
|
|
1261
|
+
pwdError.style.display = 'block';
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
if (newPwd !== confirmPwd) {
|
|
1265
|
+
pwdError.textContent = 'Passwords do not match';
|
|
1266
|
+
pwdError.style.display = 'block';
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
try {
|
|
1270
|
+
const res = await fetch('/api/change-password', {
|
|
1271
|
+
method: 'POST',
|
|
1272
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1273
|
+
body: JSON.stringify({ currentPassword: currentPwd, newPassword: newPwd })
|
|
1274
|
+
});
|
|
1275
|
+
const data = await res.json();
|
|
1276
|
+
if (data.success) {
|
|
1277
|
+
pwdModal.classList.remove('show');
|
|
1278
|
+
alert('Password changed successfully');
|
|
1279
|
+
} else {
|
|
1280
|
+
pwdError.textContent = data.error || 'Failed to change password';
|
|
1281
|
+
pwdError.style.display = 'block';
|
|
1282
|
+
}
|
|
1283
|
+
} catch (err) {
|
|
1284
|
+
pwdError.textContent = 'Network error';
|
|
1285
|
+
pwdError.style.display = 'block';
|
|
1286
|
+
}
|
|
1287
|
+
};
|
|
1079
1288
|
}
|
|
1080
1289
|
|
|
1081
1290
|
function setupMobileToolbar() {
|
|
@@ -1502,16 +1711,27 @@ class TermitesServer {
|
|
|
1502
1711
|
}
|
|
1503
1712
|
listEl.innerHTML = clients.map(c => {
|
|
1504
1713
|
const isSelected = c.id === selectedClientId;
|
|
1714
|
+
const hasUnread = unreadTerminals.has(c.id);
|
|
1505
1715
|
const bgColor = isSelected ? t.sidebarSelected : 'transparent';
|
|
1506
1716
|
return '<div class="client-item' + (isSelected ? ' selected' : '') + '" ' +
|
|
1507
|
-
'style="background: ' + bgColor + ';"
|
|
1508
|
-
'onclick="selectClient(\\'' + c.id + '\\')">' +
|
|
1717
|
+
'style="background: ' + bgColor + ';">' +
|
|
1718
|
+
'<div class="client-main" onclick="selectClient(\\'' + c.id + '\\')">' +
|
|
1509
1719
|
'<div class="client-name"><span class="status-dot online"></span>' +
|
|
1510
|
-
c.username + '@' + c.hostname +
|
|
1511
|
-
'<
|
|
1720
|
+
c.username + '@' + c.hostname +
|
|
1721
|
+
(hasUnread ? '<span class="unread-badge"></span>' : '') + '</div>' +
|
|
1722
|
+
'<div class="client-info">' + (c.cwd || '') + ' · ' + (c.platform || '') + '</div></div>' +
|
|
1723
|
+
'<button class="close-terminal-btn" onclick="event.stopPropagation(); closeTerminal(\\'' + c.id + '\\')" title="Close terminal">×</button></div>';
|
|
1512
1724
|
}).join('');
|
|
1513
1725
|
}
|
|
1514
1726
|
|
|
1727
|
+
function closeTerminal(clientId) {
|
|
1728
|
+
if (confirm('Close this terminal?')) {
|
|
1729
|
+
if (ws?.readyState === WebSocket.OPEN) {
|
|
1730
|
+
ws.send(JSON.stringify({ type: 'close-terminal', clientId }));
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1515
1735
|
function selectClient(clientId) {
|
|
1516
1736
|
if (selectedClientId === clientId) {
|
|
1517
1737
|
document.getElementById('drawer').classList.remove('open');
|
|
@@ -1519,6 +1739,8 @@ class TermitesServer {
|
|
|
1519
1739
|
return;
|
|
1520
1740
|
}
|
|
1521
1741
|
selectedClientId = clientId;
|
|
1742
|
+
// Clear unread badge for this terminal
|
|
1743
|
+
unreadTerminals.delete(clientId);
|
|
1522
1744
|
const client = clients.find(c => c.id === clientId);
|
|
1523
1745
|
if (client) {
|
|
1524
1746
|
document.getElementById('header-title').innerHTML =
|
|
@@ -1534,8 +1756,8 @@ class TermitesServer {
|
|
|
1534
1756
|
term.focus();
|
|
1535
1757
|
}
|
|
1536
1758
|
|
|
1537
|
-
function init() {
|
|
1538
|
-
loadSettings();
|
|
1759
|
+
async function init() {
|
|
1760
|
+
await loadSettings();
|
|
1539
1761
|
setupDrawer();
|
|
1540
1762
|
setupMobileToolbar();
|
|
1541
1763
|
setupHistoryOverlay();
|
|
@@ -1696,6 +1918,12 @@ class TermitesServer {
|
|
|
1696
1918
|
addToHistory(d.data);
|
|
1697
1919
|
term.write(d.data);
|
|
1698
1920
|
term.scrollToBottom();
|
|
1921
|
+
} else {
|
|
1922
|
+
// Mark terminal as having unread output
|
|
1923
|
+
if (!unreadTerminals.has(d.clientId)) {
|
|
1924
|
+
unreadTerminals.add(d.clientId);
|
|
1925
|
+
updateClientList();
|
|
1926
|
+
}
|
|
1699
1927
|
}
|
|
1700
1928
|
break;
|
|
1701
1929
|
}
|