diff --git a/Plugin/unraid/include/auth.php b/Plugin/unraid/include/auth.php index e896dde..84517ed 100644 --- a/Plugin/unraid/include/auth.php +++ b/Plugin/unraid/include/auth.php @@ -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, [ diff --git a/Plugin/unraid/include/confform.php b/Plugin/unraid/include/confform.php index 31edf23..2809bb2 100644 --- a/Plugin/unraid/include/confform.php +++ b/Plugin/unraid/include/confform.php @@ -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 diff --git a/Plugin/unraid/pages/auth.php b/Plugin/unraid/pages/auth.php index 8f1ccde..c6d5d90 100644 --- a/Plugin/unraid/pages/auth.php +++ b/Plugin/unraid/pages/auth.php @@ -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'; ?>