From 2fb68cd20ed21636ea07f40f3d1e7bb689797f5b Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Tue, 29 Sep 2026 22:00:55 -0500 Subject: [PATCH] feat(dashboard): Network connections name the web client's dashboard user Web clients are one entry per address and signed-in account, with user and account_id; the user is shown without network_ips too, the address then masked as before. Co-Authored-By: Claude Opus 5.5 --- dDashboardServer/routes/NetworkView.cpp | 12 ++++++--- dDashboardServer/routes/NetworkView.h | 8 +++--- tests/dWebTests/NetworkViewTests.cpp | 33 +++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 6 deletions(-) diff --git a/dDashboardServer/routes/NetworkView.cpp b/dDashboardServer/routes/NetworkView.cpp index 4434c8e26..66bf5635f 100644 --- a/dDashboardServer/routes/NetworkView.cpp +++ b/dDashboardServer/routes/NetworkView.cpp @@ -112,9 +112,11 @@ namespace NetworkView { for (const auto& c : server.connections) { const std::string kind = c.peer != TrafficStats::Peer::CLIENTS ? "server" : c.http ? "web" : "game"; // One entry per player (their account), else per connection: an address alone is not enough, since the - // servers' own links and players on the same machine or network share it + // servers' own links and players on the same machine or network share it. Web clients: one per address + // and signed-in dashboard account, so people behind one address are listed apart const std::string groupKey = kind == "game" && c.accountId ? "account:" + std::to_string(c.accountId) - : kind + "|" + c.address + (c.http ? std::string() : ":" + std::to_string(c.port)); + : kind + "|" + c.address + (c.http ? std::string() : ":" + std::to_string(c.port)) + + (c.http && c.accountId ? "|" + std::to_string(c.accountId) : std::string()); auto& group = groups[groupKey]; if (group.address.empty()) group.address = c.address; group.seconds = std::max(group.seconds, seconds); @@ -157,7 +159,11 @@ namespace NetworkView { entry["address"] = showAddresses ? group->address : MaskAddress(group->address, salt); entry["kind"] = group->kind; entry["servers"] = group->servers; - if (group->accountId) { + if (group->accountId && group->kind == "web") { + // The dashboard account the requests were signed in as (a session or an API key) + entry["account_id"] = group->accountId; + entry["user"] = group->account; + } else if (group->accountId) { entry["account_id"] = group->accountId; entry["account"] = group->account; if (group->characterId) { diff --git a/dDashboardServer/routes/NetworkView.h b/dDashboardServer/routes/NetworkView.h index dddb291da..913bb125d 100644 --- a/dDashboardServer/routes/NetworkView.h +++ b/dDashboardServer/routes/NetworkView.h @@ -26,9 +26,11 @@ namespace NetworkView { nlohmann::json Summary(const TrafficHistory& history, int64_t now, int64_t onlineSeconds, const Labeler& label); /** - * The remote ends of every server (from each one's last report) grouped by address: game clients by their RakNet - * connections, HTTP clients of the dashboard and the UGC server, and the servers' own links. Rates per second. - * Without `showAddresses` each address is replaced by a token that stays the same for the same address and `salt`. + * The remote ends of every server (from each one's last report): game clients by player (else by RakNet connection), + * HTTP clients of the dashboard and the UGC server by address and signed-in account (`user`, the dashboard account + * the requests were signed in as), and the servers' own links. Rates per second. Account and user names are always + * included; without `showAddresses` each address is replaced by a token that stays the same for the same address + * and `salt`, and ports are left out. */ nlohmann::json Connections(const TrafficHistory& history, int64_t now, int64_t onlineSeconds, bool showAddresses, uint64_t salt, const Labeler& label); diff --git a/tests/dWebTests/NetworkViewTests.cpp b/tests/dWebTests/NetworkViewTests.cpp index ce9ebb238..d5ec0d23f 100644 --- a/tests/dWebTests/NetworkViewTests.cpp +++ b/tests/dWebTests/NetworkViewTests.cpp @@ -150,3 +150,36 @@ TEST(NetworkViewTest, ServerLinksOnThePlayersAddressStayApart) { } } } + +TEST(NetworkViewTest, WebClientsBySignedInAccountWithTheirUserName) { + // Two staff members behind one address and a request that wasn't signed in, on the dashboard + TrafficHistory history; + Report dashboard; + dashboard.seconds = { Second{ .time = NOW - 1 } }; + dashboard.connections = { + Connection{ .address = "198.51.100.7", .peer = Peer::CLIENTS, .http = true, .packetsIn = 4, .packetsOut = 4, .bytesIn = 400, .bytesOut = 40000, .accountId = 7, .account = "alice" }, + Connection{ .address = "198.51.100.7", .peer = Peer::CLIENTS, .http = true, .packetsIn = 2, .packetsOut = 2, .bytesIn = 200, .bytesOut = 20000, .accountId = 9, .account = "bob" }, + Connection{ .address = "198.51.100.7", .peer = Peer::CLIENTS, .http = true, .packetsIn = 1, .packetsOut = 1, .bytesIn = 100, .bytesOut = 100 } }; + dashboard.hasConnections = true; + history.Ingest(static_cast(ServiceType::DASHBOARD), 0, 0, dashboard, NOW); + + const auto shown = NetworkView::Connections(history, NOW, 20, true, 5, Label); + ASSERT_EQ(shown["peers"].size(), 3u); + std::vector users; + for (const auto& p : shown["peers"]) { + EXPECT_EQ(p["kind"], "web"); + EXPECT_EQ(p["address"], "198.51.100.7"); + EXPECT_FALSE(p.contains("account")); // a web client's name is its dashboard user, not a player + users.push_back(p.value("user", std::string("-"))); + } + EXPECT_EQ(users, (std::vector{ "alice", "bob", "-" })); // busiest first + EXPECT_EQ(shown["peers"][0]["account_id"], 7); + + // Without network_ips: still the user names, and the address as its token (the same for all three) + const auto hidden = NetworkView::Connections(history, NOW, 20, false, 5, Label); + EXPECT_EQ(hidden.dump().find("198.51.100.7"), std::string::npos); + ASSERT_EQ(hidden["peers"].size(), 3u); + EXPECT_EQ(hidden["peers"][0]["user"], "alice"); + EXPECT_EQ(hidden["peers"][1]["user"], "bob"); + for (const auto& p : hidden["peers"]) EXPECT_EQ(p["address"], NetworkView::MaskAddress("198.51.100.7", 5)); +}