Add AUTH_STACK so the Auth tab follows the stack in force

Authentik is the likely destination and the page had Authelia and lldap wired in at every
level, so the seam goes in now: the panels and every endpoint action route off one conf value,
and a stack that cannot be driven yet says so rather than drawing controls with nothing behind.
This commit is contained in:
Gmer4Lfe
2026-08-15 15:27:50 -04:00
parent 2697b46616
commit 8157673291
6 changed files with 219 additions and 25 deletions
+86 -21
View File
@@ -37,6 +37,12 @@
// LLDAP user and group management
// Authelia access-control rules and default policy
//
// STACK SWITCHING
// AUTH_STACK in master.conf decides which panels exist. Proxies and Certs belong to Nginx
// Proxy Manager and are drawn for every stack; Users and Access Control are the identity
// stack's and are drawn only for one that Varaverk can drive. api/auth.php applies the same
// rule to every action, so a tab left open across a switch cannot write to the old stack.
//
// DEPENDS ON
// include/auth.php required directly for initial render
// api/auth.php mutations
@@ -60,6 +66,17 @@ require_once dirname(__DIR__) . '/include/ai_chat.php';
.vv-au-btn.green{ border-color:#1a3a1a;background:#0d1f0d;color:#4caf50; }
.vv-au-btn:disabled { opacity:.4;cursor:default; }
/* ── Stack indicator ─────────────────────────────────────────────────────── */
/* Which stack the tab is driving. Always visible, because every panel below it means something
different depending on this, and it is a conf value nothing else on the page reveals. */
.vv-au-stackchip{ font-size:10px;padding:2px 8px;border-radius:3px;background:#12181f;
border:1px solid #1e2a38;color:#5c9fd4;white-space:nowrap; }
.vv-au-stacknote{ font-size:11px;color:#997;background:#1a1400;border:1px solid #3a2d00;
border-radius:4px;padding:8px 12px;margin-bottom:10px;line-height:1.55; }
.vv-au-stacknote b { color:#ffb74d;display:block;margin-bottom:2px; }
.vv-au-stacknote.bad { background:#1a0d0d;border-color:#3a1a1a;color:#a77; }
.vv-au-stacknote.bad b { color:#ef5350; }
/* ── Panels ──────────────────────────────────────────────────────────────── */
.vv-au-panel { display:none; }
.vv-au-panel.active { display:block; }
@@ -231,19 +248,45 @@ require_once dirname(__DIR__) . '/include/ai_chat.php';
<?php
require_once __DIR__ . '/../include/auth.php';
$isOwner = vv_is_owner();
// AUTH_STACK decides which panels exist at all. Read once here and used for both the tabs and the
// panels, so a tab can never be drawn for a panel the stack does not carry.
$stackDef= vv_auth_stack_def();
$panels = $stackDef['panels'];
$first = $panels[0] ?? 'proxies';
$tabs = ['proxies' => 'Proxies', 'users' => 'Users &amp; Groups',
'acl' => 'Access Control', 'certs' => 'Certs'];
?>
<div class="vv-au-toolbar">
<span class="vv-au-title">Auth Stack</span>
<button class="vv-au-tab active" data-tab="proxies">Proxies</button>
<button class="vv-au-tab" data-tab="users">Users &amp; Groups</button>
<button class="vv-au-tab" data-tab="acl">Access Control</button>
<button class="vv-au-tab" data-tab="certs">Certs</button>
<?php foreach ($tabs as $id => $label): if (!in_array($id, $panels, true)) continue; ?>
<button class="vv-au-tab<?= $id === $first ? ' active' : '' ?>" data-tab="<?= $id ?>"><?= $label ?></button>
<?php endforeach; ?>
<span class="vv-au-stackchip" title="AUTH_STACK in master.conf — Settings below"><?= htmlspecialchars($stackDef['label']) ?></span>
<button class="vv-au-btn" id="vv-au-refresh" title="Refresh current tab">&#8635; Refresh</button>
</div>
<?php if (!vv_auth_stack_valid()): ?>
<!-- A value that is not one of the known stacks. Named rather than silently corrected, because
the tab is now showing a different stack than the conf asks for and that is worth knowing. -->
<div class="vv-au-stacknote bad">
<b>AUTH_STACK is set to "<?= htmlspecialchars(trim(vv_conf_vars()['AUTH_STACK'] ?? '')) ?>", which is not a stack this page knows.</b>
Showing <?= htmlspecialchars($stackDef['label']) ?> instead. Fix it in Auth settings, below.
</div>
<?php elseif (!$stackDef['ready']): ?>
<!-- Selected but not implemented. The panels that do work are still drawn — proxy hosts and
certificates belong to Nginx Proxy Manager, not to the identity stack — and the two that
cannot are absent with the reason stated, rather than present and broken. -->
<div class="vv-au-stacknote">
<b><?= htmlspecialchars($stackDef['label']) ?> is selected, and Varaverk cannot manage it yet.</b>
<?= htmlspecialchars($stackDef['summary']) ?>
<span style="color:#4a4a4a">Needed: <?= htmlspecialchars($stackDef['needs'] ?? '') ?></span>
</div>
<?php endif; ?>
<!-- ── Proxies ─────────────────────────────────────────────────────────────── -->
<div class="vv-au-panel active" id="vv-au-panel-proxies">
<?php if (in_array("proxies",$panels,true)): ?>
<div class="vv-au-panel<?= $first==="proxies" ? " active" : "" ?>" id="vv-au-panel-proxies">
<div class="vv-au-sec-bar">
<span class="vv-au-sec-title" id="vv-au-proxy-count"></span>
<button class="vv-au-btn prim" id="vv-au-proxy-add">+ Add Proxy</button>
@@ -259,9 +302,11 @@ $isOwner = vv_is_owner();
<div class="vv-au-empty" id="vv-au-proxy-empty" style="display:none">No proxy hosts configured.</div>
</div>
</div>
<?php endif; ?>
<!-- ── Users & Groups ─────────────────────────────────────────────────────── -->
<div class="vv-au-panel" id="vv-au-panel-users">
<?php if (in_array("users",$panels,true)): ?>
<div class="vv-au-panel<?= $first==="users" ? " active" : "" ?>" id="vv-au-panel-users">
<div class="vv-au-ug-grid">
<div class="vv-au-card">
@@ -286,9 +331,11 @@ $isOwner = vv_is_owner();
</div>
</div>
<?php endif; ?>
<!-- ── Access Control ─────────────────────────────────────────────────────── -->
<div class="vv-au-panel" id="vv-au-panel-acl">
<?php if (in_array("acl",$panels,true)): ?>
<div class="vv-au-panel<?= $first==="acl" ? " active" : "" ?>" id="vv-au-panel-acl">
<div class="vv-au-ac-defpol" id="vv-au-ac-defpol-bar">
<span>Default policy:</span>
<select id="vv-au-ac-defpol" <?= $isOwner ? '' : 'disabled' ?>>
@@ -315,9 +362,11 @@ $isOwner = vv_is_owner();
<div class="vv-au-acl-grid" id="vv-au-acl-body" style="display:none"></div>
<div class="vv-au-card"><div class="vv-au-empty" id="vv-au-acl-empty" style="display:none">No rules configured.</div></div>
</div>
<?php endif; ?>
<!-- ── Certs ───────────────────────────────────────────────────────────────── -->
<div class="vv-au-panel" id="vv-au-panel-certs">
<?php if (in_array("certs",$panels,true)): ?>
<div class="vv-au-panel<?= $first==="certs" ? " active" : "" ?>" id="vv-au-panel-certs">
<div class="vv-au-sec-bar">
<span class="vv-au-sec-title" id="vv-au-cert-ts"></span>
<button class="vv-au-btn prim" id="vv-au-cert-run">Refresh</button>
@@ -327,6 +376,7 @@ $isOwner = vv_is_owner();
</div>
<div id="vv-au-cert-cfg" style="margin-top:10px;font-size:10px;color:#3a3a3a;"></div>
</div>
<?php endif; ?>
<!-- ── Modal overlay ──────────────────────────────────────────────────────── -->
<div class="vv-au-overlay" id="vv-au-overlay">
@@ -368,7 +418,7 @@ $isOwner = vv_is_owner();
//
// Both passwords render masked and are logged by name only — vv_conf_key_is_secret() matches
// PASS, so the same key cannot be redacted in the audit log and legible in the form.
vv_conf_ui_card('vv-cf-auth', 'NginxProxyManager|lldap|Authelia|Certificate Monitor', 'Auth settings');
vv_conf_ui_card('vv-cf-auth', 'Auth Stack|NginxProxyManager|lldap|Authelia|Certificate Monitor', 'Auth settings');
?>
<script>
@@ -386,7 +436,10 @@ let _rules = [], _defaultPolicy = 'deny';
// The trailing comment on the default_policy line, carried so a save puts it back. The block is
// rebuilt from this model, so anything not held here is deleted by the next save.
let _defaultNote = '';
let _activeTab = 'proxies';
// Seeded from PHP rather than hardcoded to 'proxies'. Which panels exist is AUTH_STACK's decision,
// and a stack whose first panel is not Proxies would boot with _activeTab naming a tab that is not
// on the page — so Refresh would reload nothing and the first render would be empty.
let _activeTab = <?= json_encode($first) ?>;
// ── Helpers ───────────────────────────────────────────────────────────────────
function _esc(s) {
@@ -409,6 +462,16 @@ function _post(params, cb) {
// wide is for the one dialog that carries a photo beside a form. Reset on every call rather than
// only set, or a narrow dialog opened after the editor would inherit its width.
// Attach only if the element is there. AUTH_STACK decides which panels the page renders, so on a
// stack that does not carry Users or Access Control those containers genuinely do not exist —
// and a bare getElementById(...).addEventListener would throw on load and take every listener
// after it down with it, including the ones for the panels that do exist.
function _on(id, ev, fn) {
const el = document.getElementById(id);
if (el) el.addEventListener(ev, fn);
return !!el;
}
function _modal(html, wide) {
const m = document.getElementById('vv-au-modal');
m.innerHTML = html;
@@ -474,7 +537,7 @@ document.querySelectorAll('.vv-au-tab').forEach(btn => {
});
});
document.getElementById('vv-au-refresh').addEventListener('click', () => _loadTab(_activeTab));
_on('vv-au-refresh', 'click', () => _loadTab(_activeTab));
function _loadTab(tab) {
if (tab === 'proxies') _loadProxies();
@@ -641,7 +704,7 @@ function _showModalErr(id, msg) {
}
// Proxy event delegation
document.getElementById('vv-au-panel-proxies').addEventListener('click', async e => {
_on('vv-au-panel-proxies', 'click', async e => {
// Add button
if (e.target.id === 'vv-au-proxy-add') { _proxyModal(null); return; }
// Toggle
@@ -1143,7 +1206,7 @@ function _addToGroupModal(uid) {
}
// User event delegation
document.getElementById('vv-au-panel-users').addEventListener('click', async e => {
_on('vv-au-panel-users', 'click', async e => {
if (e.target.id === 'vv-au-user-add') { _userModal(null); return; }
const editBtn = e.target.closest('[data-user-edit]');
@@ -1215,7 +1278,7 @@ function _loadGroups(done) {
}
// Group event delegation
document.getElementById('vv-au-panel-users').addEventListener('click', async e => {
_on('vv-au-panel-users', 'click', async e => {
if (e.target.id === 'vv-au-group-add') {
_modal(`<h3>Add Group</h3>
<div class="vv-au-field">
@@ -1519,7 +1582,7 @@ function _aclMarkDirty(on) {
// Writes straight into the model and deliberately does not re-render: the element being typed in
// is inside the markup a render would replace, which would drop focus on the first keystroke.
document.getElementById('vv-au-panel-acl').addEventListener('input', e => {
_on('vv-au-panel-acl', 'input', e => {
const inp = e.target.closest('.vv-au-dominput');
if (!inp) return;
const r = parseInt(inp.dataset.rule), d = parseInt(inp.dataset.dom);
@@ -1532,7 +1595,7 @@ document.getElementById('vv-au-panel-acl').addEventListener('input', e => {
// Enter adds a row underneath and moves to it, so a list of fourteen can be typed straight
// through instead of returning to the add button between each one.
document.getElementById('vv-au-panel-acl').addEventListener('keydown', e => {
_on('vv-au-panel-acl', 'keydown', e => {
const inp = e.target.closest('.vv-au-dominput');
if (!inp || e.key !== 'Enter') return;
e.preventDefault();
@@ -1550,7 +1613,7 @@ function _focusDomain(r, d) {
}
// ACL event delegation
document.getElementById('vv-au-panel-acl').addEventListener('click', async e => {
_on('vv-au-panel-acl', 'click', async e => {
if (e.target.id === 'vv-au-rule-add') { _ruleModal(null); return; }
const domAdd = e.target.closest('[data-dom-add]');
@@ -1645,10 +1708,10 @@ document.getElementById('vv-au-panel-acl').addEventListener('click', async e =>
// The default policy is the one control here that is not a rule, and it is the most consequential
// one on the tab — it decides what happens to every hostname no rule names.
document.getElementById('vv-au-ac-defpol').addEventListener('change', () => _aclMarkDirty(true));
_on('vv-au-ac-defpol', 'change', () => _aclMarkDirty(true));
// ── Close modal on overlay click ──────────────────────────────────────────────
document.getElementById('vv-au-overlay').addEventListener('click', e => {
_on('vv-au-overlay', 'click', e => {
if (e.target === document.getElementById('vv-au-overlay')) _closeModal();
});
@@ -1722,7 +1785,7 @@ function _loadCerts(onDone) {
});
}
document.getElementById('vv-au-cert-run').addEventListener('click', function() {
_on('vv-au-cert-run', 'click', function() {
const btn = this;
btn.disabled = true; btn.textContent = 'Loading…';
_loadCerts(() => { btn.disabled = false; btn.textContent = 'Refresh'; });
@@ -1750,7 +1813,9 @@ if (document.getElementById('vv-au-ai-chat')) {
}
// ── Boot ──────────────────────────────────────────────────────────────────────
_loadProxies();
// Through _loadTab so the opening panel follows AUTH_STACK, rather than calling _loadProxies()
// directly and reaching into elements a different stack does not render.
_loadTab(_activeTab);
})();
</script>