Tell the three auth failures apart, and give the Auth tab a settings card
Not set, rejected and unreachable all reached the page as one message about checking credentials, which sends you to a password when the field is simply empty — as both of HOST1's were, with no card on the page to fill them in from.
This commit is contained in:
@@ -45,14 +45,20 @@
|
||||
//
|
||||
// Auth failure is reported, not retried into a lockout.
|
||||
// A failed token fetch returns an _err string immediately. Nothing loops on bad
|
||||
// credentials against a service that may rate-limit or lock the account.
|
||||
// credentials against a service that may rate-limit or lock the account. A blank
|
||||
// credential is caught before the request rather than sent as a guess.
|
||||
//
|
||||
// The three auth failures are told apart.
|
||||
// Not set, rejected, and unreachable all reach a caller as an empty token and need
|
||||
// three different fixes. vv_auth_creds_missing() and vv_auth_token_err() name which.
|
||||
//
|
||||
// Every remote call has a timeout, and every function returns a structured result —
|
||||
// ['ok' => bool] or an _err key — so no caller has to distinguish an exception from a
|
||||
// legitimately empty list.
|
||||
//
|
||||
// EXPORTS
|
||||
// Config vv_auth_conf()
|
||||
// Config vv_auth_conf(), vv_auth_creds_missing(), vv_auth_token_err(),
|
||||
// vv_auth_last_transport()
|
||||
// NPM vv_npm_list_proxies(), vv_npm_list_certs(), vv_npm_create_proxy(),
|
||||
// vv_npm_update_proxy(), vv_npm_delete_proxy(), vv_npm_toggle_proxy()
|
||||
// LLDAP vv_lldap_list_users(), vv_lldap_list_groups(), vv_lldap_create_user(),
|
||||
@@ -92,6 +98,48 @@ function vv_auth_conf(): array {
|
||||
];
|
||||
}
|
||||
|
||||
// ── Credential state ──────────────────────────────────────────────────────────
|
||||
|
||||
// Three different failures arrive at a token fetch as the same empty string: the credential was
|
||||
// never filled in, the service rejected it, or the service is not answering. They need three
|
||||
// different actions, and "check credentials" sends someone to look at a password that is fine
|
||||
// while the container is down — or at a container that is fine while the field is empty.
|
||||
//
|
||||
// Blank is checked first and without a request, because there is nothing to ask: a login with an
|
||||
// empty identity is a guess against a service that may rate-limit or lock the account, and
|
||||
// vv_npm_raw() would report its 401 as if a real password had been rejected.
|
||||
function vv_auth_creds_missing(string $svc): string {
|
||||
$conf = vv_auth_conf();
|
||||
$h = strtoupper(vv_detect_host());
|
||||
if ($svc === 'npm')
|
||||
return ($conf['npm_user'] === '' || $conf['npm_pass'] === '')
|
||||
? "NPM credentials are not set — {$h}_NPM_USER / {$h}_NPM_PASS are empty. Fill them in Auth settings, below."
|
||||
: '';
|
||||
return ($conf['lldap_user'] === '' || $conf['lldap_pass'] === '')
|
||||
? "lldap credentials are not set — {$h}_LLDAP_USER / {$h}_LLDAP_PASS are empty. Fill them in Auth settings, below."
|
||||
: '';
|
||||
}
|
||||
|
||||
// The transport result of the last auth-stack curl, so a caller holding an empty token can say
|
||||
// which of the two remaining failures it was. Static rather than returned through every signature
|
||||
// because the token functions return a plain string and always have; widening them would touch
|
||||
// every call site to carry a value only the failure path reads.
|
||||
function vv_auth_last_transport(?array $set = null): array {
|
||||
static $last = ['errno' => 0, 'error' => '', 'code' => 0];
|
||||
if ($set !== null) $last = $set;
|
||||
return $last;
|
||||
}
|
||||
|
||||
function vv_auth_token_err(string $svc, string $url): string {
|
||||
$t = vv_auth_last_transport();
|
||||
$name = $svc === 'npm' ? 'NPM' : 'lldap';
|
||||
if ($t['errno'])
|
||||
return "$name unreachable at $url — " . ($t['error'] ?: 'connection failed');
|
||||
$h = strtoupper(vv_detect_host());
|
||||
$k = $svc === 'npm' ? "{$h}_NPM_USER / {$h}_NPM_PASS" : "{$h}_LLDAP_USER / {$h}_LLDAP_PASS";
|
||||
return "$name rejected the login — check $k in Auth settings, below.";
|
||||
}
|
||||
|
||||
// ── NPM ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
function vv_npm_token(): string {
|
||||
@@ -129,13 +177,19 @@ function vv_npm_raw(string $method, string $path, array $data, string $token, ar
|
||||
if ($data && in_array($method, ['POST', 'PUT'], true))
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
||||
$body = curl_exec($ch);
|
||||
vv_auth_last_transport([
|
||||
'errno' => curl_errno($ch),
|
||||
'error' => curl_error($ch),
|
||||
'code' => (int) curl_getinfo($ch, CURLINFO_HTTP_CODE),
|
||||
]);
|
||||
curl_close($ch);
|
||||
return json_decode($body ?: '{}', true) ?: [];
|
||||
}
|
||||
|
||||
function vv_npm_req(string $method, string $path, array $data = []): array {
|
||||
if ($miss = vv_auth_creds_missing('npm')) return ['_err' => $miss];
|
||||
$token = vv_npm_token();
|
||||
if (!$token) return ['_err' => 'NPM auth failed — check credentials in host conf'];
|
||||
if (!$token) return ['_err' => vv_auth_token_err('npm', vv_auth_conf()['npm_url'])];
|
||||
return vv_npm_raw($method, $path, $data, $token);
|
||||
}
|
||||
|
||||
@@ -146,9 +200,14 @@ function vv_npm_list_proxies(): array {
|
||||
return ['ok' => true, 'proxies' => $list];
|
||||
}
|
||||
|
||||
// Returns a list, always. An auth failure arrives here as a map with an _err key, and the
|
||||
// is_array() check passed it straight through as if it were the certificates — the caller then
|
||||
// held an object where it expected an array and lost .find() on it. The contract is a list, so a
|
||||
// failure is an empty one; vv_npm_list_proxies() runs on the same page and reports the reason.
|
||||
function vv_npm_list_certs(): array {
|
||||
$list = vv_npm_req('GET', '/api/nginx/certificates');
|
||||
return is_array($list) ? $list : [];
|
||||
if (!is_array($list) || isset($list['_err'])) return [];
|
||||
return array_values($list);
|
||||
}
|
||||
|
||||
function vv_npm_create_proxy(array $data): array {
|
||||
@@ -189,6 +248,11 @@ function vv_lldap_token(): string {
|
||||
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
|
||||
]);
|
||||
$body = curl_exec($ch);
|
||||
vv_auth_last_transport([
|
||||
'errno' => curl_errno($ch),
|
||||
'error' => curl_error($ch),
|
||||
'code' => (int) curl_getinfo($ch, CURLINFO_HTTP_CODE),
|
||||
]);
|
||||
curl_close($ch);
|
||||
$resp = json_decode($body ?: '{}', true) ?: [];
|
||||
$token = $resp['token'] ?? '';
|
||||
@@ -201,8 +265,9 @@ function vv_lldap_token(): string {
|
||||
|
||||
function vv_lldap_gql(string $query, array $variables = []): array {
|
||||
$conf = vv_auth_conf();
|
||||
if ($miss = vv_auth_creds_missing('lldap')) return ['errors' => [['message' => $miss]]];
|
||||
$token = vv_lldap_token();
|
||||
if (!$token) return ['errors' => [['message' => 'lldap auth failed — check credentials']]];
|
||||
if (!$token) return ['errors' => [['message' => vv_auth_token_err('lldap', $conf['lldap_url'])]]];
|
||||
|
||||
$ch = curl_init($conf['lldap_url'] . '/api/graphql');
|
||||
curl_setopt_array($ch, [
|
||||
@@ -259,8 +324,9 @@ function vv_lldap_delete_user(string $id): array {
|
||||
|
||||
function vv_lldap_set_password(string $userId, string $password): array {
|
||||
$conf = vv_auth_conf();
|
||||
if ($miss = vv_auth_creds_missing('lldap')) return ['ok' => false, 'error' => $miss];
|
||||
$token = vv_lldap_token();
|
||||
if (!$token) return ['ok' => false, 'error' => 'lldap auth failed'];
|
||||
if (!$token) return ['ok' => false, 'error' => vv_auth_token_err('lldap', $conf['lldap_url'])];
|
||||
|
||||
$ch = curl_init($conf['lldap_url'] . '/auth/admin/resetPassword');
|
||||
curl_setopt_array($ch, [
|
||||
|
||||
@@ -124,6 +124,12 @@ const VV_UI_SECTION_SURFACES = [
|
||||
'route' => 'Media Stack tab → Media settings'],
|
||||
['match' => 'docker', 'tab' => 'Docker',
|
||||
'route' => 'Docker tab → Docker settings'],
|
||||
// NPM, lldap and Authelia are the three services the Auth tab drives, and Certificate Monitor
|
||||
// is what its Certs panel reports. The credentials in particular belong on the page that fails
|
||||
// without them: a blank NPM_USER surfaces there as a refused login, and the fix is two panels
|
||||
// away rather than in a hundred-and-twenty-field catch-all.
|
||||
['match' => 'NginxProxyManager|lldap|Authelia|Certificate Monitor', 'tab' => 'Auth',
|
||||
'route' => 'Auth tab → Auth settings'],
|
||||
|
||||
// Everything that is not structurally excluded. The catch-all exists so no ordinary setting
|
||||
// is reachable only by editing a file — a settings page whose answer to a third of the conf
|
||||
|
||||
@@ -41,6 +41,9 @@
|
||||
// include/auth.php required directly for initial render
|
||||
// api/auth.php mutations
|
||||
// api/cert.php certificate status
|
||||
// api/confform.php inline conf edits → include/confui.php
|
||||
require_once dirname(__DIR__) . '/include/confui.php';
|
||||
require_once dirname(__DIR__) . '/include/ai_chat.php';
|
||||
?>
|
||||
<style>
|
||||
/* ── Toolbar ─────────────────────────────────────────────────────────────── */
|
||||
@@ -271,6 +274,44 @@ $isOwner = vv_is_owner();
|
||||
<div class="vv-au-modal" id="vv-au-modal"></div>
|
||||
</div>
|
||||
|
||||
<?php if (vv_ai_ui_on()): ?>
|
||||
<div class="vv-card" id="vv-au-ai-card" style="margin-top:12px;">
|
||||
<?php
|
||||
// The factory, the profile registry and the store are three separate emits and none implies
|
||||
// the others — omitting any renders a chat that looks complete and dies on the first click.
|
||||
vv_ai_profiles_script();
|
||||
vv_ai_chat_store_script();
|
||||
vv_ai_chat_assets();
|
||||
// Scoped to the tab. This page's vocabulary is the part of the stack least likely to be in
|
||||
// anyone's head — a proxy host, a forward target, an Authelia policy and an LDAP group are four
|
||||
// different objects that all end up deciding whether one person can open one URL, and the
|
||||
// question is nearly always "which of these is stopping me".
|
||||
vv_ai_chat_markup('vv-au-ai', [
|
||||
'profile' => 'varaverk',
|
||||
'compact' => true,
|
||||
'title' => 'Assistant',
|
||||
'scopeLabel' => 'Auth',
|
||||
'empty' => 'Ask about a proxy host, a rule, a group, or why a login is being refused.',
|
||||
'placeholder' => 'Ask about what is on this page…',
|
||||
]); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
// The three services this page drives, plus the cert thresholds behind the Certs panel. They had
|
||||
// no card at all until now, so the credentials that make the whole page work were reachable only
|
||||
// from the Settings catch-all or over SSH — and an empty NPM_USER renders here as a failed login,
|
||||
// which sends you looking for a password rather than a blank field.
|
||||
//
|
||||
// The match mirrors VV_UI_SECTION_SURFACES in confform.php and has to keep mirroring it: that
|
||||
// constant is what tells the assistant where to send someone, and this is what the page actually
|
||||
// draws. The two disagreeing means being given directions to a card that is not there.
|
||||
//
|
||||
// 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');
|
||||
?>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
@@ -44,7 +44,10 @@ Saved into `host1.conf`, which does not need to be opened by hand.
|
||||
|
||||
## Authelia
|
||||
|
||||
Route: Settings tab → All settings → *Authelia*
|
||||
Reachable from:
|
||||
|
||||
- Auth tab → Auth settings → *Authelia*
|
||||
- Settings tab → All settings → *Authelia*
|
||||
|
||||
Saved into `host1.conf`, which does not need to be opened by hand.
|
||||
|
||||
@@ -84,6 +87,7 @@ Reachable from:
|
||||
|
||||
- Scheduler tab → **Cert Monitor** → Config → *Certificate Monitor*
|
||||
- Scheduler tab → **Weekly Health Digest** → Config → *Certificate Monitor*
|
||||
- Auth tab → Auth settings → *Certificate Monitor*
|
||||
- Settings tab → All settings → *Certificate Monitor*
|
||||
|
||||
Saved into `host1.conf`, which does not need to be opened by hand.
|
||||
@@ -418,7 +422,10 @@ Saved into `host1.conf`, which does not need to be opened by hand.
|
||||
|
||||
## NginxProxyManager
|
||||
|
||||
Route: Settings tab → All settings → *NginxProxyManager*
|
||||
Reachable from:
|
||||
|
||||
- Auth tab → Auth settings → *NginxProxyManager*
|
||||
- Settings tab → All settings → *NginxProxyManager*
|
||||
|
||||
Saved into `host1.conf`, which does not need to be opened by hand.
|
||||
|
||||
@@ -714,7 +721,10 @@ Saved into `host1.conf`, which does not need to be opened by hand.
|
||||
|
||||
## lldap
|
||||
|
||||
Route: Settings tab → All settings → *lldap*
|
||||
Reachable from:
|
||||
|
||||
- Auth tab → Auth settings → *lldap*
|
||||
- Settings tab → All settings → *lldap*
|
||||
|
||||
Saved into `host1.conf`, which does not need to be opened by hand.
|
||||
|
||||
@@ -1178,6 +1188,7 @@ Reachable from:
|
||||
|
||||
- Scheduler tab → **Cert Monitor** → Config → *Certificate Monitor*
|
||||
- Scheduler tab → **Weekly Health Digest** → Config → *Certificate Monitor*
|
||||
- Auth tab → Auth settings → *Certificate Monitor*
|
||||
- Settings tab → All settings → *Certificate Monitor*
|
||||
|
||||
Saved into `master.conf`, which does not need to be opened by hand.
|
||||
|
||||
Reference in New Issue
Block a user