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.
|
// Auth failure is reported, not retried into a lockout.
|
||||||
// A failed token fetch returns an _err string immediately. Nothing loops on bad
|
// 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 —
|
// 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
|
// ['ok' => bool] or an _err key — so no caller has to distinguish an exception from a
|
||||||
// legitimately empty list.
|
// legitimately empty list.
|
||||||
//
|
//
|
||||||
// EXPORTS
|
// 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(),
|
// 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()
|
// 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(),
|
// 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 ───────────────────────────────────────────────────────────────────────
|
// ── NPM ───────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function vv_npm_token(): string {
|
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))
|
if ($data && in_array($method, ['POST', 'PUT'], true))
|
||||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
||||||
$body = curl_exec($ch);
|
$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);
|
curl_close($ch);
|
||||||
return json_decode($body ?: '{}', true) ?: [];
|
return json_decode($body ?: '{}', true) ?: [];
|
||||||
}
|
}
|
||||||
|
|
||||||
function vv_npm_req(string $method, string $path, array $data = []): array {
|
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();
|
$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);
|
return vv_npm_raw($method, $path, $data, $token);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,9 +200,14 @@ function vv_npm_list_proxies(): array {
|
|||||||
return ['ok' => true, 'proxies' => $list];
|
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 {
|
function vv_npm_list_certs(): array {
|
||||||
$list = vv_npm_req('GET', '/api/nginx/certificates');
|
$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 {
|
function vv_npm_create_proxy(array $data): array {
|
||||||
@@ -189,6 +248,11 @@ function vv_lldap_token(): string {
|
|||||||
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
|
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
|
||||||
]);
|
]);
|
||||||
$body = curl_exec($ch);
|
$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);
|
curl_close($ch);
|
||||||
$resp = json_decode($body ?: '{}', true) ?: [];
|
$resp = json_decode($body ?: '{}', true) ?: [];
|
||||||
$token = $resp['token'] ?? '';
|
$token = $resp['token'] ?? '';
|
||||||
@@ -201,8 +265,9 @@ function vv_lldap_token(): string {
|
|||||||
|
|
||||||
function vv_lldap_gql(string $query, array $variables = []): array {
|
function vv_lldap_gql(string $query, array $variables = []): array {
|
||||||
$conf = vv_auth_conf();
|
$conf = vv_auth_conf();
|
||||||
|
if ($miss = vv_auth_creds_missing('lldap')) return ['errors' => [['message' => $miss]]];
|
||||||
$token = vv_lldap_token();
|
$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');
|
$ch = curl_init($conf['lldap_url'] . '/api/graphql');
|
||||||
curl_setopt_array($ch, [
|
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 {
|
function vv_lldap_set_password(string $userId, string $password): array {
|
||||||
$conf = vv_auth_conf();
|
$conf = vv_auth_conf();
|
||||||
|
if ($miss = vv_auth_creds_missing('lldap')) return ['ok' => false, 'error' => $miss];
|
||||||
$token = vv_lldap_token();
|
$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');
|
$ch = curl_init($conf['lldap_url'] . '/auth/admin/resetPassword');
|
||||||
curl_setopt_array($ch, [
|
curl_setopt_array($ch, [
|
||||||
|
|||||||
@@ -124,6 +124,12 @@ const VV_UI_SECTION_SURFACES = [
|
|||||||
'route' => 'Media Stack tab → Media settings'],
|
'route' => 'Media Stack tab → Media settings'],
|
||||||
['match' => 'docker', 'tab' => 'Docker',
|
['match' => 'docker', 'tab' => 'Docker',
|
||||||
'route' => 'Docker tab → Docker settings'],
|
'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
|
// 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
|
// 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
|
// include/auth.php required directly for initial render
|
||||||
// api/auth.php mutations
|
// api/auth.php mutations
|
||||||
// api/cert.php certificate status
|
// 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>
|
<style>
|
||||||
/* ── Toolbar ─────────────────────────────────────────────────────────────── */
|
/* ── Toolbar ─────────────────────────────────────────────────────────────── */
|
||||||
@@ -271,6 +274,44 @@ $isOwner = vv_is_owner();
|
|||||||
<div class="vv-au-modal" id="vv-au-modal"></div>
|
<div class="vv-au-modal" id="vv-au-modal"></div>
|
||||||
</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>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|||||||
@@ -44,7 +44,10 @@ Saved into `host1.conf`, which does not need to be opened by hand.
|
|||||||
|
|
||||||
## Authelia
|
## 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.
|
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 → **Cert Monitor** → Config → *Certificate Monitor*
|
||||||
- Scheduler tab → **Weekly Health Digest** → Config → *Certificate Monitor*
|
- Scheduler tab → **Weekly Health Digest** → Config → *Certificate Monitor*
|
||||||
|
- Auth tab → Auth settings → *Certificate Monitor*
|
||||||
- Settings tab → All settings → *Certificate Monitor*
|
- Settings tab → All settings → *Certificate Monitor*
|
||||||
|
|
||||||
Saved into `host1.conf`, which does not need to be opened by hand.
|
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
|
## 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.
|
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
|
## 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.
|
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 → **Cert Monitor** → Config → *Certificate Monitor*
|
||||||
- Scheduler tab → **Weekly Health Digest** → Config → *Certificate Monitor*
|
- Scheduler tab → **Weekly Health Digest** → Config → *Certificate Monitor*
|
||||||
|
- Auth tab → Auth settings → *Certificate Monitor*
|
||||||
- Settings tab → All settings → *Certificate Monitor*
|
- Settings tab → All settings → *Certificate Monitor*
|
||||||
|
|
||||||
Saved into `master.conf`, which does not need to be opened by hand.
|
Saved into `master.conf`, which does not need to be opened by hand.
|
||||||
|
|||||||
Reference in New Issue
Block a user