// POST action=npm_update id, data= // POST action=npm_delete id // POST action=npm_toggle id, enabled=0|1 // POST action=lldap_create_user uid, email, display_name, password // POST action=lldap_update_user uid, email, display_name // POST action=lldap_delete_user uid // POST action=lldap_set_password uid, password // POST action=lldap_create_group name // POST action=lldap_delete_group id // POST action=lldap_add_to_group uid, gid // POST action=lldap_remove_from_group uid, gid // POST action=authelia_save rules=, default_policy // // RESPONSE // Whatever the invoked library call returns — ['ok' => bool] with a payload or an error, // or an _err key on a failed remote call. npm_certs is wrapped as {"ok":true,"certs":[…]}. // {"ok":false,"error":"Unknown action: …"} for anything outside the lists above. // // DEPENDS ON // include/auth.php vv_npm_*(), vv_lldap_*(), vv_authelia_read_rules(), // vv_authelia_write_rules() // ═══════════════════════════════════════════════════════════════════════════════════════════════ header('Content-Type: application/json'); require_once dirname(__DIR__) . '/include/auth.php'; // Which panel each action belongs to. AUTH_STACK decides which panels the tab draws, and this is // the same decision applied to the endpoint — a tab left open from before a switch would otherwise // keep writing to the stack that is no longer in force, which on this page means editing the // directory or the rules of a system nobody is authenticating against any more. // // Proxies and certs are Nginx Proxy Manager's, not the identity stack's, so they are listed under // panels every stack carries rather than gated to one. const VV_AUTH_ACTION_PANEL = [ // GET 'npm_proxies' => 'proxies', 'npm_certs' => 'proxies', 'npm_stats' => 'proxies', 'npm_uptime' => 'proxies', 'lldap_users' => 'users', 'lldap_groups' => 'users', 'lldap_avatar' => 'users', 'authelia_rules' => 'acl', // POST 'npm_create' => 'proxies', 'npm_update' => 'proxies', 'npm_delete' => 'proxies', 'npm_toggle' => 'proxies', 'lldap_create_user' => 'users', 'lldap_update_user' => 'users', 'lldap_delete_user' => 'users', 'lldap_set_password' => 'users', 'lldap_set_avatar' => 'users', 'lldap_remove_avatar' => 'users', 'lldap_create_group' => 'users', 'lldap_delete_group' => 'users', 'lldap_rename_group' => 'users', 'lldap_add_to_group' => 'users', 'lldap_remove_from_group' => 'users', 'authelia_save' => 'acl', ]; // Same windowing rule as uptime_probe.php: buckets are keyed by time, so "the last N" is a key // sort rather than an assumption that every period produced a sample. function vv_uptime_window_api(array $buckets, int $n): ?float { if (!$buckets) return null; krsort($buckets); $u = $t = 0; foreach (array_slice($buckets, 0, $n, true) as $b) { $u += $b['u'] ?? 0; $t += $b['t'] ?? 0; } return $t > 0 ? round($u / $t * 100, 2) : null; } function vv_auth_action_allowed(string $action): bool { $panel = VV_AUTH_ACTION_PANEL[$action] ?? null; // Unmapped actions are left to the existing "Unknown action" answer rather than being refused // here, so a new action is never silently blocked by a table someone forgot to extend. return $panel === null || vv_auth_panel_on($panel); } function vv_auth_action_refusal(string $action): array { $d = vv_auth_stack_def(); return ['ok' => false, 'error' => 'AUTH_STACK is "' . vv_auth_stack() . '" (' . $d['label'] . '), which does not serve this request. Reload the Auth tab.']; } if ($_SERVER['REQUEST_METHOD'] === 'GET') { $action = $_GET['action'] ?? ''; if (!vv_auth_action_allowed($action)) { echo json_encode(vv_auth_action_refusal($action)); exit; } // The one route here that does not answer in JSON — it streams the stored JPEG so the page can // point an at it, rather than carrying 470 KB of base64 through the user list on every // load. Handled before the match so the Content-Type set above is replaced rather than sent // alongside image bytes. if ($action === 'lldap_avatar') { $raw = vv_lldap_avatar((string) ($_GET['uid'] ?? '')); if ($raw === '') { header('Content-Type: application/json'); http_response_code(404); echo json_encode(['ok' => false, 'error' => 'No avatar']); exit; } header('Content-Type: image/jpeg'); header('Content-Length: ' . strlen($raw)); // Private, because this is a photograph of a person behind an authenticated admin page, // and must not be held by anything between here and the browser. Short, because the // operator changing an avatar expects to see it change. header('Cache-Control: private, max-age=60'); echo $raw; exit; } // Per-host request and byte totals, written by Tools/npm_access_stats.sh. Served rather than // computed: the logs behind these numbers are 475 MB and reading them is a scheduled job, not // something a page load can do. if ($action === 'npm_stats') { $f = rtrim(defined('DB_DIR') ? DB_DIR : (DATA_DIR . '/db'), '/') . '/npm_access.json'; $s = is_file($f) ? (json_decode((string) @file_get_contents($f), true) ?: []) : []; echo json_encode(['ok' => true, 'hosts' => $s['hosts'] ?? [], 'last_pass' => $s['last_pass'] ?? null]); exit; } // Per-domain uptime, written by Tools/uptime_probe.sh every minute. Keyed by hostname rather // than proxy id, because that is what was probed. if ($action === 'npm_uptime') { $f = rtrim(defined('DB_DIR') ? DB_DIR : (DATA_DIR . '/db'), '/') . '/uptime.json'; $u = is_file($f) ? (json_decode((string) @file_get_contents($f), true) ?: []) : []; $out = []; foreach ($u['domains'] ?? [] as $dom => $r) { // Only what the row draws. The hourly and daily buckets are dozens of entries per // domain and the page shows three percentages and a strip. $out[$dom] = [ 'state' => $r['state'] ?? null, 'samples' => array_slice($r['samples'] ?? [], -60), 'h1' => vv_uptime_window_api($r['hours'] ?? [], 1), 'h24' => vv_uptime_window_api($r['hours'] ?? [], 24), 'd30' => vv_uptime_window_api($r['days'] ?? [], 30), 'last_change' => $r['last_change'] ?? null, 'last_detail' => $r['last_detail'] ?? null, 'last_ms' => $r['last_ms'] ?? null, ]; } echo json_encode(['ok' => true, 'domains' => $out, 'last_pass' => $u['last_pass'] ?? null]); exit; } $result = match ($action) { 'npm_proxies' => vv_npm_list_proxies(), 'npm_certs' => ['ok' => true, 'certs' => vv_npm_list_certs()], 'lldap_users' => vv_lldap_list_users(), 'lldap_groups' => vv_lldap_list_groups(), 'authelia_rules' => vv_authelia_read_rules(), default => ['ok' => false, 'error' => 'Unknown action: ' . $action], }; echo json_encode($result); exit; } if ($_SERVER['REQUEST_METHOD'] !== 'POST') { http_response_code(405); echo json_encode(['ok' => false, 'error' => 'GET or POST only']); exit; } $action = trim($_POST['action'] ?? ''); if (!vv_auth_action_allowed($action)) { echo json_encode(vv_auth_action_refusal($action)); exit; } $result = match ($action) { // NPM 'npm_create' => vv_npm_create_proxy(json_decode($_POST['data'] ?? '{}', true) ?: []), 'npm_update' => vv_npm_update_proxy((int)($_POST['id'] ?? 0), json_decode($_POST['data'] ?? '{}', true) ?: []), 'npm_delete' => vv_npm_delete_proxy((int)($_POST['id'] ?? 0)), 'npm_toggle' => vv_npm_toggle_proxy((int)($_POST['id'] ?? 0), ($_POST['enabled'] ?? '0') === '1'), // lldap 'lldap_create_user' => vv_lldap_create_user($_POST['uid'] ?? '', $_POST['email'] ?? '', $_POST['display_name'] ?? '', $_POST['password'] ?? '', $_POST['first_name'] ?? '', $_POST['last_name'] ?? ''), // isset, not ??'' — the update helper reads null as "not offered" and '' as "cleared", and // collapsing the two here would erase a first name every time a form omitted the field. 'lldap_update_user' => vv_lldap_update_user($_POST['uid'] ?? '', $_POST['email'] ?? '', $_POST['display_name'] ?? '', isset($_POST['first_name']) ? (string) $_POST['first_name'] : null, isset($_POST['last_name']) ? (string) $_POST['last_name'] : null), 'lldap_set_avatar' => vv_lldap_set_avatar($_POST['uid'] ?? '', $_POST['avatar'] ?? ''), 'lldap_remove_avatar' => vv_lldap_remove_avatar($_POST['uid'] ?? ''), 'lldap_rename_group' => vv_lldap_rename_group((int)($_POST['id'] ?? 0), $_POST['name'] ?? ''), 'lldap_delete_user' => vv_lldap_delete_user($_POST['uid'] ?? ''), 'lldap_set_password' => vv_lldap_set_password($_POST['uid'] ?? '', $_POST['password'] ?? ''), 'lldap_create_group' => vv_lldap_create_group($_POST['name'] ?? ''), 'lldap_delete_group' => vv_lldap_delete_group((int)($_POST['id'] ?? 0)), 'lldap_add_to_group' => vv_lldap_add_to_group($_POST['uid'] ?? '', (int)($_POST['gid'] ?? 0)), 'lldap_remove_from_group' => vv_lldap_remove_from_group($_POST['uid'] ?? '', (int)($_POST['gid'] ?? 0)), // Authelia 'authelia_save' => vv_authelia_write_rules(json_decode($_POST['rules'] ?? '[]', true) ?: [], $_POST['default_policy'] ?? 'deny', $_POST['default_note'] ?? ''), default => ['ok' => false, 'error' => 'Unknown action: ' . $action], }; echo json_encode($result);