locadot 2.2.0 → 2.3.0
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/CHANGELOG.md +12 -0
- package/README.md +18 -2
- package/dist/cli/commands/hosts.js +29 -1
- package/dist/cli/commands/tunnel.js +9 -3
- package/dist/dashboard/api.js +22 -5
- package/dist/dashboard/index.js +2 -0
- package/dist/dashboard/page.js +740 -86
- package/dist/index.js +12 -2
- package/dist/lib/allow.js +60 -0
- package/dist/lib/hosts.js +52 -4
- package/dist/proxy/guard.js +30 -1
- package/dist/proxy/options.js +10 -2
- package/dist/proxy/router.js +27 -7
- package/dist/proxy/tunnel.js +62 -22
- package/dist/server/index.js +1 -0
- package/package.json +1 -1
package/dist/dashboard/page.js
CHANGED
|
@@ -348,11 +348,16 @@ td.badges .badge + .badge { margin-left: 4px; }
|
|
|
348
348
|
.btn:hover:not(:disabled) { background: var(--surface-2); border-color: var(--muted-dim); }
|
|
349
349
|
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
350
350
|
.btn-sm { height: 28px; padding: 0 10px; font-size: 12px; }
|
|
351
|
+
.btn-sm.btn-icon { width: 28px; padding: 0; color: var(--muted); }
|
|
352
|
+
.btn-icon svg { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
|
|
353
|
+
.btn-icon.is-set { color: var(--accent); border-color: transparent; background: var(--accent-bg); }
|
|
351
354
|
.btn-block { width: 100%; }
|
|
352
355
|
.btn-primary { border-color: var(--accent); background: var(--accent); color: var(--accent-fg); }
|
|
353
356
|
.btn-primary:hover:not(:disabled) { background: var(--accent-strong); border-color: var(--accent-strong); }
|
|
354
357
|
.btn-danger { border-color: var(--down); color: var(--down); background: transparent; }
|
|
355
358
|
.btn-danger:hover:not(:disabled) { background: var(--down-bg); border-color: var(--down); }
|
|
359
|
+
.modal-foot .btn-danger { background: var(--down); border-color: var(--down); color: #fff; }
|
|
360
|
+
.modal-foot .btn-danger:hover:not(:disabled) { background: var(--down); filter: brightness(0.92); }
|
|
356
361
|
.btn-ghost { background: transparent; border-color: transparent; box-shadow: none; color: var(--muted); }
|
|
357
362
|
.btn-ghost:hover:not(:disabled) { background: var(--surface-2); border-color: transparent; color: var(--fg); }
|
|
358
363
|
.btn-ghost-danger { background: transparent; border-color: transparent; box-shadow: none; color: var(--muted); }
|
|
@@ -575,6 +580,8 @@ td .inline-check + .inline-check { margin-top: 4px; }
|
|
|
575
580
|
.inline-check { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--muted); }
|
|
576
581
|
.tunnel-cell { white-space: normal; }
|
|
577
582
|
.tunnel-status { display: flex; align-items: center; gap: 6px; max-width: 240px; }
|
|
583
|
+
.tunnel-login { display: flex; flex-direction: column; gap: 2px; font-size: 12px; white-space: nowrap; }
|
|
584
|
+
.tunnel-login a { font-weight: 500; }
|
|
578
585
|
.tunnel-status a { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 180px; display: inline-block; vertical-align: middle; font-size: 12px; }
|
|
579
586
|
.tunnel-error { color: var(--down); cursor: help; }
|
|
580
587
|
.tunnel-error-text {
|
|
@@ -729,6 +736,139 @@ details.card[open] summary { border-bottom: 1px solid var(--border); border-bott
|
|
|
729
736
|
margin: 0;
|
|
730
737
|
}
|
|
731
738
|
|
|
739
|
+
/* ---------- allow dialog ---------- */
|
|
740
|
+
#allow-dialog {
|
|
741
|
+
width: min(520px, calc(100vw - 32px));
|
|
742
|
+
padding: 0;
|
|
743
|
+
border: 1px solid var(--border);
|
|
744
|
+
border-radius: var(--radius);
|
|
745
|
+
background: var(--surface);
|
|
746
|
+
color: var(--fg);
|
|
747
|
+
box-shadow: var(--shadow-md);
|
|
748
|
+
}
|
|
749
|
+
#allow-dialog::backdrop { background: rgba(0, 0, 0, 0.45); }
|
|
750
|
+
.modal-head {
|
|
751
|
+
display: flex;
|
|
752
|
+
align-items: flex-start;
|
|
753
|
+
justify-content: space-between;
|
|
754
|
+
gap: 12px;
|
|
755
|
+
padding: 16px 20px;
|
|
756
|
+
border-bottom: 1px solid var(--border);
|
|
757
|
+
}
|
|
758
|
+
.modal-title { font-size: 15px; font-weight: 600; margin: 0; letter-spacing: -0.01em; }
|
|
759
|
+
.modal-sub { margin: 2px 0 0; font-size: 12.5px; color: var(--muted); }
|
|
760
|
+
.modal-close { width: 28px; height: 28px; padding: 0; border-radius: 999px; flex-shrink: 0; }
|
|
761
|
+
.modal-close svg { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; }
|
|
762
|
+
.modal-body { padding: 20px; display: flex; flex-direction: column; gap: 14px; }
|
|
763
|
+
.modal-desc { margin: 0; font-size: 13px; color: var(--muted); line-height: 1.5; }
|
|
764
|
+
.modal-warn {
|
|
765
|
+
font-size: 12.5px;
|
|
766
|
+
color: var(--warn);
|
|
767
|
+
background: var(--warn-bg);
|
|
768
|
+
border: 1px solid var(--warn);
|
|
769
|
+
border-radius: var(--radius-sm);
|
|
770
|
+
padding: 8px 12px;
|
|
771
|
+
line-height: 1.5;
|
|
772
|
+
}
|
|
773
|
+
.allow-list { display: flex; flex-direction: column; gap: 6px; max-height: 220px; overflow-y: auto; }
|
|
774
|
+
.allow-row {
|
|
775
|
+
display: flex;
|
|
776
|
+
align-items: center;
|
|
777
|
+
justify-content: space-between;
|
|
778
|
+
gap: 10px;
|
|
779
|
+
padding: 6px 10px;
|
|
780
|
+
border: 1px solid var(--border);
|
|
781
|
+
border-radius: var(--radius-sm);
|
|
782
|
+
background: var(--surface-2);
|
|
783
|
+
}
|
|
784
|
+
.allow-row span { font-size: 13px; word-break: break-all; }
|
|
785
|
+
.allow-remove {
|
|
786
|
+
display: inline-flex;
|
|
787
|
+
align-items: center;
|
|
788
|
+
justify-content: center;
|
|
789
|
+
width: 22px;
|
|
790
|
+
height: 22px;
|
|
791
|
+
padding: 0;
|
|
792
|
+
font-size: 15px;
|
|
793
|
+
line-height: 1;
|
|
794
|
+
border-radius: var(--radius-xs);
|
|
795
|
+
border: 1px solid transparent;
|
|
796
|
+
background: transparent;
|
|
797
|
+
color: var(--muted);
|
|
798
|
+
cursor: pointer;
|
|
799
|
+
flex-shrink: 0;
|
|
800
|
+
}
|
|
801
|
+
.allow-remove:hover { color: var(--down); background: var(--down-bg); border-color: transparent; }
|
|
802
|
+
.allow-empty { font-size: 13px; padding: 4px 0; }
|
|
803
|
+
.allow-add-row { display: flex; gap: 8px; }
|
|
804
|
+
.allow-add-row .input { flex: 1; }
|
|
805
|
+
.allow-add-row .btn { height: auto; align-self: stretch; padding: 0 14px; }
|
|
806
|
+
.modal-hint { margin: 0; font-size: 12px; color: var(--muted-dim); }
|
|
807
|
+
.modal-foot {
|
|
808
|
+
display: flex;
|
|
809
|
+
justify-content: flex-end;
|
|
810
|
+
gap: 8px;
|
|
811
|
+
padding: 16px 20px;
|
|
812
|
+
border-top: 1px solid var(--border);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/* ---------- confirm / prompt / share dialogs ---------- */
|
|
816
|
+
#confirm-dialog, #prompt-dialog, #share-dialog {
|
|
817
|
+
padding: 0;
|
|
818
|
+
border: 1px solid var(--border);
|
|
819
|
+
border-radius: var(--radius);
|
|
820
|
+
background: var(--surface);
|
|
821
|
+
color: var(--fg);
|
|
822
|
+
box-shadow: var(--shadow-md);
|
|
823
|
+
}
|
|
824
|
+
#confirm-dialog::backdrop, #prompt-dialog::backdrop, #share-dialog::backdrop { background: rgba(0, 0, 0, 0.45); }
|
|
825
|
+
#confirm-dialog { width: min(420px, calc(100vw - 32px)); }
|
|
826
|
+
#prompt-dialog { width: min(420px, calc(100vw - 32px)); }
|
|
827
|
+
#share-dialog { width: min(480px, calc(100vw - 32px)); }
|
|
828
|
+
.share-options { display: flex; flex-direction: column; gap: 10px; }
|
|
829
|
+
.option-card {
|
|
830
|
+
display: flex;
|
|
831
|
+
align-items: flex-start;
|
|
832
|
+
gap: 12px;
|
|
833
|
+
width: 100%;
|
|
834
|
+
text-align: left;
|
|
835
|
+
padding: 14px;
|
|
836
|
+
border: 1px solid var(--border);
|
|
837
|
+
border-radius: var(--radius);
|
|
838
|
+
background: var(--surface-2);
|
|
839
|
+
color: inherit;
|
|
840
|
+
font: inherit;
|
|
841
|
+
cursor: pointer;
|
|
842
|
+
}
|
|
843
|
+
.option-card:focus-visible { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-bg); }
|
|
844
|
+
.option-card[aria-checked="true"] { border-color: var(--accent); background: var(--accent-bg); }
|
|
845
|
+
.option-dot {
|
|
846
|
+
width: 16px;
|
|
847
|
+
height: 16px;
|
|
848
|
+
margin-top: 2px;
|
|
849
|
+
flex-shrink: 0;
|
|
850
|
+
border-radius: 999px;
|
|
851
|
+
border: 1.5px solid var(--border-strong);
|
|
852
|
+
background: var(--surface);
|
|
853
|
+
position: relative;
|
|
854
|
+
}
|
|
855
|
+
.option-card[aria-checked="true"] .option-dot { border-color: var(--accent); }
|
|
856
|
+
.option-card[aria-checked="true"] .option-dot::after {
|
|
857
|
+
content: "";
|
|
858
|
+
position: absolute;
|
|
859
|
+
top: 3px;
|
|
860
|
+
left: 3px;
|
|
861
|
+
width: 8px;
|
|
862
|
+
height: 8px;
|
|
863
|
+
border-radius: 999px;
|
|
864
|
+
background: var(--accent);
|
|
865
|
+
}
|
|
866
|
+
.option-body { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
|
|
867
|
+
.option-title { font-size: 13.5px; font-weight: 600; }
|
|
868
|
+
.option-desc { font-size: 12.5px; color: var(--muted); line-height: 1.45; }
|
|
869
|
+
.option-extra { margin-top: 10px; display: flex; flex-direction: column; gap: 6px; }
|
|
870
|
+
.option-extra .field-error { margin-top: 4px; }
|
|
871
|
+
|
|
732
872
|
/* ---------- toasts ---------- */
|
|
733
873
|
.toast-container {
|
|
734
874
|
position: fixed;
|
|
@@ -1080,6 +1220,7 @@ const clientJs = `
|
|
|
1080
1220
|
var startupBusy = false;
|
|
1081
1221
|
var cfInstallBusy = false;
|
|
1082
1222
|
var cloudflaredInstalled = false;
|
|
1223
|
+
var cloudflareLoggedIn = false;
|
|
1083
1224
|
var proxyStopped = false;
|
|
1084
1225
|
var pollTimer = null;
|
|
1085
1226
|
var remotesPollTimer = null;
|
|
@@ -1297,6 +1438,7 @@ const clientJs = `
|
|
|
1297
1438
|
|
|
1298
1439
|
var cf = sys.cloudflared || { installed: false };
|
|
1299
1440
|
cloudflaredInstalled = !!cf.installed;
|
|
1441
|
+
cloudflareLoggedIn = !!cf.loggedIn;
|
|
1300
1442
|
if (cf.installed) {
|
|
1301
1443
|
cfValueEl.textContent = "Installed";
|
|
1302
1444
|
cfValueEl.className = "tile-value ok";
|
|
@@ -1378,11 +1520,13 @@ const clientJs = `
|
|
|
1378
1520
|
revokeBtn.className = "btn btn-sm btn-danger";
|
|
1379
1521
|
revokeBtn.textContent = "Revoke";
|
|
1380
1522
|
revokeBtn.addEventListener("click", function () {
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1523
|
+
confirmDialog({ title: "Revoke " + peer.name + "?", message: "This peer will lose access immediately.", confirmLabel: "Revoke", danger: true }).then(function (ok) {
|
|
1524
|
+
if (!ok) return;
|
|
1525
|
+
revokeBtn.disabled = true;
|
|
1526
|
+
apiFetch("/api/peers/" + encodeURIComponent(peer.id), { method: "DELETE" })
|
|
1527
|
+
.then(function () { showToast("Revoked " + peer.name, "success"); return loadHub(); })
|
|
1528
|
+
.catch(function (err) { apiError(err, "Couldn't revoke peer"); revokeBtn.disabled = false; });
|
|
1529
|
+
});
|
|
1386
1530
|
});
|
|
1387
1531
|
controls.appendChild(revokeBtn);
|
|
1388
1532
|
row.appendChild(controls);
|
|
@@ -1565,13 +1709,15 @@ const clientJs = `
|
|
|
1565
1709
|
});
|
|
1566
1710
|
|
|
1567
1711
|
hubStopBtn.addEventListener("click", function () {
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1712
|
+
confirmDialog({ title: "Stop remote access?", message: "Connected peers will be disconnected.", confirmLabel: "Stop remote access", danger: true }).then(function (ok) {
|
|
1713
|
+
if (!ok) return;
|
|
1714
|
+
hubStopBtn.disabled = true;
|
|
1715
|
+
hubStopBtn.classList.add("busy");
|
|
1716
|
+
apiFetch("/api/hub", { method: "POST", body: JSON.stringify({ mode: "off" }) })
|
|
1717
|
+
.then(function () { showToast("Remote access stopped", "success"); return loadHub(); })
|
|
1718
|
+
.catch(function (err) { apiError(err, "Couldn't stop remote access"); })
|
|
1719
|
+
.then(function () { hubStopBtn.disabled = false; hubStopBtn.classList.remove("busy"); });
|
|
1720
|
+
});
|
|
1575
1721
|
});
|
|
1576
1722
|
|
|
1577
1723
|
// ---------- remote access: connected machines (receiver) ----------
|
|
@@ -1633,11 +1779,12 @@ const clientJs = `
|
|
|
1633
1779
|
urlEditBtn.className = "btn btn-sm btn-ghost";
|
|
1634
1780
|
urlEditBtn.textContent = "Update URL";
|
|
1635
1781
|
urlEditBtn.addEventListener("click", function () {
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1782
|
+
promptDialog({ title: "Update URL", message: "New URL for " + remote.name, label: "URL", value: remote.url, confirmLabel: "Save" }).then(function (next) {
|
|
1783
|
+
if (!next || !next.trim() || next.trim() === remote.url) return;
|
|
1784
|
+
apiFetch("/api/remotes/" + encodeURIComponent(remote.name), { method: "PUT", body: JSON.stringify({ url: next.trim() }) })
|
|
1785
|
+
.then(function () { showToast("Updated URL for " + remote.name, "success"); return loadRemotes(); })
|
|
1786
|
+
.catch(function (err) { apiError(err, "Couldn't update URL"); });
|
|
1787
|
+
});
|
|
1641
1788
|
});
|
|
1642
1789
|
urlRow.appendChild(urlEditBtn);
|
|
1643
1790
|
item.appendChild(urlRow);
|
|
@@ -1663,11 +1810,13 @@ const clientJs = `
|
|
|
1663
1810
|
disconnectBtn.className = "btn btn-sm btn-danger";
|
|
1664
1811
|
disconnectBtn.textContent = "Disconnect";
|
|
1665
1812
|
disconnectBtn.addEventListener("click", function () {
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1813
|
+
confirmDialog({ title: "Disconnect from " + remote.name + "?", message: "This removes its hosts too.", confirmLabel: "Disconnect", danger: true }).then(function (ok) {
|
|
1814
|
+
if (!ok) return;
|
|
1815
|
+
disconnectBtn.disabled = true;
|
|
1816
|
+
apiFetch("/api/remotes/" + encodeURIComponent(remote.name), { method: "DELETE" })
|
|
1817
|
+
.then(function () { showToast("Disconnected " + remote.name, "success"); return Promise.all([loadRemotes(), loadHosts()]); })
|
|
1818
|
+
.catch(function (err) { apiError(err, "Couldn't disconnect"); disconnectBtn.disabled = false; });
|
|
1819
|
+
});
|
|
1671
1820
|
});
|
|
1672
1821
|
actions.appendChild(disconnectBtn);
|
|
1673
1822
|
item.appendChild(actions);
|
|
@@ -1718,11 +1867,13 @@ const clientJs = `
|
|
|
1718
1867
|
delBtn.className = "btn btn-sm btn-danger";
|
|
1719
1868
|
delBtn.textContent = "Delete";
|
|
1720
1869
|
delBtn.addEventListener("click", function () {
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1870
|
+
confirmDialog({ title: "Delete " + h.host + " on " + remote.name + "?", message: "This can't be undone.", confirmLabel: "Delete", danger: true }).then(function (ok) {
|
|
1871
|
+
if (!ok) return;
|
|
1872
|
+
delBtn.disabled = true;
|
|
1873
|
+
apiFetch("/api/remotes/" + encodeURIComponent(remote.name) + "/hosts/" + encodeURIComponent(h.host), { method: "DELETE" })
|
|
1874
|
+
.then(function () { showToast("Deleted " + h.host, "success"); return Promise.all([loadRemotes(), loadHosts()]); })
|
|
1875
|
+
.catch(function (err) { apiError(err, "Couldn't delete host"); delBtn.disabled = false; });
|
|
1876
|
+
});
|
|
1726
1877
|
});
|
|
1727
1878
|
hostRow.appendChild(delBtn);
|
|
1728
1879
|
}
|
|
@@ -1961,29 +2112,31 @@ const clientJs = `
|
|
|
1961
2112
|
|
|
1962
2113
|
stopProxyBtn.addEventListener("click", function () {
|
|
1963
2114
|
if (proxyStopped) return;
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
2115
|
+
confirmDialog({ title: "Stop the locadot proxy?", message: "Run \\"locadot start\\" to bring it back up.", confirmLabel: "Stop proxy", danger: true }).then(function (ok) {
|
|
2116
|
+
if (!ok) return;
|
|
2117
|
+
stopProxyBtn.disabled = true;
|
|
2118
|
+
stopProxyBtn.classList.add("busy");
|
|
2119
|
+
apiFetch("/api/proxy/stop", { method: "POST" })
|
|
2120
|
+
.then(function () {
|
|
2121
|
+
proxyStopped = true;
|
|
2122
|
+
clear(stoppedBanner);
|
|
2123
|
+
stoppedBanner.appendChild(document.createTextNode("Proxy stopped. Run "));
|
|
2124
|
+
var code = document.createElement("code");
|
|
2125
|
+
code.className = "mono";
|
|
2126
|
+
code.textContent = "locadot start";
|
|
2127
|
+
stoppedBanner.appendChild(code);
|
|
2128
|
+
stoppedBanner.appendChild(document.createTextNode(" to bring it back."));
|
|
2129
|
+
stoppedBanner.hidden = false;
|
|
2130
|
+
if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
|
|
2131
|
+
if (remotesPollTimer) { clearInterval(remotesPollTimer); remotesPollTimer = null; }
|
|
2132
|
+
showToast("Proxy stopped", "warn");
|
|
2133
|
+
})
|
|
2134
|
+
.catch(function (err) {
|
|
2135
|
+
apiError(err, "Couldn't stop the proxy");
|
|
2136
|
+
stopProxyBtn.disabled = false;
|
|
2137
|
+
stopProxyBtn.classList.remove("busy");
|
|
2138
|
+
});
|
|
2139
|
+
});
|
|
1987
2140
|
});
|
|
1988
2141
|
|
|
1989
2142
|
cfInstallBtn.addEventListener("click", function () {
|
|
@@ -2132,12 +2285,36 @@ const clientJs = `
|
|
|
2132
2285
|
link.textContent = t.url.replace("https://", "");
|
|
2133
2286
|
link.title = t.url;
|
|
2134
2287
|
statusWrap.appendChild(link);
|
|
2288
|
+
if (t.mode === "custom") {
|
|
2289
|
+
var modeBadge = document.createElement("span");
|
|
2290
|
+
modeBadge.className = "badge badge-accent";
|
|
2291
|
+
modeBadge.textContent = "custom";
|
|
2292
|
+
statusWrap.appendChild(modeBadge);
|
|
2293
|
+
}
|
|
2135
2294
|
statusWrap.appendChild(makeCopyButton(function () { return t.url; }));
|
|
2136
2295
|
} else if (t.status === "starting") {
|
|
2137
2296
|
var spin = document.createElement("span");
|
|
2138
2297
|
spin.className = "spinner";
|
|
2139
2298
|
statusWrap.appendChild(spin);
|
|
2140
2299
|
statusWrap.appendChild(document.createTextNode("Starting\\u2026"));
|
|
2300
|
+
} else if (t.status === "login") {
|
|
2301
|
+
var loginDot = document.createElement("span");
|
|
2302
|
+
loginDot.className = "dot warn";
|
|
2303
|
+
statusWrap.appendChild(loginDot);
|
|
2304
|
+
var loginText = document.createElement("span");
|
|
2305
|
+
loginText.className = "tunnel-login";
|
|
2306
|
+
var loginLabel = document.createElement("span");
|
|
2307
|
+
loginLabel.textContent = "Waiting for Cloudflare login";
|
|
2308
|
+
loginText.appendChild(loginLabel);
|
|
2309
|
+
if (t.loginUrl) {
|
|
2310
|
+
var loginLink = document.createElement("a");
|
|
2311
|
+
loginLink.href = t.loginUrl;
|
|
2312
|
+
loginLink.target = "_blank";
|
|
2313
|
+
loginLink.rel = "noopener noreferrer";
|
|
2314
|
+
loginLink.textContent = "Log in to Cloudflare \u2197";
|
|
2315
|
+
loginText.appendChild(loginLink);
|
|
2316
|
+
}
|
|
2317
|
+
statusWrap.appendChild(loginText);
|
|
2141
2318
|
} else if (t.status === "error") {
|
|
2142
2319
|
var errDot = document.createElement("span");
|
|
2143
2320
|
errDot.className = "dot down";
|
|
@@ -2159,35 +2336,392 @@ const clientJs = `
|
|
|
2159
2336
|
|
|
2160
2337
|
function shareButton(row) {
|
|
2161
2338
|
var t = tunnelInfo(row);
|
|
2162
|
-
var isOn = t.enabled || t.status === "up" || t.status === "starting";
|
|
2339
|
+
var isOn = t.enabled || t.status === "up" || t.status === "starting" || t.status === "login";
|
|
2163
2340
|
var isRetry = !isOn && t.status === "error";
|
|
2164
2341
|
var shareBtn = document.createElement("button");
|
|
2165
2342
|
shareBtn.type = "button";
|
|
2166
2343
|
shareBtn.className = "btn btn-sm" + (isOn ? " btn-danger" : "");
|
|
2167
2344
|
shareBtn.textContent = isOn ? "Unshare" : (isRetry ? "Retry" : "Share");
|
|
2168
|
-
shareBtn.title = isOn ? "Stop the public tunnel" :
|
|
2345
|
+
shareBtn.title = isOn ? "Stop the public tunnel" : "Share on a public URL";
|
|
2169
2346
|
if (!cloudflaredInstalled && !isOn) {
|
|
2170
2347
|
shareBtn.disabled = true;
|
|
2171
2348
|
shareBtn.title = "Install cloudflared first";
|
|
2172
2349
|
}
|
|
2173
2350
|
shareBtn.addEventListener("click", function () {
|
|
2174
|
-
if (
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2351
|
+
if (isOn) {
|
|
2352
|
+
confirmDialog({ title: "Stop sharing " + row.host + "?", message: "The public URL for " + row.host + " stops working.", confirmLabel: "Stop sharing", danger: true }).then(function (ok) {
|
|
2353
|
+
if (!ok) return;
|
|
2354
|
+
shareBtn.disabled = true;
|
|
2355
|
+
shareBtn.classList.add("busy");
|
|
2356
|
+
apiFetch("/api/hosts/" + encodeURIComponent(row.host), { method: "PUT", body: JSON.stringify({ tunnel: false }) })
|
|
2357
|
+
.then(function () {
|
|
2358
|
+
showToast("Stopping tunnel for " + row.host, "success");
|
|
2359
|
+
return loadHosts();
|
|
2360
|
+
})
|
|
2361
|
+
.catch(function (err) {
|
|
2362
|
+
apiError(err, "Couldn't change sharing for " + row.host);
|
|
2363
|
+
shareBtn.disabled = false;
|
|
2364
|
+
shareBtn.classList.remove("busy");
|
|
2365
|
+
});
|
|
2186
2366
|
});
|
|
2367
|
+
} else {
|
|
2368
|
+
openShareDialog(row);
|
|
2369
|
+
}
|
|
2187
2370
|
});
|
|
2188
2371
|
return shareBtn;
|
|
2189
2372
|
}
|
|
2190
2373
|
|
|
2374
|
+
// ---------- share dialog ----------
|
|
2375
|
+
|
|
2376
|
+
var shareDialogEl = document.getElementById("share-dialog");
|
|
2377
|
+
var shareHostEl = document.getElementById("share-host");
|
|
2378
|
+
var shareCloseBtn = document.getElementById("share-close");
|
|
2379
|
+
var shareCancelBtn = document.getElementById("share-cancel");
|
|
2380
|
+
var shareSubmitBtn = document.getElementById("share-submit");
|
|
2381
|
+
var shareOptionRandom = document.getElementById("share-option-random");
|
|
2382
|
+
var shareOptionCustom = document.getElementById("share-option-custom");
|
|
2383
|
+
var shareCustomExtra = document.getElementById("share-custom-extra");
|
|
2384
|
+
var shareDomainInput = document.getElementById("share-domain-input");
|
|
2385
|
+
var shareDomainError = document.getElementById("share-domain-error");
|
|
2386
|
+
var shareDomainHint = document.getElementById("share-domain-hint");
|
|
2387
|
+
var shareErrorEl = document.getElementById("share-error");
|
|
2388
|
+
var shareRow = null;
|
|
2389
|
+
var shareMode = "random";
|
|
2390
|
+
var HOSTNAME_RE = /^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z]{2,}$/;
|
|
2391
|
+
|
|
2392
|
+
function normalizeHostname(value) {
|
|
2393
|
+
return String(value || "").trim().toLowerCase().replace(/^https?:\\/\\//, "").replace(/\\/$/, "");
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
function validateHostname(raw) {
|
|
2397
|
+
var value = normalizeHostname(raw);
|
|
2398
|
+
if (!value) return "Enter a hostname.";
|
|
2399
|
+
if (value.slice(-10) === ".localhost") return "Hostname can't be a .localhost address.";
|
|
2400
|
+
if (!HOSTNAME_RE.test(value)) return "Enter a valid hostname, e.g. app.example.com.";
|
|
2401
|
+
return null;
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
function setShareMode(mode) {
|
|
2405
|
+
shareMode = mode;
|
|
2406
|
+
shareOptionRandom.setAttribute("aria-checked", String(mode === "random"));
|
|
2407
|
+
shareOptionCustom.setAttribute("aria-checked", String(mode === "custom"));
|
|
2408
|
+
shareCustomExtra.hidden = mode !== "custom";
|
|
2409
|
+
shareDomainHint.textContent = cloudflareLoggedIn
|
|
2410
|
+
? "The DNS record is created or updated for you."
|
|
2411
|
+
: "You'll be asked to log in to Cloudflare once; the login link appears on the row.";
|
|
2412
|
+
shareDomainError.hidden = true;
|
|
2413
|
+
shareDomainError.textContent = "";
|
|
2414
|
+
shareErrorEl.hidden = true;
|
|
2415
|
+
shareErrorEl.textContent = "";
|
|
2416
|
+
if (mode === "custom") setTimeout(function () { shareDomainInput.focus(); }, 0);
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
function closeShareDialog() {
|
|
2420
|
+
shareRow = null;
|
|
2421
|
+
shareDialogEl.close();
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
function openShareDialog(row) {
|
|
2425
|
+
shareRow = row;
|
|
2426
|
+
shareHostEl.textContent = row.host;
|
|
2427
|
+
shareErrorEl.hidden = true;
|
|
2428
|
+
shareErrorEl.textContent = "";
|
|
2429
|
+
shareDomainError.hidden = true;
|
|
2430
|
+
shareDomainError.textContent = "";
|
|
2431
|
+
var hasDomain = typeof row.tunnelDomain === "string" && !!row.tunnelDomain;
|
|
2432
|
+
shareDomainInput.value = hasDomain ? row.tunnelDomain : "";
|
|
2433
|
+
setShareMode(hasDomain ? "custom" : "random");
|
|
2434
|
+
shareSubmitBtn.disabled = false;
|
|
2435
|
+
shareSubmitBtn.classList.remove("busy");
|
|
2436
|
+
shareCancelBtn.disabled = false;
|
|
2437
|
+
shareDialogEl.showModal();
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
shareOptionRandom.addEventListener("click", function () { setShareMode("random"); });
|
|
2441
|
+
shareOptionCustom.addEventListener("click", function () { setShareMode("custom"); });
|
|
2442
|
+
shareOptionRandom.addEventListener("keydown", function (e) {
|
|
2443
|
+
if (e.key === "ArrowDown" || e.key === "ArrowRight") { e.preventDefault(); setShareMode("custom"); shareOptionCustom.focus(); }
|
|
2444
|
+
});
|
|
2445
|
+
shareOptionCustom.addEventListener("keydown", function (e) {
|
|
2446
|
+
if (e.key === "ArrowUp" || e.key === "ArrowLeft") { e.preventDefault(); setShareMode("random"); shareOptionRandom.focus(); }
|
|
2447
|
+
});
|
|
2448
|
+
shareCancelBtn.addEventListener("click", closeShareDialog);
|
|
2449
|
+
shareCloseBtn.addEventListener("click", closeShareDialog);
|
|
2450
|
+
shareDialogEl.addEventListener("click", function (e) {
|
|
2451
|
+
if (e.target === shareDialogEl) closeShareDialog();
|
|
2452
|
+
});
|
|
2453
|
+
shareDomainInput.addEventListener("keydown", function (e) {
|
|
2454
|
+
if (e.key === "Enter") { e.preventDefault(); shareSubmitBtn.click(); }
|
|
2455
|
+
});
|
|
2456
|
+
|
|
2457
|
+
shareSubmitBtn.addEventListener("click", function () {
|
|
2458
|
+
if (!shareRow) return;
|
|
2459
|
+
var row = shareRow;
|
|
2460
|
+
var body = { tunnel: true };
|
|
2461
|
+
if (shareMode === "custom") {
|
|
2462
|
+
var err = validateHostname(shareDomainInput.value);
|
|
2463
|
+
if (err) {
|
|
2464
|
+
shareDomainError.textContent = err;
|
|
2465
|
+
shareDomainError.hidden = false;
|
|
2466
|
+
return;
|
|
2467
|
+
}
|
|
2468
|
+
body.domain = normalizeHostname(shareDomainInput.value);
|
|
2469
|
+
}
|
|
2470
|
+
shareSubmitBtn.disabled = true;
|
|
2471
|
+
shareSubmitBtn.classList.add("busy");
|
|
2472
|
+
shareCancelBtn.disabled = true;
|
|
2473
|
+
apiFetch("/api/hosts/" + encodeURIComponent(row.host), { method: "PUT", body: JSON.stringify(body) })
|
|
2474
|
+
.then(function () {
|
|
2475
|
+
showToast("Starting tunnel for " + row.host, "success");
|
|
2476
|
+
closeShareDialog();
|
|
2477
|
+
return loadHosts();
|
|
2478
|
+
})
|
|
2479
|
+
.catch(function (err2) {
|
|
2480
|
+
shareErrorEl.textContent = (err2 && err2.message) ? err2.message : String(err2);
|
|
2481
|
+
shareErrorEl.hidden = false;
|
|
2482
|
+
})
|
|
2483
|
+
.then(function () {
|
|
2484
|
+
shareSubmitBtn.disabled = false;
|
|
2485
|
+
shareSubmitBtn.classList.remove("busy");
|
|
2486
|
+
shareCancelBtn.disabled = false;
|
|
2487
|
+
});
|
|
2488
|
+
});
|
|
2489
|
+
|
|
2490
|
+
var allowDialog = document.getElementById("allow-dialog");
|
|
2491
|
+
var allowHostEl = document.getElementById("allow-host");
|
|
2492
|
+
var allowWarnEl = document.getElementById("allow-warn");
|
|
2493
|
+
var allowListEl = document.getElementById("allow-list");
|
|
2494
|
+
var allowInput = document.getElementById("allow-input");
|
|
2495
|
+
var allowAddBtn = document.getElementById("allow-add");
|
|
2496
|
+
var allowErrorEl = document.getElementById("allow-error");
|
|
2497
|
+
var allowCancelBtn = document.getElementById("allow-cancel");
|
|
2498
|
+
var allowSaveBtn = document.getElementById("allow-save");
|
|
2499
|
+
var allowCloseBtn = document.getElementById("allow-close");
|
|
2500
|
+
var allowOriginal = [];
|
|
2501
|
+
var allowCurrent = [];
|
|
2502
|
+
var allowRow = null;
|
|
2503
|
+
|
|
2504
|
+
function allowListsEqual(a, b) {
|
|
2505
|
+
if (a.length !== b.length) return false;
|
|
2506
|
+
for (var i = 0; i < a.length; i++) {
|
|
2507
|
+
if (a[i] !== b[i]) return false;
|
|
2508
|
+
}
|
|
2509
|
+
return true;
|
|
2510
|
+
}
|
|
2511
|
+
|
|
2512
|
+
function updateAllowSave() {
|
|
2513
|
+
allowSaveBtn.disabled = allowListsEqual(allowCurrent, allowOriginal);
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
function renderAllowList() {
|
|
2517
|
+
clear(allowListEl);
|
|
2518
|
+
if (!allowCurrent.length) {
|
|
2519
|
+
var empty = document.createElement("div");
|
|
2520
|
+
empty.className = "dim allow-empty";
|
|
2521
|
+
empty.textContent = "No internal addresses. Shared visitors reach public hosts only.";
|
|
2522
|
+
allowListEl.appendChild(empty);
|
|
2523
|
+
return;
|
|
2524
|
+
}
|
|
2525
|
+
allowCurrent.forEach(function (entry) {
|
|
2526
|
+
var entryRow = document.createElement("div");
|
|
2527
|
+
entryRow.className = "allow-row";
|
|
2528
|
+
var text = document.createElement("span");
|
|
2529
|
+
text.className = "mono";
|
|
2530
|
+
text.textContent = entry;
|
|
2531
|
+
entryRow.appendChild(text);
|
|
2532
|
+
var removeBtn = document.createElement("button");
|
|
2533
|
+
removeBtn.type = "button";
|
|
2534
|
+
removeBtn.className = "allow-remove";
|
|
2535
|
+
removeBtn.textContent = "\\u00d7";
|
|
2536
|
+
removeBtn.setAttribute("aria-label", "Remove " + entry);
|
|
2537
|
+
removeBtn.addEventListener("click", function () {
|
|
2538
|
+
allowCurrent = allowCurrent.filter(function (item) { return item !== entry; });
|
|
2539
|
+
renderAllowList();
|
|
2540
|
+
updateAllowSave();
|
|
2541
|
+
});
|
|
2542
|
+
entryRow.appendChild(removeBtn);
|
|
2543
|
+
allowListEl.appendChild(entryRow);
|
|
2544
|
+
});
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
function allowAddEntry() {
|
|
2548
|
+
var value = allowInput.value.trim().toLowerCase().replace(/^[a-z][a-z0-9+.-]*:\\/\\//, "").replace(/\\/$/, "");
|
|
2549
|
+
allowErrorEl.hidden = true;
|
|
2550
|
+
allowErrorEl.textContent = "";
|
|
2551
|
+
if (!value || /\\s/.test(value)) {
|
|
2552
|
+
allowErrorEl.textContent = "Enter a host or host:port, e.g. localhost:3000.";
|
|
2553
|
+
allowErrorEl.hidden = false;
|
|
2554
|
+
return;
|
|
2555
|
+
}
|
|
2556
|
+
if (allowCurrent.indexOf(value) !== -1) {
|
|
2557
|
+
allowErrorEl.textContent = value + " is already in the list.";
|
|
2558
|
+
allowErrorEl.hidden = false;
|
|
2559
|
+
return;
|
|
2560
|
+
}
|
|
2561
|
+
allowCurrent.push(value);
|
|
2562
|
+
allowInput.value = "";
|
|
2563
|
+
renderAllowList();
|
|
2564
|
+
updateAllowSave();
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
function closeAllowDialog() {
|
|
2568
|
+
allowDialog.close();
|
|
2569
|
+
}
|
|
2570
|
+
|
|
2571
|
+
function openAllowDialog(row) {
|
|
2572
|
+
allowRow = row;
|
|
2573
|
+
allowOriginal = (row.allow || []).slice();
|
|
2574
|
+
allowCurrent = allowOriginal.slice();
|
|
2575
|
+
allowHostEl.textContent = row.host;
|
|
2576
|
+
allowWarnEl.hidden = !!row.cors;
|
|
2577
|
+
allowErrorEl.hidden = true;
|
|
2578
|
+
allowErrorEl.textContent = "";
|
|
2579
|
+
allowInput.value = "";
|
|
2580
|
+
renderAllowList();
|
|
2581
|
+
allowSaveBtn.disabled = true;
|
|
2582
|
+
allowSaveBtn.classList.remove("busy");
|
|
2583
|
+
allowCancelBtn.disabled = false;
|
|
2584
|
+
allowDialog.showModal();
|
|
2585
|
+
setTimeout(function () { allowInput.focus(); }, 0);
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2588
|
+
allowAddBtn.addEventListener("click", allowAddEntry);
|
|
2589
|
+
allowInput.addEventListener("keydown", function (e) {
|
|
2590
|
+
if (e.key === "Enter") { e.preventDefault(); allowAddEntry(); }
|
|
2591
|
+
});
|
|
2592
|
+
allowCancelBtn.addEventListener("click", closeAllowDialog);
|
|
2593
|
+
allowCloseBtn.addEventListener("click", closeAllowDialog);
|
|
2594
|
+
allowDialog.addEventListener("click", function (e) {
|
|
2595
|
+
if (e.target === allowDialog) closeAllowDialog();
|
|
2596
|
+
});
|
|
2597
|
+
|
|
2598
|
+
allowSaveBtn.addEventListener("click", function () {
|
|
2599
|
+
if (!allowRow) return;
|
|
2600
|
+
var host = allowRow.host;
|
|
2601
|
+
allowSaveBtn.disabled = true;
|
|
2602
|
+
allowSaveBtn.classList.add("busy");
|
|
2603
|
+
allowCancelBtn.disabled = true;
|
|
2604
|
+
apiFetch("/api/hosts/" + encodeURIComponent(host), { method: "PUT", body: JSON.stringify({ allow: allowCurrent }) })
|
|
2605
|
+
.then(function () {
|
|
2606
|
+
showToast("Saved internal access for " + host, "success");
|
|
2607
|
+
closeAllowDialog();
|
|
2608
|
+
return loadHosts();
|
|
2609
|
+
})
|
|
2610
|
+
.catch(function (err) {
|
|
2611
|
+
allowErrorEl.textContent = err && err.message ? err.message : String(err);
|
|
2612
|
+
allowErrorEl.hidden = false;
|
|
2613
|
+
allowSaveBtn.disabled = false;
|
|
2614
|
+
allowSaveBtn.classList.remove("busy");
|
|
2615
|
+
allowCancelBtn.disabled = false;
|
|
2616
|
+
});
|
|
2617
|
+
});
|
|
2618
|
+
|
|
2619
|
+
// ---------- confirm / prompt dialogs ----------
|
|
2620
|
+
|
|
2621
|
+
var confirmDialogEl = document.getElementById("confirm-dialog");
|
|
2622
|
+
var confirmTitleEl = document.getElementById("confirm-title");
|
|
2623
|
+
var confirmMessageEl = document.getElementById("confirm-message");
|
|
2624
|
+
var confirmCancelBtn = document.getElementById("confirm-cancel");
|
|
2625
|
+
var confirmOkBtn = document.getElementById("confirm-ok");
|
|
2626
|
+
var confirmCloseBtn = document.getElementById("confirm-close");
|
|
2627
|
+
var confirmResolve = null;
|
|
2628
|
+
|
|
2629
|
+
function closeConfirmDialog(result) {
|
|
2630
|
+
var resolve = confirmResolve;
|
|
2631
|
+
confirmResolve = null;
|
|
2632
|
+
confirmDialogEl.close();
|
|
2633
|
+
if (resolve) resolve(result);
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
function confirmDialog(opts) {
|
|
2637
|
+
opts = opts || {};
|
|
2638
|
+
return new Promise(function (resolve) {
|
|
2639
|
+
confirmResolve = resolve;
|
|
2640
|
+
confirmTitleEl.textContent = opts.title || "Are you sure?";
|
|
2641
|
+
confirmMessageEl.textContent = opts.message || "";
|
|
2642
|
+
confirmOkBtn.textContent = opts.confirmLabel || "OK";
|
|
2643
|
+
confirmOkBtn.className = "btn " + (opts.danger ? "btn-danger" : "btn-primary");
|
|
2644
|
+
confirmDialogEl.showModal();
|
|
2645
|
+
setTimeout(function () { confirmOkBtn.focus(); }, 0);
|
|
2646
|
+
});
|
|
2647
|
+
}
|
|
2648
|
+
|
|
2649
|
+
confirmCancelBtn.addEventListener("click", function () { closeConfirmDialog(false); });
|
|
2650
|
+
confirmCloseBtn.addEventListener("click", function () { closeConfirmDialog(false); });
|
|
2651
|
+
confirmOkBtn.addEventListener("click", function () { closeConfirmDialog(true); });
|
|
2652
|
+
confirmDialogEl.addEventListener("click", function (e) {
|
|
2653
|
+
if (e.target === confirmDialogEl) closeConfirmDialog(false);
|
|
2654
|
+
});
|
|
2655
|
+
confirmDialogEl.addEventListener("close", function () {
|
|
2656
|
+
if (confirmResolve) { var resolve = confirmResolve; confirmResolve = null; resolve(false); }
|
|
2657
|
+
});
|
|
2658
|
+
|
|
2659
|
+
var promptDialogEl = document.getElementById("prompt-dialog");
|
|
2660
|
+
var promptTitleEl = document.getElementById("prompt-title");
|
|
2661
|
+
var promptMessageEl = document.getElementById("prompt-message");
|
|
2662
|
+
var promptLabelEl = document.getElementById("prompt-label");
|
|
2663
|
+
var promptInput = document.getElementById("prompt-input");
|
|
2664
|
+
var promptErrorEl = document.getElementById("prompt-error");
|
|
2665
|
+
var promptCancelBtn = document.getElementById("prompt-cancel");
|
|
2666
|
+
var promptConfirmBtn = document.getElementById("prompt-confirm");
|
|
2667
|
+
var promptCloseBtn = document.getElementById("prompt-close");
|
|
2668
|
+
var promptResolve = null;
|
|
2669
|
+
var promptValidate = null;
|
|
2670
|
+
|
|
2671
|
+
function closePromptDialog(result) {
|
|
2672
|
+
var resolve = promptResolve;
|
|
2673
|
+
promptResolve = null;
|
|
2674
|
+
promptValidate = null;
|
|
2675
|
+
promptDialogEl.close();
|
|
2676
|
+
if (resolve) resolve(result);
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
function promptDialog(opts) {
|
|
2680
|
+
opts = opts || {};
|
|
2681
|
+
return new Promise(function (resolve) {
|
|
2682
|
+
promptResolve = resolve;
|
|
2683
|
+
promptValidate = typeof opts.validate === "function" ? opts.validate : null;
|
|
2684
|
+
promptTitleEl.textContent = opts.title || "";
|
|
2685
|
+
promptMessageEl.textContent = opts.message || "";
|
|
2686
|
+
promptMessageEl.hidden = !opts.message;
|
|
2687
|
+
promptLabelEl.textContent = opts.label || "";
|
|
2688
|
+
promptLabelEl.hidden = !opts.label;
|
|
2689
|
+
promptInput.placeholder = opts.placeholder || "";
|
|
2690
|
+
promptInput.value = opts.value || "";
|
|
2691
|
+
promptConfirmBtn.textContent = opts.confirmLabel || "OK";
|
|
2692
|
+
promptErrorEl.hidden = true;
|
|
2693
|
+
promptErrorEl.textContent = "";
|
|
2694
|
+
promptDialogEl.showModal();
|
|
2695
|
+
setTimeout(function () { promptInput.focus(); promptInput.select(); }, 0);
|
|
2696
|
+
});
|
|
2697
|
+
}
|
|
2698
|
+
|
|
2699
|
+
function submitPromptDialog() {
|
|
2700
|
+
var value = promptInput.value;
|
|
2701
|
+
if (promptValidate) {
|
|
2702
|
+
var err = promptValidate(value);
|
|
2703
|
+
if (err) {
|
|
2704
|
+
promptErrorEl.textContent = err;
|
|
2705
|
+
promptErrorEl.hidden = false;
|
|
2706
|
+
return;
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
closePromptDialog(value);
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
promptCancelBtn.addEventListener("click", function () { closePromptDialog(null); });
|
|
2713
|
+
promptCloseBtn.addEventListener("click", function () { closePromptDialog(null); });
|
|
2714
|
+
promptConfirmBtn.addEventListener("click", submitPromptDialog);
|
|
2715
|
+
promptInput.addEventListener("keydown", function (e) {
|
|
2716
|
+
if (e.key === "Enter") { e.preventDefault(); submitPromptDialog(); }
|
|
2717
|
+
});
|
|
2718
|
+
promptDialogEl.addEventListener("click", function (e) {
|
|
2719
|
+
if (e.target === promptDialogEl) closePromptDialog(null);
|
|
2720
|
+
});
|
|
2721
|
+
promptDialogEl.addEventListener("close", function () {
|
|
2722
|
+
if (promptResolve) { var resolve = promptResolve; promptResolve = null; resolve(null); }
|
|
2723
|
+
});
|
|
2724
|
+
|
|
2191
2725
|
function labelCells(tr, labels) {
|
|
2192
2726
|
for (var i = 0; i < tr.children.length; i++) tr.children[i].setAttribute("data-label", labels[i] || "");
|
|
2193
2727
|
}
|
|
@@ -2228,13 +2762,14 @@ const clientJs = `
|
|
|
2228
2762
|
|
|
2229
2763
|
var optionsTd = document.createElement("td");
|
|
2230
2764
|
var flags = [];
|
|
2231
|
-
if (row.insecure) flags.push(["insecure", "Upstream TLS certificate is not verified"]);
|
|
2232
|
-
if (row.cors) flags.push(["cors", "Origin/Referer rewritten to the target; any origin may call this host"]);
|
|
2765
|
+
if (row.insecure) flags.push(["insecure", "Upstream TLS certificate is not verified", "badge-warn"]);
|
|
2766
|
+
if (row.cors) flags.push(["cors", "Origin/Referer rewritten to the target; any origin may call this host", "badge-cors"]);
|
|
2767
|
+
if (row.allow && row.allow.length) flags.push(["allow " + row.allow.length, row.allow.join(", "), "badge-accent"]);
|
|
2233
2768
|
if (flags.length) {
|
|
2234
2769
|
optionsTd.className = "badges";
|
|
2235
2770
|
flags.forEach(function (flag) {
|
|
2236
2771
|
var badge = document.createElement("span");
|
|
2237
|
-
badge.className = "badge " +
|
|
2772
|
+
badge.className = "badge " + flag[2];
|
|
2238
2773
|
badge.textContent = flag[0];
|
|
2239
2774
|
badge.title = flag[1];
|
|
2240
2775
|
optionsTd.appendChild(badge);
|
|
@@ -2265,6 +2800,20 @@ const clientJs = `
|
|
|
2265
2800
|
var actionsWrap = document.createElement("div");
|
|
2266
2801
|
actionsWrap.className = "row-actions";
|
|
2267
2802
|
actionsTd.appendChild(actionsWrap);
|
|
2803
|
+
var accessBtn = document.createElement("button");
|
|
2804
|
+
accessBtn.type = "button";
|
|
2805
|
+
var allowCount = row.allow ? row.allow.length : 0;
|
|
2806
|
+
accessBtn.className = "btn btn-sm btn-icon" + (allowCount ? " is-set" : "");
|
|
2807
|
+
accessBtn.title = allowCount ? "Internal access: " + row.allow.join(", ") : "Internal access: let shared visitors reach addresses such as localhost:3000";
|
|
2808
|
+
accessBtn.setAttribute("aria-label", "Internal access for " + row.host);
|
|
2809
|
+
var accessSvg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
2810
|
+
accessSvg.setAttribute("viewBox", "0 0 24 24");
|
|
2811
|
+
accessSvg.setAttribute("aria-hidden", "true");
|
|
2812
|
+
var accessPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
2813
|
+
accessPath.setAttribute("d", "M12 3l7 3v5c0 4.5-3 8.3-7 10-4-1.7-7-5.5-7-10V6l7-3zM9.5 12l1.8 1.8L15 10");
|
|
2814
|
+
accessSvg.appendChild(accessPath);
|
|
2815
|
+
accessBtn.appendChild(accessSvg);
|
|
2816
|
+
accessBtn.addEventListener("click", function () { openAllowDialog(row); });
|
|
2268
2817
|
var editBtn = document.createElement("button");
|
|
2269
2818
|
editBtn.type = "button";
|
|
2270
2819
|
editBtn.className = "btn btn-sm";
|
|
@@ -2275,20 +2824,23 @@ const clientJs = `
|
|
|
2275
2824
|
removeBtn.className = "btn btn-sm btn-danger";
|
|
2276
2825
|
removeBtn.textContent = "Remove";
|
|
2277
2826
|
removeBtn.addEventListener("click", function () {
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2827
|
+
(prefs.confirm ? confirmDialog({ title: "Remove host " + row.host + "?", message: "This can't be undone.", confirmLabel: "Remove host", danger: true }) : Promise.resolve(true)).then(function (ok) {
|
|
2828
|
+
if (!ok) return;
|
|
2829
|
+
removeBtn.disabled = true;
|
|
2830
|
+
apiFetch("/api/hosts/" + encodeURIComponent(row.host), { method: "DELETE" })
|
|
2831
|
+
.then(function () {
|
|
2832
|
+
showToast("Removed " + row.host, "success");
|
|
2833
|
+
return loadHosts();
|
|
2834
|
+
})
|
|
2835
|
+
.catch(function (err) {
|
|
2836
|
+
apiError(err, "Couldn't remove " + row.host);
|
|
2837
|
+
removeBtn.disabled = false;
|
|
2838
|
+
});
|
|
2839
|
+
});
|
|
2289
2840
|
});
|
|
2290
2841
|
if (!row.remote) {
|
|
2291
2842
|
actionsWrap.appendChild(shareButton(row));
|
|
2843
|
+
actionsWrap.appendChild(accessBtn);
|
|
2292
2844
|
actionsWrap.appendChild(editBtn);
|
|
2293
2845
|
}
|
|
2294
2846
|
actionsWrap.appendChild(removeBtn);
|
|
@@ -2454,13 +3006,15 @@ const clientJs = `
|
|
|
2454
3006
|
|
|
2455
3007
|
logsRefreshBtn.addEventListener("click", loadLogs);
|
|
2456
3008
|
logsClearBtn.addEventListener("click", function () {
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
3009
|
+
confirmDialog({ title: "Clear logs?", message: "This can't be undone.", confirmLabel: "Clear logs", danger: true }).then(function (ok) {
|
|
3010
|
+
if (!ok) return;
|
|
3011
|
+
apiFetch("/api/logs/clear", { method: "POST" })
|
|
3012
|
+
.then(function () {
|
|
3013
|
+
showToast("Logs cleared", "success");
|
|
3014
|
+
return loadLogs();
|
|
3015
|
+
})
|
|
3016
|
+
.catch(function (err) { apiError(err, "Couldn't clear logs"); });
|
|
3017
|
+
});
|
|
2464
3018
|
});
|
|
2465
3019
|
|
|
2466
3020
|
cliAddCopy.addEventListener("click", function () { copyText(cliAddPre.textContent, cliAddCopy); });
|
|
@@ -2886,7 +3440,8 @@ const clientJs = `
|
|
|
2886
3440
|
});
|
|
2887
3441
|
|
|
2888
3442
|
restartBtn.addEventListener("click", function () {
|
|
2889
|
-
|
|
3443
|
+
confirmDialog({ title: "Restart the proxy?", message: "Hosts are unavailable for a few seconds.", confirmLabel: "Restart", danger: true }).then(function (ok) {
|
|
3444
|
+
if (!ok) return;
|
|
2890
3445
|
var s = settingsLoaded;
|
|
2891
3446
|
var nextPort = s ? ((s.env && s.env.httpPort) ? s.httpPort : ((s.saved && s.saved.httpPort) || s.httpPort)) : null;
|
|
2892
3447
|
restartBtn.disabled = true;
|
|
@@ -2919,6 +3474,7 @@ const clientJs = `
|
|
|
2919
3474
|
restartBtn.disabled = false;
|
|
2920
3475
|
restartBtn.classList.remove("busy");
|
|
2921
3476
|
});
|
|
3477
|
+
});
|
|
2922
3478
|
});
|
|
2923
3479
|
|
|
2924
3480
|
document.getElementById("set-home-copy").addEventListener("click", function (e) { copyText(setHome.textContent, e.currentTarget); });
|
|
@@ -3649,6 +4205,104 @@ function renderPage(nonce, token, uiAuth = false) {
|
|
|
3649
4205
|
<a href="https://github.com/avinashid/locadot" target="_blank" rel="noopener noreferrer">github.com/avinashid/locadot</a>
|
|
3650
4206
|
</footer>
|
|
3651
4207
|
</div>
|
|
4208
|
+
<dialog id="allow-dialog">
|
|
4209
|
+
<div class="modal-head">
|
|
4210
|
+
<div>
|
|
4211
|
+
<h3 class="modal-title">Internal access</h3>
|
|
4212
|
+
<p id="allow-host" class="modal-sub mono"></p>
|
|
4213
|
+
</div>
|
|
4214
|
+
<button type="button" id="allow-close" class="btn btn-ghost modal-close" aria-label="Close"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg></button>
|
|
4215
|
+
</div>
|
|
4216
|
+
<div class="modal-body">
|
|
4217
|
+
<p class="modal-desc">People who open this mapping through a tunnel or remote access can only reach public hosts. Allow the internal addresses this app calls, such as its API on localhost.</p>
|
|
4218
|
+
<div id="allow-warn" class="modal-warn" hidden>Bypass CORS is off for this mapping. These addresses take effect once it's on, because the page reaches them through locadot's pass-through.</div>
|
|
4219
|
+
<div id="allow-list" class="allow-list"></div>
|
|
4220
|
+
<div class="allow-add-row">
|
|
4221
|
+
<input type="text" id="allow-input" class="input" placeholder="localhost:3000" aria-label="Address" autocomplete="off">
|
|
4222
|
+
<button type="button" id="allow-add" class="btn btn-sm">Add</button>
|
|
4223
|
+
</div>
|
|
4224
|
+
<p class="modal-hint">host:port, or a host alone for any port. locadot's own ports are always blocked.</p>
|
|
4225
|
+
<div id="allow-error" class="field-error" role="alert" hidden></div>
|
|
4226
|
+
</div>
|
|
4227
|
+
<div class="modal-foot">
|
|
4228
|
+
<button type="button" id="allow-cancel" class="btn">Cancel</button>
|
|
4229
|
+
<button type="button" id="allow-save" class="btn btn-primary" disabled>Save changes</button>
|
|
4230
|
+
</div>
|
|
4231
|
+
</dialog>
|
|
4232
|
+
<dialog id="confirm-dialog">
|
|
4233
|
+
<div class="modal-head">
|
|
4234
|
+
<div>
|
|
4235
|
+
<h3 class="modal-title" id="confirm-title"></h3>
|
|
4236
|
+
</div>
|
|
4237
|
+
<button type="button" id="confirm-close" class="btn btn-ghost modal-close" aria-label="Close"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg></button>
|
|
4238
|
+
</div>
|
|
4239
|
+
<div class="modal-body">
|
|
4240
|
+
<p class="modal-desc" id="confirm-message"></p>
|
|
4241
|
+
</div>
|
|
4242
|
+
<div class="modal-foot">
|
|
4243
|
+
<button type="button" id="confirm-cancel" class="btn">Cancel</button>
|
|
4244
|
+
<button type="button" id="confirm-ok" class="btn btn-primary">OK</button>
|
|
4245
|
+
</div>
|
|
4246
|
+
</dialog>
|
|
4247
|
+
<dialog id="prompt-dialog">
|
|
4248
|
+
<div class="modal-head">
|
|
4249
|
+
<div>
|
|
4250
|
+
<h3 class="modal-title" id="prompt-title"></h3>
|
|
4251
|
+
</div>
|
|
4252
|
+
<button type="button" id="prompt-close" class="btn btn-ghost modal-close" aria-label="Close"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg></button>
|
|
4253
|
+
</div>
|
|
4254
|
+
<div class="modal-body">
|
|
4255
|
+
<p class="modal-desc" id="prompt-message"></p>
|
|
4256
|
+
<div class="field">
|
|
4257
|
+
<label id="prompt-label" for="prompt-input"></label>
|
|
4258
|
+
<input type="text" id="prompt-input" class="input" autocomplete="off">
|
|
4259
|
+
</div>
|
|
4260
|
+
<div id="prompt-error" class="field-error" role="alert" hidden></div>
|
|
4261
|
+
</div>
|
|
4262
|
+
<div class="modal-foot">
|
|
4263
|
+
<button type="button" id="prompt-cancel" class="btn">Cancel</button>
|
|
4264
|
+
<button type="button" id="prompt-confirm" class="btn btn-primary">OK</button>
|
|
4265
|
+
</div>
|
|
4266
|
+
</dialog>
|
|
4267
|
+
<dialog id="share-dialog">
|
|
4268
|
+
<div class="modal-head">
|
|
4269
|
+
<div>
|
|
4270
|
+
<h3 class="modal-title">Share publicly</h3>
|
|
4271
|
+
<p id="share-host" class="modal-sub mono"></p>
|
|
4272
|
+
</div>
|
|
4273
|
+
<button type="button" id="share-close" class="btn btn-ghost modal-close" aria-label="Close"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg></button>
|
|
4274
|
+
</div>
|
|
4275
|
+
<div class="modal-body">
|
|
4276
|
+
<p class="modal-desc">Anyone with the link will be able to reach this host, without signing in.</p>
|
|
4277
|
+
<div id="share-options" class="share-options" role="radiogroup" aria-label="Sharing method">
|
|
4278
|
+
<button type="button" id="share-option-random" class="option-card" role="radio" aria-checked="false">
|
|
4279
|
+
<span class="option-dot" aria-hidden="true"></span>
|
|
4280
|
+
<span class="option-body">
|
|
4281
|
+
<span class="option-title">Random URL</span>
|
|
4282
|
+
<span class="option-desc">https://<random>.trycloudflare.com · No account needed. Changes each time sharing restarts.</span>
|
|
4283
|
+
</span>
|
|
4284
|
+
</button>
|
|
4285
|
+
<button type="button" id="share-option-custom" class="option-card" role="radio" aria-checked="false">
|
|
4286
|
+
<span class="option-dot" aria-hidden="true"></span>
|
|
4287
|
+
<span class="option-body">
|
|
4288
|
+
<span class="option-title">Custom domain</span>
|
|
4289
|
+
<span class="option-desc">Your own hostname on a domain in your Cloudflare account. Stable URL.</span>
|
|
4290
|
+
<span id="share-custom-extra" class="option-extra" hidden>
|
|
4291
|
+
<label for="share-domain-input" class="dim">Hostname</label>
|
|
4292
|
+
<input type="text" id="share-domain-input" class="input mono" placeholder="app.example.com" autocomplete="off">
|
|
4293
|
+
<div id="share-domain-error" class="field-error" role="alert" hidden></div>
|
|
4294
|
+
<p id="share-domain-hint" class="modal-hint"></p>
|
|
4295
|
+
</span>
|
|
4296
|
+
</span>
|
|
4297
|
+
</button>
|
|
4298
|
+
</div>
|
|
4299
|
+
<div id="share-error" class="field-error" role="alert" hidden></div>
|
|
4300
|
+
</div>
|
|
4301
|
+
<div class="modal-foot">
|
|
4302
|
+
<button type="button" id="share-cancel" class="btn">Cancel</button>
|
|
4303
|
+
<button type="button" id="share-submit" class="btn btn-primary">Start sharing</button>
|
|
4304
|
+
</div>
|
|
4305
|
+
</dialog>
|
|
3652
4306
|
<div id="toast-container" class="toast-container" aria-live="polite"></div>
|
|
3653
4307
|
<script nonce="${safeNonce}">${clientJs}</script>
|
|
3654
4308
|
</body>
|