Pick a resource's public IP from the edge's addresses, labeled with the domains pointed at each
A hostname under a domain that points at one of the edge's addresses selects that address. An address saved earlier stays selectable even before the edge reports it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
42eb6410b4
commit
c2845d8630
1 file changed
+25
-6
+25
-6
@@ -662,7 +662,8 @@ function portForm(existing) {
|
||||
const st = S.st, m = existing || { proto:'tcp', enabled:true, bindIp:'0.0.0.0', targets:[{siteId:'', host:'127.0.0.1', port:''}], auth:{}, rules:[] };
|
||||
const a = m.auth || {};
|
||||
const edgeSel = h('select', {}, st.edges.map(e => h('option', {value:e.id, selected:e.id === m.edgeId}, e.name + (e.online ? '' : ' (offline)'))));
|
||||
const ipIn = h('input', {list:'ips', value:m.bindIp}), ipList = h('datalist', {id:'ips'});
|
||||
// the public address is picked from the edge's own addresses, each shown with the domains pointed at it
|
||||
const ipIn = h('select', {}), ipList = document.createTextNode('');
|
||||
const protoSel = h('select', {disabled:!!existing}, PROTOS.filter(p => !existing || p[0] !== 'both').map(([v, t]) => h('option', {value:v, selected:v === m.proto}, t)));
|
||||
const name = h('input', {value:m.name || '', placeholder:'optional'}), port = h('input', {value:m.port ? portText(m) : '', placeholder:'25565 or 2456-2458'});
|
||||
const host = h('input', {value:m.hostname || '', list:'doms', placeholder:'app.example.com or *.example.com'});
|
||||
@@ -687,7 +688,25 @@ function portForm(existing) {
|
||||
const webBox = h('div', {class:'stack'});
|
||||
const hostLbl = hostBox;
|
||||
|
||||
const refreshIPs = () => { const e = byId(st.edges, edgeSel.value); ipList.replaceChildren(...['0.0.0.0', ...((e && e.ips) || [])].map(ip => h('option', {value:ip}))); };
|
||||
const ipLabel = (e, ip) => {
|
||||
const doms = st.domains.filter(d => d.ip === ip && (!d.edgeId || !e || d.edgeId === e.id)).map(d => d.name);
|
||||
return doms.length ? `${ip} (${doms.join(', ')})` : ip;
|
||||
};
|
||||
const refreshIPs = () => {
|
||||
const e = byId(st.edges, edgeSel.value), keep = ipIn.value || m.bindIp || '0.0.0.0';
|
||||
// an address saved earlier stays selectable even if the edge has not reported it yet
|
||||
const saved = existing && m.edgeId === edgeSel.value && m.bindIp !== '0.0.0.0' ? [m.bindIp] : [];
|
||||
const ips = [...new Set([...((e && e.ips) || []), ...saved])];
|
||||
ipIn.replaceChildren(h('option', {value:'0.0.0.0'}, 'Every address on the edge'), ...ips.map(ip => h('option', {value:ip}, ipLabel(e, ip))));
|
||||
ipIn.value = ips.includes(keep) ? keep : '0.0.0.0';
|
||||
};
|
||||
// choosing a hostname under a domain that points at one of the edge's addresses selects that address
|
||||
const pickIPForHost = () => {
|
||||
const name = host.value.trim().toLowerCase().replace(/^\*\./, ''), e = byId(st.edges, edgeSel.value);
|
||||
const d = st.domains.find(d => d.ip && (name === d.name || name.endsWith('.' + d.name)) && (!d.edgeId || d.edgeId === edgeSel.value));
|
||||
if (d && ipIn.value === '0.0.0.0' && e && (e.ips || []).includes(d.ip)) ipIn.value = d.ip;
|
||||
};
|
||||
host.addEventListener('change', pickIPForHost);
|
||||
const siteOpts = () => st.sites.filter(s => !s.local || s.edgeId === edgeSel.value);
|
||||
const drawTargets = () => {
|
||||
const many = protoSel.value !== 'udp' && protoSel.value !== 'both';
|
||||
@@ -743,7 +762,7 @@ function portForm(existing) {
|
||||
if (!pr) throw new Error('Enter a port like 25565, or a range like 2456-2458');
|
||||
const roles = roleChecks.filter(x => x.c.box.checked).map(x => x.r.id);
|
||||
const base = {
|
||||
name:name.value, edgeId:edgeSel.value, bindIp:ipIn.value.trim() || '0.0.0.0', port:+pr[1], portEnd:pr[2] ? +pr[2] : 0, hostname:host.value, targets,
|
||||
name:name.value, edgeId:edgeSel.value, bindIp:ipIn.value || '0.0.0.0', port:+pr[1], portEnd:pr[2] ? +pr[2] : 0, hostname:host.value, targets,
|
||||
enabled:m.enabled !== false, rules:rules.filter(r => r.value.trim()),
|
||||
auth:{sso:ssoC.box.checked, password:pwC.box.checked, pin:pinC.box.checked, email:mailC.box.checked, allowedRoles:roles, allowedUsers:a.allowedUsers || [],
|
||||
allowedMails:mails.value.split(',').map(s => s.trim()).filter(Boolean), sessionHours:+hours.value || 0},
|
||||
@@ -758,7 +777,7 @@ function portForm(existing) {
|
||||
close(); toast(existing ? `Saved ${base.name || 'the resource'}` : `Added ${base.name || 'the resource'}`); refresh();
|
||||
});
|
||||
const close = existing ? modal('Edit ' + (m.name || 'resource'), h('div', {class:'stack'},
|
||||
h('div', {class:'grid'}, field('Name', name), field('Edge', edgeSel), field('Public IP', ipIn), field('Protocol', protoSel), field('Public port', port, 'One port, or a range like 2456-2458 for TCP and UDP')),
|
||||
h('div', {class:'grid'}, field('Name', name), field('Edge', edgeSel), field('Public IP', ipIn, 'Domains pointed at an address are shown next to it'), field('Protocol', protoSel), field('Public port', port, 'One port, or a range like 2456-2458 for TCP and UDP')),
|
||||
ipList, domList, hostBox, proxyBox, h('fieldset', {}, h('legend', {}, 'Where does the traffic go?'), tgtBox), adv, webBox,
|
||||
h('div', {class:'actions form-actions'}, h('button', {class:'ghost', onclick:() => close()}, 'Cancel'), h('button', {class:'btn', onclick:save}, existing ? 'Save changes' : 'Open port'))), {sticky:true}) : wizardModal();
|
||||
|
||||
@@ -786,7 +805,7 @@ function portForm(existing) {
|
||||
const t = targets.map(x => ((byId(st.sites, x.siteId) || {name:'?'}).name) + ' → ' + x.host + ':' + x.port).join(', '), pr = (port.value || '').trim();
|
||||
const protoName = {both:'TCP + UDP', tcp:'TCP', udp:'UDP', tls:'TLS'}[protoSel.value];
|
||||
const where = isWeb() ? protoSel.value + '://' + (host.value.trim() || '*') + (pr && pr !== '443' && pr !== '80' ? ':' + pr : '')
|
||||
: protoName + ' port ' + pr + (protoSel.value === 'tls' && host.value.trim() ? ' for ' + host.value.trim() : '') + ' on ' + (ipIn.value.trim() || 'every address');
|
||||
: protoName + ' port ' + pr + (protoSel.value === 'tls' && host.value.trim() ? ' for ' + host.value.trim() : '') + ' on ' + (ipIn.value === '0.0.0.0' ? 'every address' : ipIn.selectedOptions[0].textContent);
|
||||
const login = ssoC.box.checked || pwC.box.checked || pinC.box.checked || mailC.box.checked;
|
||||
return h('div', {class:'stack'}, h('div', {}, h('b', {class:'mono'}, where)), h('div', {class:'small'}, '→ ' + t),
|
||||
isWeb() ? h('div', {class:'small dim'}, login ? 'Visitors have to sign in.' : 'Anyone can open it.') : null);
|
||||
@@ -846,7 +865,7 @@ function portForm(existing) {
|
||||
const web = isWeb();
|
||||
kids.push(h('div', {class:'grid'}, web || protoSel.value === 'tls' ? hostBox : null,
|
||||
field('Public port', port, web ? 'Websites normally use ' + (protoSel.value === 'https' ? '443' : '80') + '.' : 'One port, or a range like 2456-2458: each port goes to the target port plus the same offset.'),
|
||||
field('Public IP', ipIn, '0.0.0.0 means every address on the edge.')), ipList, domList);
|
||||
field('Public IP', ipIn, (byId(st.edges, edgeSel.value) || {}).ips && byId(st.edges, edgeSel.value).ips.length ? 'Domains pointed at an address are shown next to it.' : 'The edge has not reported its addresses yet. Start it to pick a specific one.')), ipList, domList);
|
||||
} else if (cur === 'access') {
|
||||
kids.push(accessFs);
|
||||
} else {
|
||||
|
||||
Reference in new issue
Block a user