@@ -1286,6 +1290,11 @@ inline std::string GetWebPageHTML() {
renderUsersList(msg.users);
}
break;
+ case 'groups':
+ if (msg.ok) {
+ renderGroupsCheckboxes(msg.groups);
+ }
+ break;
}
}
)HTML";
@@ -1661,7 +1670,35 @@ inline std::string GetWebPageHTML() {
function openUsersModal() {
document.getElementById('users-modal').classList.add('active');
document.getElementById('user-msg').innerHTML = '';
+ document.getElementById('new-role').value = 'viewer'; // Reset to default
+ onRoleChange(); // Update groups section visibility
listUsers();
+ getGroups();
+ }
+
+ function getGroups() {
+ if (ws && ws.readyState === WebSocket.OPEN && token) {
+ ws.send(JSON.stringify({ cmd: 'get_groups', token }));
+ }
+ }
+
+ function renderGroupsCheckboxes(groups) {
+ const container = document.getElementById('groups-checkboxes');
+ if (!groups || groups.length === 0) {
+ container.innerHTML = '
No groups available';
+ return;
+ }
+ container.innerHTML = groups.map(g =>
+ '
'
+ ).join('');
+ }
+
+ function onRoleChange() {
+ const role = document.getElementById('new-role').value;
+ const groupsSection = document.getElementById('groups-section');
+ groupsSection.style.display = (role === 'admin') ? 'none' : 'block';
}
function closeUsersModal() {
@@ -1685,8 +1722,12 @@ inline std::string GetWebPageHTML() {
return;
}
+ // Collect selected groups
+ const checkboxes = document.querySelectorAll('#groups-checkboxes input[type="checkbox"]:checked');
+ const allowed_groups = Array.from(checkboxes).map(cb => cb.value);
+
if (ws && ws.readyState === WebSocket.OPEN && token) {
- ws.send(JSON.stringify({ cmd: 'create_user', token, username, password, role }));
+ ws.send(JSON.stringify({ cmd: 'create_user', token, username, password, role, allowed_groups }));
}
}
@@ -1712,10 +1753,14 @@ inline std::string GetWebPageHTML() {
container.innerHTML = users.map(u => {
const isAdmin = u.role === 'admin';
const canDelete = u.username !== 'admin'; // Cannot delete built-in admin
+ const groups = u.allowed_groups || [];
+ const groupsText = u.username === 'admin' ? '(all)' :
+ (groups.length > 0 ? groups.join(', ') : '(none)');
return '
' +
'
' +
'
' + escapeHtml(u.username) + '
' +
'
' + u.role + '
' +
+ '
Groups: ' + escapeHtml(groupsText) + '
' +
'
' +
(canDelete ? '
' : '') +
'
';
diff --git a/server/2015Remote/WebService.cpp b/server/2015Remote/WebService.cpp
index 601605d..df59cc3 100644
--- a/server/2015Remote/WebService.cpp
+++ b/server/2015Remote/WebService.cpp
@@ -11,6 +11,7 @@
#include
#include
#include
+#include
// Algorithm constants (same as ScreenSpyDlg.cpp)
#define ALGORITHM_H264 2
@@ -363,6 +364,8 @@ void CWebService::ServerThread(int port) {
HandleDeleteUser(ws_ptr, msg);
} else if (cmd == "list_users") {
HandleListUsers(ws_ptr, token);
+ } else if (cmd == "get_groups") {
+ HandleGetGroups(ws_ptr, token);
}
}
});
@@ -566,7 +569,7 @@ void CWebService::HandleGetDevices(void* ws_ptr, const std::string& token) {
return;
}
- SendText(ws_ptr, BuildDeviceListJson());
+ SendText(ws_ptr, BuildDeviceListJson(username));
}
void CWebService::HandleConnect(void* ws_ptr, const std::string& token, uint64_t device_id) {
@@ -588,6 +591,32 @@ void CWebService::HandleConnect(void* ws_ptr, const std::string& token, uint64_t
return;
}
+ // Check group permission (admin can access all devices)
+ if (username != "admin") {
+ std::string deviceGroup = ctx->GetGroupName();
+ if (deviceGroup.empty()) deviceGroup = "default";
+
+ bool hasAccess = false;
+ {
+ std::lock_guard lock(m_UsersMutex);
+ for (const auto& u : m_Users) {
+ if (u.username == username) {
+ for (const auto& g : u.allowed_groups) {
+ if (g == deviceGroup) {
+ hasAccess = true;
+ break;
+ }
+ }
+ break;
+ }
+ }
+ }
+ if (!hasAccess) {
+ SendText(ws_ptr, BuildJsonResponse("connect_result", false, "Permission denied"));
+ return;
+ }
+ }
+
// Check max clients per device
int current_count = GetWebClientCount(device_id);
if (current_count >= m_nMaxClientsPerDevice) {
@@ -954,12 +983,23 @@ void CWebService::HandleCreateUser(void* ws_ptr, const std::string& msg) {
std::string newPassword = root.get("password", "").asString();
std::string newRole = root.get("role", "viewer").asString();
+ // Parse allowed_groups array
+ std::vector allowedGroups;
+ const Json::Value& groups = root["allowed_groups"];
+ if (groups.isArray()) {
+ for (const auto& g : groups) {
+ if (g.isString() && !g.asString().empty()) {
+ allowedGroups.push_back(g.asString());
+ }
+ }
+ }
+
if (newUsername.empty() || newPassword.empty()) {
SendText(ws_ptr, BuildJsonResponse("create_user_result", false, "Username and password required"));
return;
}
- if (CreateUser(newUsername, newPassword, newRole)) {
+ if (CreateUser(newUsername, newPassword, newRole, allowedGroups)) {
SendText(ws_ptr, BuildJsonResponse("create_user_result", true));
} else {
SendText(ws_ptr, BuildJsonResponse("create_user_result", false, "Failed to create user (may already exist)"));
@@ -1009,18 +1049,27 @@ void CWebService::HandleListUsers(void* ws_ptr, const std::string& token) {
return;
}
- auto users = ListUsers();
-
Json::Value res;
res["cmd"] = "list_users_result";
res["ok"] = true;
Json::Value usersArray(Json::arrayValue);
- for (const auto& u : users) {
- Json::Value user;
- user["username"] = u.first;
- user["role"] = u.second;
- usersArray.append(user);
+ {
+ std::lock_guard lock(m_UsersMutex);
+ for (const auto& u : m_Users) {
+ Json::Value user;
+ user["username"] = u.username;
+ user["role"] = u.role;
+
+ // Include allowed_groups
+ Json::Value groups(Json::arrayValue);
+ for (const auto& g : u.allowed_groups) {
+ groups.append(g);
+ }
+ user["allowed_groups"] = groups;
+
+ usersArray.append(user);
+ }
}
res["users"] = usersArray;
@@ -1030,6 +1079,48 @@ void CWebService::HandleListUsers(void* ws_ptr, const std::string& token) {
SendText(ws_ptr, json);
}
+void CWebService::HandleGetGroups(void* ws_ptr, const std::string& token) {
+ std::string username, role;
+ if (!ValidateToken(token, username, role)) {
+ SendText(ws_ptr, BuildJsonResponse("groups", false, "Invalid token"));
+ return;
+ }
+
+ // Only admin can get groups list (for user management)
+ if (role != "admin") {
+ SendText(ws_ptr, BuildJsonResponse("groups", false, "Permission denied"));
+ return;
+ }
+
+ // Collect all unique groups from online devices
+ std::set groups;
+ groups.insert("default"); // Always include default group
+
+ if (m_pParentDlg) {
+ EnterCriticalSection(&m_pParentDlg->m_cs);
+ for (context* ctx : m_pParentDlg->m_HostList) {
+ if (!ctx || !ctx->IsLogin()) continue;
+ std::string g = ctx->GetGroupName();
+ groups.insert(g.empty() ? "default" : g);
+ }
+ LeaveCriticalSection(&m_pParentDlg->m_cs);
+ }
+
+ // Build response
+ Json::Value res;
+ res["cmd"] = "groups";
+ res["ok"] = true;
+ res["groups"] = Json::Value(Json::arrayValue);
+ for (const auto& g : groups) {
+ res["groups"].append(g);
+ }
+
+ Json::StreamWriterBuilder builder;
+ builder["indentation"] = "";
+ std::string json = Json::writeString(builder, res);
+ SendText(ws_ptr, json);
+}
+
//////////////////////////////////////////////////////////////////////////
// Token Management (delegated to WebServiceAuth module)
//////////////////////////////////////////////////////////////////////////
@@ -1170,6 +1261,14 @@ void CWebService::LoadUsers() {
user.salt = u.get("salt", "").asString();
user.role = u.get("role", "viewer").asString();
+ // Load allowed_groups
+ const Json::Value& groups = u["allowed_groups"];
+ if (groups.isArray()) {
+ for (const auto& g : groups) {
+ user.allowed_groups.push_back(g.asString());
+ }
+ }
+
if (!user.password_hash.empty()) {
m_Users.push_back(user);
loaded++;
@@ -1197,6 +1296,14 @@ void CWebService::SaveUsers() {
user["password_hash"] = u.password_hash;
user["salt"] = u.salt;
user["role"] = u.role;
+
+ // Save allowed_groups
+ Json::Value groups(Json::arrayValue);
+ for (const auto& g : u.allowed_groups) {
+ groups.append(g);
+ }
+ user["allowed_groups"] = groups;
+
users.append(user);
}
@@ -1217,7 +1324,8 @@ void CWebService::SaveUsers() {
Mprintf("[WebService] Saved %d users to users.json\n", (int)users.size());
}
-bool CWebService::CreateUser(const std::string& username, const std::string& password, const std::string& role) {
+bool CWebService::CreateUser(const std::string& username, const std::string& password, const std::string& role,
+ const std::vector& allowed_groups) {
if (username.empty() || password.empty()) return false;
if (username == "admin") return false; // Cannot create user named "admin"
if (role != "admin" && role != "viewer") return false;
@@ -1236,9 +1344,11 @@ bool CWebService::CreateUser(const std::string& username, const std::string& pas
user.salt = GenerateSalt();
user.password_hash = WSAuth::ComputeSHA256(password + user.salt);
user.role = role;
+ user.allowed_groups = allowed_groups;
m_Users.push_back(user);
- Mprintf("[WebService] Created user: %s (role: %s)\n", username.c_str(), role.c_str());
+ Mprintf("[WebService] Created user: %s (role: %s, groups: %d)\n",
+ username.c_str(), role.c_str(), (int)allowed_groups.size());
}
// Save to file (outside lock scope since SaveUsers acquires its own lock)
@@ -1295,17 +1405,47 @@ std::string CWebService::BuildJsonResponse(const std::string& cmd, bool ok, cons
return Json::writeString(builder, res);
}
-std::string CWebService::BuildDeviceListJson() {
+std::string CWebService::BuildDeviceListJson(const std::string& username) {
Json::Value res;
res["cmd"] = "device_list";
res["devices"] = Json::Value(Json::arrayValue);
+ // Get user's allowed groups for filtering (skip for admin or empty username)
+ std::vector allowedGroups;
+ bool filterByGroup = false;
+ if (!username.empty() && username != "admin") {
+ std::lock_guard lock(m_UsersMutex);
+ for (const auto& u : m_Users) {
+ if (u.username == username) {
+ allowedGroups = u.allowed_groups;
+ filterByGroup = true;
+ break;
+ }
+ }
+ }
+
if (m_pParentDlg) {
// Access device list with lock
EnterCriticalSection(&m_pParentDlg->m_cs);
for (context* ctx : m_pParentDlg->m_HostList) {
if (!ctx || !ctx->IsLogin()) continue;
+ // Get device group (empty = "default")
+ std::string deviceGroup = ctx->GetGroupName();
+ if (deviceGroup.empty()) deviceGroup = "default";
+
+ // Filter by allowed groups if user is not admin
+ if (filterByGroup) {
+ bool allowed = false;
+ for (const auto& g : allowedGroups) {
+ if (g == deviceGroup) {
+ allowed = true;
+ break;
+ }
+ }
+ if (!allowed) continue; // Skip device not in allowed groups
+ }
+
Json::Value device;
// Use string for ID to avoid JavaScript number precision loss
device["id"] = std::to_string(ctx->GetClientID());
@@ -1332,6 +1472,9 @@ std::string CWebService::BuildDeviceListJson() {
device["activeWindow"] = AnsiToUtf8(activeWindow);
device["online"] = true;
+ // Add device group to response
+ device["group"] = deviceGroup;
+
// Get screen info from client's reported resolution
// Format: "n:MxN" where n=monitor count, M=width, N=height
CString resolution = ctx->GetAdditionalData(RES_RESOLUTION);
diff --git a/server/2015Remote/WebService.h b/server/2015Remote/WebService.h
index 6de8fa6..196f42b 100644
--- a/server/2015Remote/WebService.h
+++ b/server/2015Remote/WebService.h
@@ -43,6 +43,7 @@ struct WebUser {
std::string password_hash; // SHA256(password + salt)
std::string salt;
std::string role; // "admin" | "viewer"
+ std::vector allowed_groups; // Groups this user can view (empty = no access, admin = all)
};
// Device info for web clients
@@ -79,7 +80,8 @@ public:
void SetAdminPassword(const std::string& password);
// User management
- bool CreateUser(const std::string& username, const std::string& password, const std::string& role);
+ bool CreateUser(const std::string& username, const std::string& password, const std::string& role,
+ const std::vector& allowed_groups = {});
bool DeleteUser(const std::string& username);
std::vector> ListUsers(); // Returns [(username, role), ...]
@@ -144,7 +146,7 @@ private:
// JSON helpers
std::string BuildJsonResponse(const std::string& cmd, bool ok, const std::string& msg = "");
- std::string BuildDeviceListJson();
+ std::string BuildDeviceListJson(const std::string& username = "");
// Password verification
bool VerifyPassword(const std::string& input, const WebUser& user);
@@ -157,6 +159,7 @@ private:
void HandleCreateUser(void* ws_ptr, const std::string& msg);
void HandleDeleteUser(void* ws_ptr, const std::string& msg);
void HandleListUsers(void* ws_ptr, const std::string& token);
+ void HandleGetGroups(void* ws_ptr, const std::string& token);
// Send to WebSocket
void SendText(void* ws_ptr, const std::string& text);