diff --git a/server/2015Remote/WebPage.h b/server/2015Remote/WebPage.h index 5bd7fdf..47e3e9d 100644 --- a/server/2015Remote/WebPage.h +++ b/server/2015Remote/WebPage.h @@ -997,10 +997,14 @@ inline std::string GetWebPageHTML() {

Create New User

- +
+ +
+
@@ -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 '
' + '' + (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);