). The node's sysop is // always the implicit default admin; this stores ADDITIONAL admins. // // Owners (user_owns_node) may LIST the admins of their nodes; only site admins // (admin_level >= 2) may add or remove entries — users must not grant // themselves or others admin rights. require __DIR__ . '/lib.php'; $uuid = require_login(); csrf_check(); $in = json_in(); $action = strtolower(trim((string)($in['action'] ?? 'list'))); function na_valid_call(string $cs): bool { return (bool)preg_match('/^[A-Z0-9]{1,3}[0-9][A-Z0-9]{0,3}[A-Z](-[A-Z0-9]{1,10})?$/', $cs); } $node = strtoupper(trim((string)($in['node'] ?? ''))); $admin = strtoupper(trim((string)($in['admin'] ?? ''))); if ($node === '' || !node_callsign_valid($node)) { json_out(['ok' => false, 'error' => 'invalid_node'], 400); } $r = redis(); $key = rkey("node_admins:$node"); // Listing requires ownership (callsign/email/node-admin — user_owns_node). if (!user_owns_node($uuid, $node)) { json_out(['ok' => false, 'error' => 'forbidden'], 403); } $stored = $r->sMembers($key) ?: []; if ($action === 'add' || $action === 'remove') { // Changing the list is site-admin only. if (acct_admin_level($uuid) < 2) { json_out(['ok' => false, 'error' => 'forbidden'], 403); } if ($admin === '' || !na_valid_call($admin)) { json_out(['ok' => false, 'error' => 'invalid_admin'], 400); } if ($action === 'add') { $r->sAdd($key, $admin); } else { $r->sRem($key, $admin); } $stored = $r->sMembers($key) ?: []; } elseif ($action !== 'list') { json_out(['ok' => false, 'error' => 'invalid_action'], 400); } sort($stored); json_out(['ok' => true, 'node' => $node, 'admins' => $stored]);