mirror of
https://github.com/kunkundi/crossdesk-server.git
synced 2026-10-26 01:34:33 +08:00
[feat] display client version and platform
This commit is contained in:
1 parent
f1807f4c2b
commit
695193ab3c
8 files changed
+119
-3
No files matched your search
@@ -610,6 +610,8 @@ AdminHttpResponse AdminController::HandleOverview(
|
||||
devices.push_back({{"id", device.device_id},
|
||||
{"online", device.online},
|
||||
{"kind", ClientKind(device.device_id)},
|
||||
{"client_version", device.client_version},
|
||||
{"client_platform", device.client_platform},
|
||||
{"updated_at", device.updated_at},
|
||||
{"last_online_at",
|
||||
device.online ? 0 : device.updated_at},
|
||||
|
||||
@@ -82,11 +82,15 @@
|
||||
.sessions-table td:nth-child(1), .sessions-table td:nth-child(2) { word-break: break-all; }
|
||||
.device-id { font-weight: 600; word-break: break-all; }
|
||||
.subline { display: block; color: #667085; font-size: 12px; margin-top: 3px; }
|
||||
.client-meta { align-items: center; display: flex; flex-wrap: wrap; gap: 6px; margin-top: 3px; }
|
||||
.client-meta .subline { margin-top: 0; }
|
||||
.badge { border-radius: 999px; display: inline-flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 700; line-height: 1; padding: 5px 8px; white-space: nowrap; }
|
||||
.badge.online { background: #dcfae6; color: #067647; }
|
||||
.badge.offline { background: #f2f4f7; color: #475467; }
|
||||
.badge.active { background: #fef0c7; color: #b54708; }
|
||||
.badge.web { background: #e0f2fe; color: #026aa2; }
|
||||
.badge.platform { background: #ecfdf3; color: #027a48; }
|
||||
.badge.version { background: #eef4ff; color: #3538cd; }
|
||||
.presence-table td:nth-child(2) { display: grid; gap: 4px; justify-items: start; }
|
||||
.presence-table td:nth-child(2) .badge { min-width: 58px; }
|
||||
.details-row td { background: #f9fafb; }
|
||||
|
||||
+17
-1
@@ -559,6 +559,13 @@
|
||||
return Number(activeCount) > 0 ? formatDuration(value) : '-';
|
||||
}
|
||||
|
||||
function platformLabel(platform) {
|
||||
if (platform === 'windows') return 'Windows';
|
||||
if (platform === 'macos') return 'macOS';
|
||||
if (platform === 'linux') return 'Linux';
|
||||
return platform || '';
|
||||
}
|
||||
|
||||
function appendDetailItem(parent, label, value, className, dataset) {
|
||||
const item = document.createElement('div');
|
||||
appendText(item, 'span', label);
|
||||
@@ -590,7 +597,16 @@
|
||||
const clientCell = document.createElement('td');
|
||||
labelCell(clientCell, 'Client');
|
||||
appendText(clientCell, 'div', device.id, 'device-id');
|
||||
appendText(clientCell, 'span', device.kind === 'web' ? 'web client' : 'PC client', 'subline');
|
||||
const clientMeta = document.createElement('div');
|
||||
clientMeta.className = 'client-meta';
|
||||
appendText(clientMeta, 'span', device.kind === 'web' ? 'web client' : 'PC client', 'subline');
|
||||
if (device.kind !== 'web' && device.client_platform) {
|
||||
appendBadge(clientMeta, platformLabel(device.client_platform), 'platform');
|
||||
}
|
||||
if (device.kind !== 'web' && device.client_version) {
|
||||
appendBadge(clientMeta, device.client_version, 'version');
|
||||
}
|
||||
clientCell.appendChild(clientMeta);
|
||||
row.appendChild(clientCell);
|
||||
|
||||
const statusCell = document.createElement('td');
|
||||
|
||||
@@ -426,6 +426,8 @@ void DeviceDBManager::InitDB() {
|
||||
EnsureTextColumn(db_, "device_presence", "geo_region");
|
||||
EnsureTextColumn(db_, "device_presence", "geo_city");
|
||||
EnsureTextColumn(db_, "device_presence", "geo_location");
|
||||
EnsureTextColumn(db_, "device_presence", "client_version");
|
||||
EnsureTextColumn(db_, "device_presence", "client_platform");
|
||||
|
||||
const char* sql_presence_backfill =
|
||||
"UPDATE device_presence SET online_since = updated_at "
|
||||
@@ -1040,6 +1042,38 @@ bool DeviceDBManager::UpdateDeviceNetworkInfo(
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool DeviceDBManager::UpdateDeviceClientInfo(
|
||||
const std::string& device_id, const std::string& client_version,
|
||||
const std::string& client_platform) {
|
||||
std::lock_guard<std::recursive_mutex> lock(db_mutex_);
|
||||
if (db_ == nullptr) {
|
||||
LOG_ERROR("Database is not initialized in UpdateDeviceClientInfo.");
|
||||
return false;
|
||||
}
|
||||
if (device_id.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const char* sql =
|
||||
"INSERT INTO device_presence "
|
||||
"(device_id, online, updated_at, client_version, client_platform) "
|
||||
"VALUES (?, 0, CAST(strftime('%s','now') AS INTEGER), ?, ?) "
|
||||
"ON CONFLICT(device_id) DO UPDATE SET "
|
||||
"client_version=excluded.client_version, "
|
||||
"client_platform=excluded.client_platform;";
|
||||
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
|
||||
return false;
|
||||
}
|
||||
sqlite3_bind_text(stmt, 1, device_id.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 2, client_version.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 3, client_platform.c_str(), -1, SQLITE_TRANSIENT);
|
||||
const bool ok = sqlite3_step(stmt) == SQLITE_DONE;
|
||||
sqlite3_finalize(stmt);
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool DeviceDBManager::StartRemoteControlSession(
|
||||
const std::string& transmission_id, const std::string& host_id,
|
||||
const std::string& guest_id) {
|
||||
@@ -1668,7 +1702,8 @@ std::vector<OnlineDeviceInfo> DeviceDBManager::ListOnlineDevices(
|
||||
"FROM remote_control_sessions "
|
||||
"WHERE normalized_host_id = device_presence.device_id), 0) "
|
||||
"AS total_controlled_seconds, "
|
||||
"client_ip, geo_country, geo_region, geo_city, geo_location "
|
||||
"client_ip, geo_country, geo_region, geo_city, geo_location, "
|
||||
"client_version, client_platform "
|
||||
"FROM device_presence "
|
||||
"WHERE online = 1 "
|
||||
"AND device_id NOT LIKE 'web-%' "
|
||||
@@ -1707,6 +1742,8 @@ std::vector<OnlineDeviceInfo> DeviceDBManager::ListOnlineDevices(
|
||||
info.region = ColumnText(stmt, 10);
|
||||
info.city = ColumnText(stmt, 11);
|
||||
info.location = ColumnText(stmt, 12);
|
||||
info.client_version = ColumnText(stmt, 13);
|
||||
info.client_platform = ColumnText(stmt, 14);
|
||||
result.push_back(info);
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
@@ -1771,7 +1808,8 @@ std::vector<OnlineDeviceInfo> DeviceDBManager::ListDevicePresence(
|
||||
"FROM remote_control_sessions "
|
||||
"WHERE normalized_host_id = device_presence.device_id), '') "
|
||||
"AS active_controlled_by, "
|
||||
"client_ip, geo_country, geo_region, geo_city, geo_location "
|
||||
"client_ip, geo_country, geo_region, geo_city, geo_location, "
|
||||
"client_version, client_platform "
|
||||
"FROM device_presence "
|
||||
"WHERE " +
|
||||
DevicePresenceFilterClause(filter, kind);
|
||||
@@ -1816,6 +1854,8 @@ std::vector<OnlineDeviceInfo> DeviceDBManager::ListDevicePresence(
|
||||
info.region = ColumnText(stmt, 16);
|
||||
info.city = ColumnText(stmt, 17);
|
||||
info.location = ColumnText(stmt, 18);
|
||||
info.client_version = ColumnText(stmt, 19);
|
||||
info.client_platform = ColumnText(stmt, 20);
|
||||
result.push_back(info);
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
@@ -42,6 +42,8 @@ struct OnlineDeviceInfo {
|
||||
std::string region;
|
||||
std::string city;
|
||||
std::string location;
|
||||
std::string client_version;
|
||||
std::string client_platform;
|
||||
};
|
||||
|
||||
struct ClientNetworkInfo {
|
||||
@@ -113,6 +115,9 @@ class DeviceDBManager {
|
||||
bool SetDeviceOnline(const std::string& device_id, bool online);
|
||||
bool UpdateDeviceNetworkInfo(const std::string& device_id,
|
||||
const ClientNetworkInfo& network_info);
|
||||
bool UpdateDeviceClientInfo(const std::string& device_id,
|
||||
const std::string& client_version,
|
||||
const std::string& client_platform);
|
||||
bool RecordRuntimeHeartbeat();
|
||||
bool StartRemoteControlSession(const std::string& transmission_id,
|
||||
const std::string& host_id,
|
||||
|
||||
@@ -18,6 +18,11 @@ bool GetStringField(const json& j, const char* key, std::string& value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ShouldTrackClientInfo(const std::string& user_id) {
|
||||
return !user_id.empty() && user_id.rfind("web-", 0) != 0 &&
|
||||
user_id.rfind("C-", 0) != 0;
|
||||
}
|
||||
|
||||
std::string PasswordFingerprint(const std::string& password) {
|
||||
unsigned char digest[SHA256_DIGEST_LENGTH];
|
||||
SHA256(reinterpret_cast<const unsigned char*>(password.data()),
|
||||
@@ -121,6 +126,10 @@ bool SignalNegotiation::login_user(websocketpp::connection_hdl hdl,
|
||||
transmission_manager_->BindHostToTransmission(ret_host_id, ret_host_id);
|
||||
|
||||
if (success) {
|
||||
if (ShouldTrackClientInfo(ret_host_id) &&
|
||||
!device_db_manager_->UpdateDeviceClientInfo(ret_host_id, "", "")) {
|
||||
LOG_WARN("Failed to clear client information for [{}]", ret_host_id);
|
||||
}
|
||||
json message = {{"type", "login"},
|
||||
{"user_id", return_host_id},
|
||||
{"status", "success"}};
|
||||
@@ -151,6 +160,42 @@ bool SignalNegotiation::login_user(websocketpp::connection_hdl hdl,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SignalNegotiation::client_info(websocketpp::connection_hdl hdl,
|
||||
const json& j) {
|
||||
std::string version;
|
||||
std::string platform;
|
||||
if (!GetStringField(j, "version", version) || version.empty() ||
|
||||
version.size() > 64) {
|
||||
LOG_ERROR("client_info missing or invalid field: version");
|
||||
return false;
|
||||
}
|
||||
if (!GetStringField(j, "platform", platform) || platform.empty() ||
|
||||
platform.size() > 32) {
|
||||
LOG_ERROR("client_info missing or invalid field: platform");
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string user_id = transmission_manager_->GetUserId(hdl);
|
||||
if (user_id.empty()) {
|
||||
LOG_WARN("Ignore client_info from unauthenticated connection");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ShouldTrackClientInfo(user_id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!device_db_manager_->UpdateDeviceClientInfo(user_id, version,
|
||||
platform)) {
|
||||
LOG_ERROR("Failed to store client information for [{}]", user_id);
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("Client [{}] reports version [{}] on [{}]", user_id, version,
|
||||
platform);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SignalNegotiation::leave_transmission(websocketpp::connection_hdl hdl,
|
||||
const json& j) {
|
||||
std::string transmission_id;
|
||||
|
||||
@@ -42,6 +42,7 @@ class SignalNegotiation {
|
||||
bool new_candidate_mid(websocketpp::connection_hdl hdl, const json& j);
|
||||
bool change_password(websocketpp::connection_hdl hdl, const json& j);
|
||||
bool turn_credentials(websocketpp::connection_hdl hdl, const json& j);
|
||||
bool client_info(websocketpp::connection_hdl hdl, const json& j);
|
||||
void OnWebClientDisconnect(const std::string& user_id);
|
||||
|
||||
private:
|
||||
|
||||
@@ -852,6 +852,9 @@ void SignalServer::OnMessage(websocketpp::connection_hdl hdl,
|
||||
case "turn_credentials"_H:
|
||||
signal_negotiation_->turn_credentials(hdl, j);
|
||||
break;
|
||||
case "client_info"_H:
|
||||
signal_negotiation_->client_info(hdl, j);
|
||||
break;
|
||||
case "recent_connections_presence"_H: {
|
||||
std::string user_id;
|
||||
if (!j.contains("user_id") || !j["user_id"].is_string()) {
|
||||
|
||||
Reference in new issue
Block a user