From 677195e58749414a650189cc9404e26d023d1515 Mon Sep 17 00:00:00 2001 From: Mirotin Artem Date: Thu, 26 Mar 2026 02:09:57 +0300 Subject: [PATCH] feat(api): add GET /v1/stats/users/active-ips endpoint Lightweight endpoint that returns only users with active TCP connections and their IP addresses. Calls only get_active_ips_for_users() without collecting recent IPs or building full UserInfo, significantly reducing CPU and memory overhead compared to /v1/stats/users. --- src/api/mod.rs | 13 +++++++++++++ src/api/model.rs | 6 ++++++ 2 files changed, 19 insertions(+) diff --git a/src/api/mod.rs b/src/api/mod.rs index c1e3557..c0eab87 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -42,6 +42,7 @@ use events::ApiEventStore; use http_utils::{error_response, read_json, read_optional_json, success_response}; use model::{ ApiFailure, CreateUserRequest, HealthData, PatchUserRequest, RotateSecretRequest, SummaryData, + UserActiveIps, }; use runtime_edge::{ EdgeConnectionsCacheEntry, build_runtime_connections_summary_data, @@ -362,6 +363,18 @@ async fn handle( ); Ok(success_response(StatusCode::OK, data, revision)) } + ("GET", "/v1/stats/users/active-ips") => { + let revision = current_revision(&shared.config_path).await?; + let usernames: Vec<_> = cfg.access.users.keys().cloned().collect(); + let active_ips_map = shared.ip_tracker.get_active_ips_for_users(&usernames).await; + let mut data: Vec = active_ips_map + .into_iter() + .filter(|(_, ips)| !ips.is_empty()) + .map(|(username, active_ips)| UserActiveIps { username, active_ips }) + .collect(); + data.sort_by(|a, b| a.username.cmp(&b.username)); + Ok(success_response(StatusCode::OK, data, revision)) + } ("GET", "/v1/stats/users") | ("GET", "/v1/users") => { let revision = current_revision(&shared.config_path).await?; let (detected_ip_v4, detected_ip_v6) = shared.detected_link_ips(); diff --git a/src/api/model.rs b/src/api/model.rs index 8ae0c0b..164042f 100644 --- a/src/api/model.rs +++ b/src/api/model.rs @@ -442,6 +442,12 @@ pub(super) struct UserInfo { pub(super) links: UserLinks, } +#[derive(Serialize)] +pub(super) struct UserActiveIps { + pub(super) username: String, + pub(super) active_ips: Vec, +} + #[derive(Serialize)] pub(super) struct CreateUserResponse { pub(super) user: UserInfo,