Host form: redesign as a tabbed modal
Replace the dense always-open add panel + inline edit card with a single Bootstrap modal (shared by Add and Edit) organized into tabs: General, TLS & Wildcard, Traffic, Headers, Access (IP + basic auth + SSO). Adds per-field explanations, a full-width proxy list, and an "Add host" button. Preserves all field names/ids, the challenge-type detection JS, and formAJAX wiring; drops the form-clone edit mechanism in favor of populating the one modal. Verified in a browser (add + edit, tab navigation, field population). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+372
-419
@@ -11,7 +11,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
div.form-group{
|
div.form-group{
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-help{
|
||||||
|
font-size: .82rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* my Div class for my search bar */
|
/* my Div class for my search bar */
|
||||||
@@ -22,25 +26,16 @@
|
|||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The input bar */
|
/* Greys out a challenge/matching option that isn't available for the host. */
|
||||||
input {
|
|
||||||
font-size: 1rem;
|
|
||||||
border-top-left-radius: 5px !important;
|
|
||||||
border-bottom-left-radius: 5px !important;
|
|
||||||
border-top-right-radius: 5px !important;
|
|
||||||
border-bottom-right-radius: 5px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.challengeType-container {
|
.challengeType-container {
|
||||||
pointer-events: none; /* Prevents clicking */
|
pointer-events: none; /* Prevents clicking */
|
||||||
opacity: 0.5; /* Greys it out */
|
opacity: 0.5; /* Greys it out */
|
||||||
filter: grayscale(1); /* Removes blue/color tint */
|
filter: grayscale(1); /* Removes blue/color tint */
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
var $editHostForm;
|
|
||||||
|
|
||||||
// Parse the JSON object for a host to something the UI wants
|
// Parse the JSON object for a host to something the UI wants
|
||||||
function hostParseRow(host) {
|
function hostParseRow(host) {
|
||||||
@@ -82,59 +77,90 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function hostEditCancle(){
|
|
||||||
$('tr.jq-repeat-hosts').each(function(idx, el){
|
|
||||||
$(el).removeClass('table-warning');
|
|
||||||
});
|
|
||||||
$.scope.editHost.remove(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mirror of utils/host_features.js stringify* helpers for populating the edit
|
// Mirror of utils/host_features.js stringify* helpers for populating the edit
|
||||||
// form's textareas. The server re-parses the posted text authoritatively.
|
// form's textareas. The server re-parses the posted text authoritatively.
|
||||||
function hostFeatureHeadersToText(obj){
|
function hostFeatureHeadersToText(obj){
|
||||||
if(!obj || typeof obj != 'object') return '';
|
if(!obj || typeof obj != 'object') return '';
|
||||||
return Object.keys(obj).map(function(name){ return name + ': ' + obj[name]; }).join('\n');
|
return Object.keys(obj).map(function(name){ return name + ': ' + obj[name]; }).join('\n');
|
||||||
}
|
}
|
||||||
function hostFeatureCidrsToText(arr){
|
function hostFeatureListToText(arr){
|
||||||
return Array.isArray(arr) ? arr.join('\n') : '';
|
return Array.isArray(arr) ? arr.join('\n') : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function hostEditOpen(btn, host){
|
// ----- Add / Edit modal --------------------------------------------------
|
||||||
hostEditCancle();
|
|
||||||
console.log('host:', host)
|
|
||||||
host = $.scope.hosts.getByKey(host);
|
|
||||||
host.__jq_$el.addClass('table-warning');
|
|
||||||
$editHostForm.find('[name=is_wildcard').attr('disabled', true);
|
|
||||||
$.scope.editHost.update({...host, form: $editHostForm.html()});
|
|
||||||
|
|
||||||
if(host.is_wildcard){
|
function hostModal(){
|
||||||
$('.hostEditPanel [name="host"]').attr('disabled', true);
|
return bootstrap.Modal.getOrCreateInstance(document.getElementById('hostModal'));
|
||||||
// Allow toggling the wildcard matching mode when editing a wildcard host.
|
}
|
||||||
$('.hostEditPanel #wildcard_matchAny-container').removeClass('challengeType-container');
|
function hostModalClose(){ hostModal().hide(); }
|
||||||
}
|
|
||||||
|
|
||||||
$.each(host, function( key, value ) { if(typeof value == "boolean"){
|
function hostShowTab(id){
|
||||||
$(".hostEditPanel #"+ key +"-"+ value).prop('checked', true)
|
bootstrap.Tab.getOrCreateInstance(document.getElementById(id)).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the form to a clean "add" state.
|
||||||
|
function hostFormReset(){
|
||||||
|
let form = document.getElementById('hostForm');
|
||||||
|
form.reset();
|
||||||
|
let $f = $(form);
|
||||||
|
$f.attr('method', 'POST').attr('action', 'host').attr('evalAJAX', 'hostModalClose()');
|
||||||
|
$f.find('[name=host]').prop('disabled', false);
|
||||||
|
if($f.validateClear) $f.validateClear();
|
||||||
|
|
||||||
|
// A fresh host only qualifies for HTTP-01 until the name says otherwise.
|
||||||
|
$('#challengeType-child-container, #challengeType-DNS-01-wildcard-container, #wildcard_matchAny-container')
|
||||||
|
.addClass('challengeType-container');
|
||||||
|
$('#challengeType-child-relatedHost').text('');
|
||||||
|
$('.basicauth-current').text('none');
|
||||||
|
hostShowTab('hostTab-general-btn');
|
||||||
|
}
|
||||||
|
|
||||||
|
function hostAddOpen(){
|
||||||
|
hostFormReset();
|
||||||
|
$('#hostModalTitle').text('Add host');
|
||||||
|
$('#hostModalSubmitText').text('Add host');
|
||||||
|
hostModal().show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hostEditOpen(host){
|
||||||
|
hostFormReset();
|
||||||
|
let h = $.scope.hosts.getByKey(host);
|
||||||
|
let $f = $('#hostForm');
|
||||||
|
|
||||||
|
$f.attr('method', 'PUT').attr('action', 'host/' + encodeURIComponent(host));
|
||||||
|
$('#hostModalTitle').text('Edit ' + host);
|
||||||
|
$('#hostModalSubmitText').text('Save changes');
|
||||||
|
|
||||||
|
// Scalar fields: booleans drive the matching radio, everything else the
|
||||||
|
// input with that name. Object/array fields are handled as text below.
|
||||||
|
$.each(h, function(key, value){
|
||||||
|
if(typeof value === 'boolean'){
|
||||||
|
$f.find('#' + key + '-' + value).prop('checked', true);
|
||||||
}else{
|
}else{
|
||||||
$(".hostEditPanel input[name='" + key + "']").val(value);
|
$f.find("input[name='" + key + "']").val(value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Object/array proxy-control fields render into textareas as text. Server
|
$f.find("textarea[name='req_headers']").val(hostFeatureHeadersToText(h.req_headers));
|
||||||
// (utils/host_features.js) parses the same text/shape back on save.
|
$f.find("textarea[name='resp_headers']").val(hostFeatureHeadersToText(h.resp_headers));
|
||||||
$(".hostEditPanel textarea[name='req_headers']").val(hostFeatureHeadersToText(host.req_headers));
|
$f.find("textarea[name='ip_allow']").val(hostFeatureListToText(h.ip_allow));
|
||||||
$(".hostEditPanel textarea[name='resp_headers']").val(hostFeatureHeadersToText(host.resp_headers));
|
$f.find("textarea[name='ip_deny']").val(hostFeatureListToText(h.ip_deny));
|
||||||
$(".hostEditPanel textarea[name='ip_allow']").val(hostFeatureCidrsToText(host.ip_allow));
|
$f.find("textarea[name='sso_allow_users']").val(hostFeatureListToText(h.sso_allow_users));
|
||||||
$(".hostEditPanel textarea[name='ip_deny']").val(hostFeatureCidrsToText(host.ip_deny));
|
$f.find("textarea[name='sso_allow_groups']").val(hostFeatureListToText(h.sso_allow_groups));
|
||||||
|
|
||||||
// Never echo basic-auth passwords back to the form; show the current
|
// Never echo basic-auth passwords; show current usernames as a hint.
|
||||||
// usernames as a hint and leave the textarea blank (blank = keep).
|
$f.find("textarea[name='basicauth_users']").val('');
|
||||||
$(".hostEditPanel textarea[name='basicauth_users']").val('');
|
$('.basicauth-current').text(Object.keys(h.basicauth_users || {}).join(', ') || 'none');
|
||||||
$(".hostEditPanel .basicauth-current").text(
|
|
||||||
Object.keys(host.basicauth_users || {}).join(', ') || 'none');
|
|
||||||
|
|
||||||
$('.hostEditPanel').scrollTo();
|
// The host name is the key; it can't change on edit. Wildcard hosts can
|
||||||
};
|
// still toggle their matching mode.
|
||||||
|
$f.find('[name=host]').prop('disabled', true);
|
||||||
|
if(h.is_wildcard){
|
||||||
|
$('#wildcard_matchAny-container').removeClass('challengeType-container');
|
||||||
|
}
|
||||||
|
|
||||||
|
hostModal().show();
|
||||||
|
}
|
||||||
|
|
||||||
function hostDownloadCert(host, type){
|
function hostDownloadCert(host, type){
|
||||||
app.host.getCert({host}, function(error, data){
|
app.host.getCert({host}, function(error, data){
|
||||||
@@ -144,13 +170,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function hostSearchInput(){
|
function hostSearchInput(){
|
||||||
//search bar html event logic stolen from here
|
|
||||||
// https://github.com/WebDevSimplified/js-search-bar/blob/main/script.js
|
|
||||||
let inputValue = $(event.target).val().toLowerCase();
|
let inputValue = $(event.target).val().toLowerCase();
|
||||||
|
|
||||||
// on each input detected we need to get all host list that was called by the hostPopulate function which is store in
|
|
||||||
// $.scope.hosts and then we need to loop through each host and check if the host name is equal to the input value
|
|
||||||
// if it is we will display the host if not we will hide it
|
|
||||||
for(let hostObj of $.scope.hosts){
|
for(let hostObj of $.scope.hosts){
|
||||||
if (hostObj.host.toLowerCase().includes(inputValue)) {
|
if (hostObj.host.toLowerCase().includes(inputValue)) {
|
||||||
hostObj.__jq_$el.show();
|
hostObj.__jq_$el.show();
|
||||||
@@ -162,9 +182,7 @@
|
|||||||
|
|
||||||
async function verifyWildcardRequirements(host){
|
async function verifyWildcardRequirements(host){
|
||||||
try{
|
try{
|
||||||
|
|
||||||
let res = await app.api.get(`dns/domain/${host}`);
|
let res = await app.api.get(`dns/domain/${host}`);
|
||||||
|
|
||||||
return res.results.length === 1;
|
return res.results.length === 1;
|
||||||
}catch(error){
|
}catch(error){
|
||||||
return false;
|
return false;
|
||||||
@@ -186,7 +204,6 @@
|
|||||||
async function hostMatchWildcard(host){
|
async function hostMatchWildcard(host){
|
||||||
try{
|
try{
|
||||||
let res = await app.api.get(`host/lookup/${host}`);
|
let res = await app.api.get(`host/lookup/${host}`);
|
||||||
|
|
||||||
if(res.results && res.results.is_wildcard){
|
if(res.results && res.results.is_wildcard){
|
||||||
return res.results;
|
return res.results;
|
||||||
}
|
}
|
||||||
@@ -196,17 +213,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
$(document).ready(function(){
|
$(document).ready(function(){
|
||||||
// Clone the new host form to be used on edit requests.
|
// Populate the host UI table
|
||||||
$editHostForm = $('#addHost').clone();
|
|
||||||
$editHostForm.find('hr.buttonBreak').nextAll().remove();
|
|
||||||
// $editHostForm.find('.autoSll').addClass('bg-secondary');
|
|
||||||
|
|
||||||
// Populate the host UI table
|
|
||||||
hostPopulate();
|
hostPopulate();
|
||||||
|
|
||||||
// Determine what lets encrypt challenge type the given host name can use
|
// Determine what Let's Encrypt challenge type the given host name can use.
|
||||||
$hostField = $('[name=host');
|
let $hostField = $('#hostForm [name=host]');
|
||||||
$hostField.keyup(async function(){
|
$hostField.on('keyup', async function(){
|
||||||
// Reset the allowed types on start
|
// Reset the allowed types on start
|
||||||
$('#challengeType-child-container').addClass('challengeType-container');
|
$('#challengeType-child-container').addClass('challengeType-container');
|
||||||
$('#challengeType-DNS-01-wildcard-container').addClass('challengeType-container');
|
$('#challengeType-DNS-01-wildcard-container').addClass('challengeType-container');
|
||||||
@@ -214,36 +226,29 @@
|
|||||||
|
|
||||||
let host = $hostField.val();
|
let host = $hostField.val();
|
||||||
|
|
||||||
// If its a wild card, we must check if the domain has a registered
|
// If it's a wildcard, we must check the domain has a registered provider.
|
||||||
// provider.
|
|
||||||
if(host.startsWith("*.") && await verifyWildcardRequirements(host)){
|
if(host.startsWith("*.") && await verifyWildcardRequirements(host)){
|
||||||
$('#challengeType-DNS-01-wildcard-container').removeClass('challengeType-container');
|
$('#challengeType-DNS-01-wildcard-container').removeClass('challengeType-container');
|
||||||
// Wildcard matching mode only applies to wildcard hosts.
|
|
||||||
$('#wildcard_matchAny-container').removeClass('challengeType-container');
|
$('#wildcard_matchAny-container').removeClass('challengeType-container');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if a wildcard cert is available for the given host.
|
// Check if a wildcard cert is available for the given host.
|
||||||
let wildcardParent = await hostMatchWildcard($hostField.val());
|
let wildcardParent = await hostMatchWildcard(host);
|
||||||
if(wildcardParent){
|
if(wildcardParent){
|
||||||
$('#challengeType-child-container').removeClass('challengeType-container');
|
$('#challengeType-child-container').removeClass('challengeType-container');
|
||||||
$('#challengeType-child-relatedHost').text(wildcardParent.host);
|
$('#challengeType-child-relatedHost').text(wildcardParent.host);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we hit here, make sure the form is reverted to a valid state
|
// Revert the form to a valid state.
|
||||||
$('#challengeType-child-relatedHost').text('');
|
$('#challengeType-child-relatedHost').text('');
|
||||||
$('#challengeType-HTTP-01').prop('checked', true);
|
$('#challengeType-HTTP-01').prop('checked', true);
|
||||||
});
|
});
|
||||||
|
|
||||||
//
|
|
||||||
$.scope.hosts.take = function($el, item, list){
|
$.scope.hosts.take = function($el, item, list){
|
||||||
$el.addClass('table-danger');
|
$el.addClass('table-danger');
|
||||||
$el.fadeOut(500, function(){
|
$el.fadeOut(500, function(){ $el.remove() });
|
||||||
$el.remove()
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
$.scope.hosts.putUpdate = function($el, $render, item, list){
|
$.scope.hosts.putUpdate = function($el, $render, item, list){
|
||||||
@@ -251,21 +256,8 @@
|
|||||||
$el.replaceWith($render);
|
$el.replaceWith($render);
|
||||||
};
|
};
|
||||||
|
|
||||||
$.scope.editHost.put = function($el, item, list){
|
|
||||||
$el.slideDown();
|
|
||||||
};
|
|
||||||
|
|
||||||
$.scope.editHost.take = function($el, item, list){
|
|
||||||
$el.slideUp();
|
|
||||||
};
|
|
||||||
|
|
||||||
// app.subscribe(/^model:Host/, function(data, topic){
|
|
||||||
// console.log(topic, data);
|
|
||||||
// });
|
|
||||||
|
|
||||||
app.subscribe(/^model:Host:create/, function(data, topic){
|
app.subscribe(/^model:Host:create/, function(data, topic){
|
||||||
let [a,b, action, host] = topic.split(':');
|
let [a,b, action, host] = topic.split(':');
|
||||||
|
|
||||||
if($.scope.hosts.indexOf(host) >= 0){
|
if($.scope.hosts.indexOf(host) >= 0){
|
||||||
$.scope.hosts.update(host, hostParseRow(data));
|
$.scope.hosts.update(host, hostParseRow(data));
|
||||||
}else{
|
}else{
|
||||||
@@ -275,7 +267,6 @@
|
|||||||
|
|
||||||
app.subscribe(/^model:Host:update/, function(data, topic){
|
app.subscribe(/^model:Host:update/, function(data, topic){
|
||||||
let [a,b, action, host] = topic.split(':');
|
let [a,b, action, host] = topic.split(':');
|
||||||
|
|
||||||
if($.scope.hosts.indexOf(host) >= 0){
|
if($.scope.hosts.indexOf(host) >= 0){
|
||||||
$.scope.hosts.update(host, hostParseRow(data));
|
$.scope.hosts.update(host, hostParseRow(data));
|
||||||
}else{
|
}else{
|
||||||
@@ -285,321 +276,26 @@
|
|||||||
|
|
||||||
app.subscribe(/^model:Host:remove/, function(data, topic){
|
app.subscribe(/^model:Host:remove/, function(data, topic){
|
||||||
let [a,b, action, host] = topic.split(':');
|
let [a,b, action, host] = topic.split(':');
|
||||||
|
|
||||||
$.scope.hosts.remove(host);
|
$.scope.hosts.remove(host);
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="row" style="display:none">
|
<div class="row" style="display:none">
|
||||||
<div class="col col-md-12 col-lg-4 col-xl-3 col-xxl-2">
|
<div class="col-12">
|
||||||
<!--
|
|
||||||
left column
|
|
||||||
-->
|
|
||||||
<div jq-repeat="editHost" class="card shadow-lg border-warning hostEditPanel mb-3" style="display:none">
|
|
||||||
<!--
|
|
||||||
Edit host card
|
|
||||||
-->
|
|
||||||
<div class="card-header text-center bg-warning">
|
|
||||||
<span class="card-icon float-start">
|
|
||||||
<i class="fa-solid fa-pencil"></i>
|
|
||||||
</span>
|
|
||||||
<span class="card-title">
|
|
||||||
Edit {{ host }}
|
|
||||||
</span>
|
|
||||||
<span class="float-end">
|
|
||||||
<i class="fa-solid fa-circle-minus"></i>
|
|
||||||
<i class="fa-solid fa-circle-xmark" onclick="hostEditCancle()"></i>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card-body">
|
|
||||||
<form class="addHost" method="PUT" action="host/{{ host }}" onsubmit="formAJAX(this)" evalAJAX="hostEditCancle()">
|
|
||||||
{{{ form }}}
|
|
||||||
<input type="hidden" name="edit_host" />
|
|
||||||
<button type="submit" data-type="edit" class="btn btn-warning">
|
|
||||||
<i class="fa-solid fa-pencil"></i>
|
|
||||||
Update
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-secondary" type="reset" onclick="hostEditCancle()">
|
|
||||||
<i class="fa-solid fa-ban"></i>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card shadow-lg mb-3 hostAddPanel">
|
|
||||||
<!--
|
|
||||||
Add new host card
|
|
||||||
-->
|
|
||||||
<div class="card-header text-center">
|
|
||||||
<span class="card-icon float-start">
|
|
||||||
<i class="fa-solid fa-plus"></i>
|
|
||||||
</span>
|
|
||||||
<span class="card-title">
|
|
||||||
New Entry
|
|
||||||
</span>
|
|
||||||
<span class="float-end">
|
|
||||||
<i class="fa-solid fa-circle-minus"></i>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card-header actionMessage" style="display:none"></div>
|
|
||||||
|
|
||||||
<div class="card-body d-none d-md-block">
|
|
||||||
<form class="addHost" id="addHost" method="POST" action="host" onsubmit="formAJAX(this)">
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">
|
|
||||||
Incoming SSL
|
|
||||||
</label>
|
|
||||||
<br />
|
|
||||||
<div class="radio">
|
|
||||||
<label>
|
|
||||||
<input type="radio" name="forcessl" id="forcessl-true" value="true" checked>
|
|
||||||
Force incoming connections over HTTPS <b>Recommended</b>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="radio">
|
|
||||||
<label>
|
|
||||||
<input type="radio" name="forcessl" id="forcessl-false" value="false">
|
|
||||||
Allow use of both HTTP and HTTPS
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="host" class="form-label">
|
|
||||||
Incoming Host Name
|
|
||||||
</label>
|
|
||||||
<div>
|
|
||||||
<input type="text" name="host" class="form-control" placeholder="ex: proxy.cloud-ops.net, *.cloud-ops.net, **.cloud-ops.net, or **" validate="host" >
|
|
||||||
<b class="invalid-feedback"></b>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group autoSll">
|
|
||||||
<label class="form-label">
|
|
||||||
SSL <a href="https://letsencrypt.org/docs/challenge-types/" target="_blank">Validation Type</a>:
|
|
||||||
</label>
|
|
||||||
<div class="radio" id="challengeType-HTTP-01-container">
|
|
||||||
<label>
|
|
||||||
<input type="radio" name="challengeType" id="challengeType-HTTP-01" value="HTTP-01" checked>
|
|
||||||
HTTP-01
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="radio challengeType-container" id="challengeType-DNS-01-wildcard-container">
|
|
||||||
<label>
|
|
||||||
<input type="radio" name="challengeType" id="challengeType-DNS-01-wildcard" value="DNS-01-wildcard">
|
|
||||||
DNS-01 Wildcard
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="radio challengeType-container" id="challengeType-child-container">
|
|
||||||
<label>
|
|
||||||
<input type="radio" name="challengeType" id="challengeType-wildcardChild" value="wildcardChild">
|
|
||||||
Parent Wildcard from <i id="challengeType-child-relatedHost"></i>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group challengeType-container" id="wildcard_matchAny-container">
|
|
||||||
<label class="form-label">
|
|
||||||
Wildcard Matching
|
|
||||||
</label>
|
|
||||||
<div class="radio">
|
|
||||||
<label>
|
|
||||||
<input type="radio" name="wildcard_matchAny" id="wildcard_matchAny-false" value="false" checked>
|
|
||||||
Match only subdomains defined here <b>Recommended</b>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="radio">
|
|
||||||
<label>
|
|
||||||
<input type="radio" name="wildcard_matchAny" id="wildcard_matchAny-true" value="true">
|
|
||||||
Match any subdomain and proxy to this host
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3 form-group">
|
|
||||||
<label for="ip" class="form-label">
|
|
||||||
Target IP or Host Name
|
|
||||||
</label>
|
|
||||||
<input type="text" name="ip" class="form-control" placeholder="ex: 10.10.10.10 or app.internal.net" validate="target:3" />
|
|
||||||
<b class="invalid-feedback"></b>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="targetPort" class="form-label">
|
|
||||||
Target TCP Port
|
|
||||||
</label>
|
|
||||||
<input type="number" name="targetPort" class="form-control" value="80" min="0" max="65535" />
|
|
||||||
<b class="invalid-feedback"></b>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">
|
|
||||||
Target SSL
|
|
||||||
</label>
|
|
||||||
<div class="radio">
|
|
||||||
<label>
|
|
||||||
<input type="radio" name="targetssl" id="targetssl-true" value="true">
|
|
||||||
Proxy to HTTPS
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="radio">
|
|
||||||
<label>
|
|
||||||
<input type="radio" name="targetssl" id="targetssl-false" value="false" checked>
|
|
||||||
Proxy to HTTP <b>Recommended</b>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<b class="invalid-feedback"></b>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr />
|
|
||||||
<h6 class="text-muted">Proxy controls</h6>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">Rate limiting</label>
|
|
||||||
<div class="radio">
|
|
||||||
<label>
|
|
||||||
<input type="radio" name="ratelimit_enabled" id="ratelimit_enabled-false" value="false" checked>
|
|
||||||
Off <b>Recommended</b>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="radio">
|
|
||||||
<label>
|
|
||||||
<input type="radio" name="ratelimit_enabled" id="ratelimit_enabled-true" value="true">
|
|
||||||
Limit requests per client IP
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col form-group">
|
|
||||||
<label for="ratelimit_rate" class="form-label">Requests / sec</label>
|
|
||||||
<input type="number" name="ratelimit_rate" class="form-control" value="10" min="1" max="1000000" />
|
|
||||||
</div>
|
|
||||||
<div class="col form-group">
|
|
||||||
<label for="ratelimit_burst" class="form-label">Burst</label>
|
|
||||||
<input type="number" name="ratelimit_burst" class="form-control" value="20" min="0" max="1000000" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">Response caching</label>
|
|
||||||
<div class="radio">
|
|
||||||
<label>
|
|
||||||
<input type="radio" name="respcache_enabled" id="respcache_enabled-false" value="false" checked>
|
|
||||||
Off <b>Recommended</b>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="radio">
|
|
||||||
<label>
|
|
||||||
<input type="radio" name="respcache_enabled" id="respcache_enabled-true" value="true">
|
|
||||||
Cache cacheable responses
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">HSTS</label>
|
|
||||||
<div class="radio">
|
|
||||||
<label>
|
|
||||||
<input type="radio" name="hsts_enabled" id="hsts_enabled-false" value="false" checked>
|
|
||||||
Off
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="radio">
|
|
||||||
<label>
|
|
||||||
<input type="radio" name="hsts_enabled" id="hsts_enabled-true" value="true">
|
|
||||||
Send Strict-Transport-Security
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="ip_allow" class="form-label">Allow IPs / CIDRs</label>
|
|
||||||
<textarea name="ip_allow" class="form-control" rows="2" placeholder="one per line; if set, only these are allowed"></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="ip_deny" class="form-label">Deny IPs / CIDRs</label>
|
|
||||||
<textarea name="ip_deny" class="form-control" rows="2" placeholder="one per line; these are blocked"></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="req_headers" class="form-label">Upstream request headers</label>
|
|
||||||
<textarea name="req_headers" class="form-control" rows="2" placeholder="Name: value, one per line"></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="resp_headers" class="form-label">Response headers</label>
|
|
||||||
<textarea name="resp_headers" class="form-control" rows="2" placeholder="Name: value, one per line"></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">Basic authentication</label>
|
|
||||||
<div class="radio">
|
|
||||||
<label>
|
|
||||||
<input type="radio" name="basicauth_enabled" id="basicauth_enabled-false" value="false" checked>
|
|
||||||
Off
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="radio">
|
|
||||||
<label>
|
|
||||||
<input type="radio" name="basicauth_enabled" id="basicauth_enabled-true" value="true">
|
|
||||||
Require username / password
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="basicauth_realm" class="form-label">Realm</label>
|
|
||||||
<input type="text" name="basicauth_realm" class="form-control" value="Restricted" placeholder="Restricted" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="basicauth_users" class="form-label">Users</label>
|
|
||||||
<textarea name="basicauth_users" class="form-control" rows="2" placeholder="username:password, one per line"></textarea>
|
|
||||||
<small class="text-muted">
|
|
||||||
Current: <span class="basicauth-current">none</span>.
|
|
||||||
Passwords are stored hashed and never shown here. Leave blank to
|
|
||||||
keep the current users; entering any lines replaces the whole list.
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr class="buttonBreak" />
|
|
||||||
<button type="submit" class="btn btn-success">
|
|
||||||
<i class="fa-solid fa-plus"></i>
|
|
||||||
Add
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col col-md-12 col-lg-8 col-xl-9 col-xxl-10">
|
|
||||||
<!--
|
|
||||||
Right column
|
|
||||||
-->
|
|
||||||
<div class="card shadow-lg hostListPanel">
|
<div class="card shadow-lg hostListPanel">
|
||||||
<!--
|
<div class="card-header d-flex align-items-center">
|
||||||
List current hosts
|
<span class="card-icon me-2"><i class="fa-solid fa-network-wired"></i></span>
|
||||||
-->
|
<span class="card-title fw-bold">Proxy List</span>
|
||||||
<div class="card-header text-center">
|
<span class="ms-auto">
|
||||||
<span class="card-icon float-start">
|
|
||||||
<i class="fa-solid fa-network-wired"></i>
|
|
||||||
</span>
|
|
||||||
<span class="card-title">
|
|
||||||
Proxy List
|
|
||||||
</span>
|
|
||||||
<span class="float-end">
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary me-2" onclick="hostClearCache(this)" title="Clear cached wildcard subdomain lookups">
|
<button type="button" class="btn btn-sm btn-outline-secondary me-2" onclick="hostClearCache(this)" title="Clear cached wildcard subdomain lookups">
|
||||||
<i class="fa-solid fa-broom"></i>
|
<i class="fa-solid fa-broom"></i>
|
||||||
Clear Cache
|
Clear cache
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-success" onclick="hostAddOpen()">
|
||||||
|
<i class="fa-solid fa-plus"></i>
|
||||||
|
Add host
|
||||||
</button>
|
</button>
|
||||||
<i class="fa-solid fa-circle-minus"></i>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -612,7 +308,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class='table-responsive'>
|
<div class='table-responsive'>
|
||||||
<table class="m-0 card-body table table-striped overflow-x-scroll">
|
<table class="m-0 card-body table table-striped overflow-x-scroll">
|
||||||
|
|
||||||
<thead>
|
<thead>
|
||||||
<th>
|
<th>
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
@@ -627,21 +322,11 @@
|
|||||||
<i class="fa-solid fa-trash-can"></i>
|
<i class="fa-solid fa-trash-can"></i>
|
||||||
</button>
|
</button>
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>SSL Expire</th>
|
||||||
SSL Expire
|
<th>Host Name</th>
|
||||||
</th>
|
<th>target</th>
|
||||||
<th>
|
<th class="hidden-xs">Updated</th>
|
||||||
Host Name
|
<th>Actions</th>
|
||||||
</th>
|
|
||||||
<th>
|
|
||||||
target
|
|
||||||
</th>
|
|
||||||
<th class="hidden-xs">
|
|
||||||
Updated
|
|
||||||
</th>
|
|
||||||
<th>
|
|
||||||
Actions
|
|
||||||
</th>
|
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -676,7 +361,6 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
|
|
||||||
<div class="btn-group" role="group">
|
<div class="btn-group" role="group">
|
||||||
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
<i class="fa-brands fa-expeditedssl"></i>
|
<i class="fa-brands fa-expeditedssl"></i>
|
||||||
@@ -707,7 +391,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="button" onclick="hostEditOpen(this, '{{ host }}');" class="btn btn-sm btn-warning">
|
<button type="button" onclick="hostEditOpen('{{ host }}');" class="btn btn-sm btn-warning">
|
||||||
<i class="fa-solid fa-pencil"></i>
|
<i class="fa-solid fa-pencil"></i>
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</button>
|
||||||
@@ -724,4 +408,273 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Add / Edit host modal ------------------------------------------------- -->
|
||||||
|
<div class="modal fade" id="hostModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
||||||
|
<div class="modal-content card border-0">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="hostModalTitle">Add host</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-header actionMessage m-0" style="display:none"></div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<ul class="nav nav-tabs" role="tablist">
|
||||||
|
<li class="nav-item"><button class="nav-link active" id="hostTab-general-btn" data-bs-toggle="tab" data-bs-target="#hostTab-general" type="button" role="tab">General</button></li>
|
||||||
|
<li class="nav-item"><button class="nav-link" id="hostTab-tls-btn" data-bs-toggle="tab" data-bs-target="#hostTab-tls" type="button" role="tab">TLS & Wildcard</button></li>
|
||||||
|
<li class="nav-item"><button class="nav-link" id="hostTab-traffic-btn" data-bs-toggle="tab" data-bs-target="#hostTab-traffic" type="button" role="tab">Traffic</button></li>
|
||||||
|
<li class="nav-item"><button class="nav-link" id="hostTab-headers-btn" data-bs-toggle="tab" data-bs-target="#hostTab-headers" type="button" role="tab">Headers</button></li>
|
||||||
|
<li class="nav-item"><button class="nav-link" id="hostTab-access-btn" data-bs-toggle="tab" data-bs-target="#hostTab-access" type="button" role="tab">Access</button></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<form class="addHost" id="hostForm" method="POST" action="host" onsubmit="formAJAX(this)" evalAJAX="hostModalClose()">
|
||||||
|
<div class="tab-content pt-3">
|
||||||
|
|
||||||
|
<!-- General -->
|
||||||
|
<div class="tab-pane fade show active" id="hostTab-general" role="tabpanel">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="host" class="form-label">Incoming host name</label>
|
||||||
|
<input type="text" name="host" class="form-control" placeholder="ex: app.example.com, *.example.com, **.example.com, or **" validate="host">
|
||||||
|
<b class="invalid-feedback"></b>
|
||||||
|
<small class="field-help text-muted d-block">
|
||||||
|
The public hostname clients request. Use <code>*.example.com</code>
|
||||||
|
for one subdomain level, <code>**.example.com</code> for any depth,
|
||||||
|
or <code>**</code> as a catch-all.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Incoming SSL</label>
|
||||||
|
<div class="radio"><label>
|
||||||
|
<input type="radio" name="forcessl" id="forcessl-true" value="true" checked>
|
||||||
|
Force HTTPS <b>(recommended)</b>
|
||||||
|
</label></div>
|
||||||
|
<div class="radio"><label>
|
||||||
|
<input type="radio" name="forcessl" id="forcessl-false" value="false">
|
||||||
|
Allow both HTTP and HTTPS
|
||||||
|
</label></div>
|
||||||
|
<small class="field-help text-muted d-block">Redirect plain HTTP requests to HTTPS.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="ip" class="form-label">Target IP or host name</label>
|
||||||
|
<input type="text" name="ip" class="form-control" placeholder="ex: 10.10.10.10 or app.internal.net" validate="target:3" />
|
||||||
|
<b class="invalid-feedback"></b>
|
||||||
|
<small class="field-help text-muted d-block">Where matching requests are proxied. Hostname or IP only — no protocol, port, or path.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col form-group">
|
||||||
|
<label for="targetPort" class="form-label">Target TCP port</label>
|
||||||
|
<input type="number" name="targetPort" class="form-control" value="80" min="0" max="65535" />
|
||||||
|
<b class="invalid-feedback"></b>
|
||||||
|
</div>
|
||||||
|
<div class="col form-group">
|
||||||
|
<label class="form-label">Target SSL</label>
|
||||||
|
<div class="radio"><label>
|
||||||
|
<input type="radio" name="targetssl" id="targetssl-false" value="false" checked>
|
||||||
|
Proxy to HTTP <b>(recommended)</b>
|
||||||
|
</label></div>
|
||||||
|
<div class="radio"><label>
|
||||||
|
<input type="radio" name="targetssl" id="targetssl-true" value="true">
|
||||||
|
Proxy to HTTPS
|
||||||
|
</label></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TLS & Wildcard -->
|
||||||
|
<div class="tab-pane fade" id="hostTab-tls" role="tabpanel">
|
||||||
|
<div class="form-group autoSll">
|
||||||
|
<label class="form-label">
|
||||||
|
SSL <a href="https://letsencrypt.org/docs/challenge-types/" target="_blank">validation type</a>
|
||||||
|
</label>
|
||||||
|
<div class="radio" id="challengeType-HTTP-01-container"><label>
|
||||||
|
<input type="radio" name="challengeType" id="challengeType-HTTP-01" value="HTTP-01" checked>
|
||||||
|
HTTP-01
|
||||||
|
</label></div>
|
||||||
|
<div class="radio challengeType-container" id="challengeType-DNS-01-wildcard-container"><label>
|
||||||
|
<input type="radio" name="challengeType" id="challengeType-DNS-01-wildcard" value="DNS-01-wildcard">
|
||||||
|
DNS-01 Wildcard
|
||||||
|
</label></div>
|
||||||
|
<div class="radio challengeType-container" id="challengeType-child-container"><label>
|
||||||
|
<input type="radio" name="challengeType" id="challengeType-wildcardChild" value="wildcardChild">
|
||||||
|
Parent Wildcard from <i id="challengeType-child-relatedHost"></i>
|
||||||
|
</label></div>
|
||||||
|
<small class="field-help text-muted d-block">
|
||||||
|
Options light up based on the host name: wildcard certs need a DNS
|
||||||
|
provider for the domain; child hosts reuse a parent wildcard.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group challengeType-container" id="wildcard_matchAny-container">
|
||||||
|
<label class="form-label">Wildcard matching</label>
|
||||||
|
<div class="radio"><label>
|
||||||
|
<input type="radio" name="wildcard_matchAny" id="wildcard_matchAny-false" value="false" checked>
|
||||||
|
Match only subdomains defined here <b>(recommended)</b>
|
||||||
|
</label></div>
|
||||||
|
<div class="radio"><label>
|
||||||
|
<input type="radio" name="wildcard_matchAny" id="wildcard_matchAny-true" value="true">
|
||||||
|
Match any subdomain and proxy to this host
|
||||||
|
</label></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Traffic -->
|
||||||
|
<div class="tab-pane fade" id="hostTab-traffic" role="tabpanel">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Rate limiting</label>
|
||||||
|
<div class="radio"><label>
|
||||||
|
<input type="radio" name="ratelimit_enabled" id="ratelimit_enabled-false" value="false" checked>
|
||||||
|
Off <b>(recommended)</b>
|
||||||
|
</label></div>
|
||||||
|
<div class="radio"><label>
|
||||||
|
<input type="radio" name="ratelimit_enabled" id="ratelimit_enabled-true" value="true">
|
||||||
|
Limit requests per client IP
|
||||||
|
</label></div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col form-group">
|
||||||
|
<label for="ratelimit_rate" class="form-label">Requests / sec</label>
|
||||||
|
<input type="number" name="ratelimit_rate" class="form-control" value="10" min="1" max="1000000" />
|
||||||
|
</div>
|
||||||
|
<div class="col form-group">
|
||||||
|
<label for="ratelimit_burst" class="form-label">Burst</label>
|
||||||
|
<input type="number" name="ratelimit_burst" class="form-control" value="20" min="0" max="1000000" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<small class="field-help text-muted d-block mb-3">Token bucket per client IP; bursts above the rate are queued, then rejected with 429.</small>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Response caching</label>
|
||||||
|
<div class="radio"><label>
|
||||||
|
<input type="radio" name="respcache_enabled" id="respcache_enabled-false" value="false" checked>
|
||||||
|
Off <b>(recommended)</b>
|
||||||
|
</label></div>
|
||||||
|
<div class="radio"><label>
|
||||||
|
<input type="radio" name="respcache_enabled" id="respcache_enabled-true" value="true">
|
||||||
|
Cache cacheable responses
|
||||||
|
</label></div>
|
||||||
|
<small class="field-help text-muted d-block">Cache upstream responses that declare themselves cacheable.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">HSTS</label>
|
||||||
|
<div class="radio"><label>
|
||||||
|
<input type="radio" name="hsts_enabled" id="hsts_enabled-false" value="false" checked>
|
||||||
|
Off
|
||||||
|
</label></div>
|
||||||
|
<div class="radio"><label>
|
||||||
|
<input type="radio" name="hsts_enabled" id="hsts_enabled-true" value="true">
|
||||||
|
Send Strict-Transport-Security
|
||||||
|
</label></div>
|
||||||
|
<small class="field-help text-muted d-block">Tells browsers to only use HTTPS for this host. Enable once HTTPS is confirmed working.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Headers -->
|
||||||
|
<div class="tab-pane fade" id="hostTab-headers" role="tabpanel">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="req_headers" class="form-label">Upstream request headers</label>
|
||||||
|
<textarea name="req_headers" class="form-control" rows="3" placeholder="Name: value, one per line"></textarea>
|
||||||
|
<small class="field-help text-muted d-block">Added to each request sent to the target. One <code>Name: value</code> per line.</small>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="resp_headers" class="form-label">Response headers</label>
|
||||||
|
<textarea name="resp_headers" class="form-control" rows="3" placeholder="Name: value, one per line"></textarea>
|
||||||
|
<small class="field-help text-muted d-block">Added to each response returned to the client.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Access -->
|
||||||
|
<div class="tab-pane fade" id="hostTab-access" role="tabpanel">
|
||||||
|
<h6 class="text-muted">IP access</h6>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="ip_allow" class="form-label">Allow IPs / CIDRs</label>
|
||||||
|
<textarea name="ip_allow" class="form-control" rows="2" placeholder="one per line; if set, only these are allowed"></textarea>
|
||||||
|
<small class="field-help text-muted d-block">If non-empty, only these sources may connect (default-deny).</small>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="ip_deny" class="form-label">Deny IPs / CIDRs</label>
|
||||||
|
<textarea name="ip_deny" class="form-control" rows="2" placeholder="one per line; these are blocked"></textarea>
|
||||||
|
<small class="field-help text-muted d-block">These sources are always blocked (deny wins over allow).</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<h6 class="text-muted">
|
||||||
|
Authentication
|
||||||
|
<small class="fw-normal">— basic auth and SSO are OR'd; either one grants access.</small>
|
||||||
|
</h6>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Basic authentication</label>
|
||||||
|
<div class="radio"><label>
|
||||||
|
<input type="radio" name="basicauth_enabled" id="basicauth_enabled-false" value="false" checked>
|
||||||
|
Off
|
||||||
|
</label></div>
|
||||||
|
<div class="radio"><label>
|
||||||
|
<input type="radio" name="basicauth_enabled" id="basicauth_enabled-true" value="true">
|
||||||
|
Require username / password
|
||||||
|
</label></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="basicauth_realm" class="form-label">Realm</label>
|
||||||
|
<input type="text" name="basicauth_realm" class="form-control" value="Restricted" placeholder="Restricted" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="basicauth_users" class="form-label">Users</label>
|
||||||
|
<textarea name="basicauth_users" class="form-control" rows="2" placeholder="username:password, one per line"></textarea>
|
||||||
|
<small class="field-help text-muted d-block">
|
||||||
|
Current: <span class="basicauth-current">none</span>.
|
||||||
|
Passwords are stored hashed and never shown here. Leave blank to keep
|
||||||
|
the current users; entering any lines replaces the whole list.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Single sign-on (SSO)</label>
|
||||||
|
<div class="radio"><label>
|
||||||
|
<input type="radio" name="sso_enabled" id="sso_enabled-false" value="false" checked>
|
||||||
|
Off
|
||||||
|
</label></div>
|
||||||
|
<div class="radio"><label>
|
||||||
|
<input type="radio" name="sso_enabled" id="sso_enabled-true" value="true">
|
||||||
|
Require login via the configured OIDC provider
|
||||||
|
</label></div>
|
||||||
|
<small class="field-help text-muted d-block">Gates the site behind the same identity provider the admin app uses.</small>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="sso_allow_users" class="form-label">Allowed users</label>
|
||||||
|
<textarea name="sso_allow_users" class="form-control" rows="2" placeholder="one email/username per line; blank = any authenticated user"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="sso_allow_groups" class="form-label">Allowed groups</label>
|
||||||
|
<textarea name="sso_allow_groups" class="form-control" rows="2" placeholder="one group per line; blank = any authenticated user"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||||
|
<i class="fa-solid fa-ban"></i> Cancel
|
||||||
|
</button>
|
||||||
|
<button type="submit" form="hostForm" class="btn btn-success">
|
||||||
|
<i class="fa-solid fa-floppy-disk"></i>
|
||||||
|
<span id="hostModalSubmitText">Add host</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<%- include('bottom') %>
|
<%- include('bottom') %>
|
||||||
|
|||||||
Reference in New Issue
Block a user