diff --git a/server/2015Remote/WebPage.h b/server/2015Remote/WebPage.h
index ded4f56..014760a 100644
--- a/server/2015Remote/WebPage.h
+++ b/server/2015Remote/WebPage.h
@@ -275,6 +275,118 @@ inline std::string GetWebPageHTML() {
margin-left: 10px;
}
.logout-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(192, 57, 43, 0.4); }
+ .users-btn {
+ padding: 10px 20px;
+ border: none;
+ border-radius: 8px;
+ background: linear-gradient(135deg, #9b59b6 0%, #8e44ad 100%);
+ color: #fff;
+ font-size: 14px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.3s;
+ margin-left: 10px;
+ display: none;
+ }
+ .users-btn.visible { display: inline-block; }
+ .users-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(142, 68, 173, 0.4); }
+ /* User Management Modal */
+ .modal-overlay {
+ display: none;
+ position: fixed;
+ top: 0; left: 0; right: 0; bottom: 0;
+ background: rgba(0,0,0,0.7);
+ z-index: 1000;
+ align-items: center;
+ justify-content: center;
+ }
+ .modal-overlay.active { display: flex; }
+ .modal-content {
+ background: rgba(22, 33, 62, 0.98);
+ border-radius: 16px;
+ padding: 24px;
+ width: 90%;
+ max-width: 500px;
+ max-height: 80vh;
+ overflow-y: auto;
+ box-shadow: 0 8px 32px rgba(0,0,0,0.5);
+ border: 1px solid rgba(233, 69, 96, 0.2);
+ }
+ .modal-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+ padding-bottom: 16px;
+ border-bottom: 1px solid rgba(255,255,255,0.1);
+ }
+ .modal-header h3 { color: #e94560; margin: 0; }
+ .modal-close {
+ background: none;
+ border: none;
+ color: #888;
+ font-size: 24px;
+ cursor: pointer;
+ padding: 0;
+ line-height: 1;
+ }
+ .modal-close:hover { color: #e94560; }
+ .user-form { margin-bottom: 24px; }
+ .user-form h4 { color: #ccc; margin-bottom: 12px; font-size: 14px; }
+ .user-form input, .user-form select {
+ width: 100%;
+ padding: 10px 12px;
+ margin-bottom: 12px;
+ border: 1px solid rgba(255,255,255,0.1);
+ border-radius: 8px;
+ background: rgba(15, 52, 96, 0.8);
+ color: #fff;
+ font-size: 14px;
+ }
+ .user-form input:focus, .user-form select:focus {
+ outline: none;
+ border-color: #e94560;
+ }
+ .user-form button {
+ width: 100%;
+ padding: 12px;
+ border: none;
+ border-radius: 8px;
+ background: linear-gradient(135deg, #27ae60 0%, #2ecc71 100%);
+ color: #fff;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.3s;
+ }
+ .user-form button:hover { transform: translateY(-1px); }
+ .user-list h4 { color: #ccc; margin-bottom: 12px; font-size: 14px; }
+ .user-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 12px;
+ background: rgba(15, 52, 96, 0.5);
+ border-radius: 8px;
+ margin-bottom: 8px;
+ }
+ .user-item .user-info { flex: 1; }
+ .user-item .username { color: #fff; font-weight: 500; }
+ .user-item .role { color: #888; font-size: 12px; }
+ .user-item .role.admin { color: #e94560; }
+ .user-item .delete-btn {
+ background: rgba(231, 76, 60, 0.2);
+ border: 1px solid rgba(231, 76, 60, 0.5);
+ color: #e74c3c;
+ padding: 6px 12px;
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 12px;
+ }
+ .user-item .delete-btn:hover { background: rgba(231, 76, 60, 0.4); }
+ .user-item .delete-btn:disabled { opacity: 0.5; cursor: not-allowed; }
+ .user-msg { padding: 10px; border-radius: 6px; margin-bottom: 12px; font-size: 13px; }
+ .user-msg.success { background: rgba(39, 174, 96, 0.2); color: #2ecc71; }
+ .user-msg.error { background: rgba(231, 76, 60, 0.2); color: #e74c3c; }
)HTML";
// Part 3: Device card styles
@@ -814,6 +926,7 @@ inline std::string GetWebPageHTML() {
+
@@ -871,6 +984,31 @@ inline std::string GetWebPageHTML() {
100%
+
+
+
+
+
+
+
+
Create New User
+
+
+
+
+
+
+
+
)HTML";
// Part 7: JavaScript - State and WebSocket
@@ -1030,11 +1168,28 @@ inline std::string GetWebPageHTML() {
challengeNonce = msg.nonce || '';
console.log('Received challenge nonce');
break;
+ case 'salt':
+ if (msg.ok) {
+ completeLogin(msg.salt || '');
+ } else {
+ pendingLogin = null; // Clear pending state on error
+ document.getElementById('login-error').textContent = msg.msg || 'Failed to get salt';
+ }
+ break;
case 'login_result':
if (msg.ok) {
token = msg.token;
+ currentUserRole = msg.role || 'viewer';
sessionStorage.setItem('token', token);
+ sessionStorage.setItem('role', currentUserRole);
document.getElementById('login-error').textContent = '';
+ // Show Users button for admin only
+ const usersBtn = document.getElementById('users-btn');
+ if (currentUserRole === 'admin') {
+ usersBtn.classList.add('visible');
+ } else {
+ usersBtn.classList.remove('visible');
+ }
showPage('devices-page');
getDevices();
} else {
@@ -1096,6 +1251,29 @@ inline std::string GetWebPageHTML() {
getDevices();
}
break;
+ case 'create_user_result':
+ if (msg.ok) {
+ showUserMsg('User created successfully', false);
+ document.getElementById('new-username').value = '';
+ document.getElementById('new-password').value = '';
+ listUsers();
+ } else {
+ showUserMsg(msg.msg || 'Failed to create user', true);
+ }
+ break;
+ case 'delete_user_result':
+ if (msg.ok) {
+ showUserMsg('User deleted', false);
+ listUsers();
+ } else {
+ showUserMsg(msg.msg || 'Failed to delete user', true);
+ }
+ break;
+ case 'list_users_result':
+ if (msg.ok) {
+ renderUsersList(msg.users);
+ }
+ break;
}
}
)HTML";
@@ -1383,6 +1561,9 @@ inline std::string GetWebPageHTML() {
}
}
+ // Pending login state for salt-based auth
+ let pendingLogin = null;
+
async function login() {
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
@@ -1390,8 +1571,18 @@ inline std::string GetWebPageHTML() {
if (!ws || ws.readyState !== WebSocket.OPEN) { document.getElementById('login-error').textContent = 'Not connected'; return; }
if (!challengeNonce) { document.getElementById('login-error').textContent = 'No challenge received'; return; }
- // Compute password hash (same as server stores)
- passwordHash = await sha256(password);
+ // Store pending login info and request salt first
+ pendingLogin = { username, password };
+ ws.send(JSON.stringify({ cmd: 'get_salt', username }));
+ }
+
+ async function completeLogin(salt) {
+ if (!pendingLogin) return;
+ const { username, password } = pendingLogin;
+ pendingLogin = null;
+
+ // Compute password hash with salt: SHA256(password + salt)
+ passwordHash = await sha256(password + salt);
// Compute response: SHA256(passwordHash + nonce)
const response = await sha256(passwordHash + challengeNonce);
ws.send(JSON.stringify({ cmd: 'login', username, response, nonce: challengeNonce }));
@@ -1409,6 +1600,75 @@ inline std::string GetWebPageHTML() {
sessionStorage.removeItem('token');
devices = [];
showPage('login-page');
+ // Hide users button
+ document.getElementById('users-btn').classList.remove('visible');
+ }
+
+ // User Management Functions
+ let currentUserRole = 'viewer';
+
+ function openUsersModal() {
+ document.getElementById('users-modal').classList.add('active');
+ document.getElementById('user-msg').innerHTML = '';
+ listUsers();
+ }
+
+ function closeUsersModal() {
+ document.getElementById('users-modal').classList.remove('active');
+ }
+
+ function showUserMsg(msg, isError) {
+ const el = document.getElementById('user-msg');
+ el.className = 'user-msg ' + (isError ? 'error' : 'success');
+ el.textContent = msg;
+ setTimeout(() => { el.innerHTML = ''; }, 3000);
+ }
+
+ function createUser() {
+ const username = document.getElementById('new-username').value.trim();
+ const password = document.getElementById('new-password').value;
+ const role = document.getElementById('new-role').value;
+
+ if (!username || !password) {
+ showUserMsg('Username and password are required', true);
+ return;
+ }
+
+ if (ws && ws.readyState === WebSocket.OPEN && token) {
+ ws.send(JSON.stringify({ cmd: 'create_user', token, username, password, role }));
+ }
+ }
+
+ function deleteUser(username) {
+ if (!confirm('Delete user "' + username + '"?')) return;
+ if (ws && ws.readyState === WebSocket.OPEN && token) {
+ ws.send(JSON.stringify({ cmd: 'delete_user', token, username }));
+ }
+ }
+
+ function listUsers() {
+ if (ws && ws.readyState === WebSocket.OPEN && token) {
+ ws.send(JSON.stringify({ cmd: 'list_users', token }));
+ }
+ }
+
+ function renderUsersList(users) {
+ const container = document.getElementById('users-list');
+ if (!users || users.length === 0) {
+ container.innerHTML = 'No users
';
+ return;
+ }
+ container.innerHTML = users.map(u => {
+ const isAdmin = u.role === 'admin';
+ const canDelete = u.username !== 'admin'; // Cannot delete built-in admin
+ return '' +
+ '
' +
+ '
' + escapeHtml(u.username) + '
' +
+ '
' + u.role + '
' +
+ '
' +
+ (canDelete ? '
' : '') +
+ '
';
+ }).join('');
}
function getDevices() {
@@ -2642,8 +2902,13 @@ inline std::string GetWebPageHTML() {
bindKeyboardBtnEvents(document.getElementById('qc-keyboard'));
bindKeyboardBtnEvents(document.getElementById('btn-keyboard'));
bindKeyboardBtnEvents(document.getElementById('btn-keyboard-bar'));
- // Restore token from sessionStorage
+ // Restore token and role from sessionStorage
token = sessionStorage.getItem('token');
+ currentUserRole = sessionStorage.getItem('role') || 'viewer';
+ // Show Users button for admin only (will be updated after login verification)
+ if (token && currentUserRole === 'admin') {
+ document.getElementById('users-btn').classList.add('visible');
+ }
connectWebSocket();
};
diff --git a/server/2015Remote/WebService.cpp b/server/2015Remote/WebService.cpp
index c9de53f..89e3ecc 100644
--- a/server/2015Remote/WebService.cpp
+++ b/server/2015Remote/WebService.cpp
@@ -9,6 +9,8 @@
#include "SimpleWebSocket.h"
#include "common/commands.h"
#include
+#include
+#include
// Algorithm constants (same as ScreenSpyDlg.cpp)
#define ALGORITHM_H264 2
@@ -20,6 +22,16 @@ static std::map s_ClientNonces;
static std::mutex s_NonceMutex;
static std::atomic s_bShuttingDown{false}; // Prevents access during static destruction
+// Generate random salt (16 hex chars) - thread-safe
+static std::string GenerateSalt() {
+ static std::random_device rd;
+ static std::mt19937_64 gen(rd());
+ std::uniform_int_distribution dis;
+ char buf[17];
+ snprintf(buf, sizeof(buf), "%016llX", dis(gen));
+ return std::string(buf);
+}
+
// Generate random nonce (32 hex chars) - thread-safe
static std::string GenerateNonce() {
if (s_bShuttingDown) return "";
@@ -112,11 +124,22 @@ CWebService::CWebService()
m_PayloadsDir = (exeDir / "Payloads").string();
std::error_code ec;
std::filesystem::create_directories(m_PayloadsDir, ec);
+
+ // Initialize config directory (same as YAMA.db location)
+ char appdata_path[MAX_PATH];
+ if (SUCCEEDED(SHGetFolderPathA(NULL, CSIDL_APPDATA, NULL, 0, appdata_path))) {
+ m_ConfigDir = std::string(appdata_path) + "\\" BRAND_DATA_FOLDER "\\";
+ } else {
+ m_ConfigDir = ".\\";
+ }
+ std::filesystem::create_directories(m_ConfigDir, ec);
}
void CWebService::SetAdminPassword(const std::string& password) {
+ std::lock_guard lock(m_UsersMutex);
m_Users.clear();
+ // Admin user is built-in, always first
WebUser admin;
admin.username = "admin";
admin.salt = ""; // Not used with challenge-response auth
@@ -125,6 +148,9 @@ void CWebService::SetAdminPassword(const std::string& password) {
m_Users.push_back(admin);
Mprintf("[WebService] Admin password configured\n");
+
+ // Load additional users from file (non-admin users)
+ LoadUsers();
}
CWebService::~CWebService() {
@@ -329,6 +355,14 @@ void CWebService::ServerThread(int port) {
HandleKey(ws_ptr, msg);
} else if (cmd == "rdp_reset") {
HandleRdpReset(ws_ptr, token);
+ } else if (cmd == "get_salt") {
+ HandleGetSalt(ws_ptr, msg);
+ } else if (cmd == "create_user") {
+ HandleCreateUser(ws_ptr, msg);
+ } else if (cmd == "delete_user") {
+ HandleDeleteUser(ws_ptr, msg);
+ } else if (cmd == "list_users") {
+ HandleListUsers(ws_ptr, token);
}
}
});
@@ -480,6 +514,51 @@ void CWebService::HandleLogin(void* ws_ptr, const std::string& msg, const std::s
SendText(ws_ptr, Json::writeString(builder, res));
}
+void CWebService::HandleGetSalt(void* ws_ptr, const std::string& msg) {
+ Json::Value root;
+ Json::Reader reader;
+ if (!reader.parse(msg, root)) {
+ SendText(ws_ptr, BuildJsonResponse("salt", false, "Invalid JSON"));
+ return;
+ }
+
+ std::string username = root.get("username", "").asString();
+ if (username.empty()) {
+ SendText(ws_ptr, BuildJsonResponse("salt", false, "Username required"));
+ return;
+ }
+
+ // Find user and get salt
+ std::string salt = "";
+ bool userFound = false;
+ {
+ std::lock_guard lock(m_UsersMutex);
+ for (const auto& u : m_Users) {
+ if (u.username == username) {
+ salt = u.salt;
+ userFound = true;
+ break;
+ }
+ }
+ }
+
+ // For security: if user doesn't exist, generate a fake deterministic salt
+ // This prevents username enumeration attacks
+ // Note: Admin has empty salt, so we must check userFound, not salt.empty()
+ if (!userFound) {
+ // Generate deterministic fake salt from username (won't match any real password)
+ salt = WSAuth::ComputeSHA256("fake_salt_prefix_" + username).substr(0, 16);
+ }
+
+ Json::Value res;
+ res["cmd"] = "salt";
+ res["ok"] = true;
+ res["salt"] = salt;
+ Json::StreamWriterBuilder builder;
+ builder["indentation"] = "";
+ SendText(ws_ptr, Json::writeString(builder, res));
+}
+
void CWebService::HandleGetDevices(void* ws_ptr, const std::string& token) {
std::string username, role;
if (!ValidateToken(token, username, role)) {
@@ -837,6 +916,111 @@ void CWebService::HandleRdpReset(void* ws_ptr, const std::string& token) {
}
}
+//////////////////////////////////////////////////////////////////////////
+// User Management Handlers
+//////////////////////////////////////////////////////////////////////////
+
+void CWebService::HandleCreateUser(void* ws_ptr, const std::string& msg) {
+ Json::Value root;
+ Json::Reader reader;
+ if (!reader.parse(msg, root)) {
+ SendText(ws_ptr, BuildJsonResponse("create_user_result", false, "Invalid JSON"));
+ return;
+ }
+
+ std::string token = root.get("token", "").asString();
+ std::string username, role;
+ if (!ValidateToken(token, username, role)) {
+ SendText(ws_ptr, BuildJsonResponse("create_user_result", false, "Invalid token"));
+ return;
+ }
+
+ // Only admin can create users
+ if (role != "admin") {
+ SendText(ws_ptr, BuildJsonResponse("create_user_result", false, "Permission denied"));
+ return;
+ }
+
+ std::string newUsername = root.get("username", "").asString();
+ std::string newPassword = root.get("password", "").asString();
+ std::string newRole = root.get("role", "viewer").asString();
+
+ if (newUsername.empty() || newPassword.empty()) {
+ SendText(ws_ptr, BuildJsonResponse("create_user_result", false, "Username and password required"));
+ return;
+ }
+
+ if (CreateUser(newUsername, newPassword, newRole)) {
+ SendText(ws_ptr, BuildJsonResponse("create_user_result", true));
+ } else {
+ SendText(ws_ptr, BuildJsonResponse("create_user_result", false, "Failed to create user (may already exist)"));
+ }
+}
+
+void CWebService::HandleDeleteUser(void* ws_ptr, const std::string& msg) {
+ Json::Value root;
+ Json::Reader reader;
+ if (!reader.parse(msg, root)) {
+ SendText(ws_ptr, BuildJsonResponse("delete_user_result", false, "Invalid JSON"));
+ return;
+ }
+
+ std::string token = root.get("token", "").asString();
+ std::string username, role;
+ if (!ValidateToken(token, username, role)) {
+ SendText(ws_ptr, BuildJsonResponse("delete_user_result", false, "Invalid token"));
+ return;
+ }
+
+ // Only admin can delete users
+ if (role != "admin") {
+ SendText(ws_ptr, BuildJsonResponse("delete_user_result", false, "Permission denied"));
+ return;
+ }
+
+ std::string targetUsername = root.get("username", "").asString();
+
+ if (DeleteUser(targetUsername)) {
+ SendText(ws_ptr, BuildJsonResponse("delete_user_result", true));
+ } else {
+ SendText(ws_ptr, BuildJsonResponse("delete_user_result", false, "Failed to delete user"));
+ }
+}
+
+void CWebService::HandleListUsers(void* ws_ptr, const std::string& token) {
+ std::string username, role;
+ if (!ValidateToken(token, username, role)) {
+ SendText(ws_ptr, BuildJsonResponse("list_users_result", false, "Invalid token"));
+ return;
+ }
+
+ // Only admin can list users
+ if (role != "admin") {
+ SendText(ws_ptr, BuildJsonResponse("list_users_result", false, "Permission denied"));
+ 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);
+ }
+ res["users"] = usersArray;
+
+ Json::StreamWriterBuilder builder;
+ builder["indentation"] = "";
+ std::string json = Json::writeString(builder, res);
+ SendText(ws_ptr, json);
+}
+
//////////////////////////////////////////////////////////////////////////
// Token Management (delegated to WebServiceAuth module)
//////////////////////////////////////////////////////////////////////////
@@ -936,6 +1120,155 @@ std::string CWebService::ComputeHash(const std::string& input) {
return WSAuth::ComputeSHA256(input);
}
+//////////////////////////////////////////////////////////////////////////
+// User Management
+//////////////////////////////////////////////////////////////////////////
+
+std::string CWebService::GetUsersFilePath() {
+ return m_ConfigDir + "users.json";
+}
+
+void CWebService::LoadUsers() {
+ // Note: m_UsersMutex should already be held by caller (SetAdminPassword)
+ // Load additional users from users.json (admin user is already in m_Users)
+
+ std::string path = GetUsersFilePath();
+ std::ifstream file(path);
+ if (!file.is_open()) {
+ Mprintf("[WebService] No users.json found, using admin only\n");
+ return;
+ }
+
+ try {
+ Json::Value root;
+ Json::CharReaderBuilder builder;
+ std::string errors;
+ if (!Json::parseFromStream(builder, file, &root, &errors)) {
+ Mprintf("[WebService] Failed to parse users.json: %s\n", errors.c_str());
+ return;
+ }
+
+ const Json::Value& users = root["users"];
+ int loaded = 0;
+ for (const auto& u : users) {
+ std::string username = u.get("username", "").asString();
+ // Skip admin user (it's built-in with master password)
+ if (username.empty() || username == "admin") continue;
+
+ WebUser user;
+ user.username = username;
+ user.password_hash = u.get("password_hash", "").asString();
+ user.salt = u.get("salt", "").asString();
+ user.role = u.get("role", "viewer").asString();
+
+ if (!user.password_hash.empty()) {
+ m_Users.push_back(user);
+ loaded++;
+ }
+ }
+ Mprintf("[WebService] Loaded %d additional users from users.json\n", loaded);
+ } catch (const std::exception& e) {
+ Mprintf("[WebService] Error loading users.json: %s\n", e.what());
+ }
+}
+
+void CWebService::SaveUsers() {
+ // Save non-admin users to users.json
+ std::lock_guard lock(m_UsersMutex);
+
+ Json::Value root;
+ Json::Value users(Json::arrayValue);
+
+ for (const auto& u : m_Users) {
+ // Skip admin user (it uses master password, not stored in file)
+ if (u.username == "admin") continue;
+
+ Json::Value user;
+ user["username"] = u.username;
+ user["password_hash"] = u.password_hash;
+ user["salt"] = u.salt;
+ user["role"] = u.role;
+ users.append(user);
+ }
+
+ root["users"] = users;
+
+ std::string path = GetUsersFilePath();
+ std::ofstream file(path);
+ if (!file.is_open()) {
+ Mprintf("[WebService] Failed to open users.json for writing\n");
+ return;
+ }
+
+ Json::StreamWriterBuilder builder;
+ builder["indentation"] = " ";
+ std::unique_ptr writer(builder.newStreamWriter());
+ writer->write(root, &file);
+
+ 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) {
+ if (username.empty() || password.empty()) return false;
+ if (username == "admin") return false; // Cannot create user named "admin"
+ if (role != "admin" && role != "viewer") return false;
+
+ {
+ std::lock_guard lock(m_UsersMutex);
+
+ // Check if user already exists
+ for (const auto& u : m_Users) {
+ if (u.username == username) return false;
+ }
+
+ // Generate salt and hash password with salt
+ WebUser user;
+ user.username = username;
+ user.salt = GenerateSalt();
+ user.password_hash = WSAuth::ComputeSHA256(password + user.salt);
+ user.role = role;
+
+ m_Users.push_back(user);
+ Mprintf("[WebService] Created user: %s (role: %s)\n", username.c_str(), role.c_str());
+ }
+
+ // Save to file (outside lock scope since SaveUsers acquires its own lock)
+ SaveUsers();
+ return true;
+}
+
+bool CWebService::DeleteUser(const std::string& username) {
+ if (username.empty() || username == "admin") return false;
+
+ bool deleted = false;
+ {
+ std::lock_guard lock(m_UsersMutex);
+
+ for (auto it = m_Users.begin(); it != m_Users.end(); ++it) {
+ if (it->username == username) {
+ m_Users.erase(it);
+ Mprintf("[WebService] Deleted user: %s\n", username.c_str());
+ deleted = true;
+ break;
+ }
+ }
+ }
+
+ if (deleted) {
+ SaveUsers();
+ }
+ return deleted;
+}
+
+std::vector> CWebService::ListUsers() {
+ std::lock_guard lock(m_UsersMutex);
+ std::vector> result;
+ for (const auto& u : m_Users) {
+ result.push_back({u.username, u.role});
+ }
+ return result;
+}
+
//////////////////////////////////////////////////////////////////////////
// JSON Helpers
//////////////////////////////////////////////////////////////////////////
diff --git a/server/2015Remote/WebService.h b/server/2015Remote/WebService.h
index fc5509d..c558bb0 100644
--- a/server/2015Remote/WebService.h
+++ b/server/2015Remote/WebService.h
@@ -78,6 +78,11 @@ public:
// Set admin password (use master password)
void SetAdminPassword(const std::string& password);
+ // User management
+ bool CreateUser(const std::string& username, const std::string& password, const std::string& role);
+ bool DeleteUser(const std::string& username);
+ std::vector> ListUsers(); // Returns [(username, role), ...]
+
// Device management (called from main app)
void MarkDeviceOnline(uint64_t device_id);
void MarkDeviceOffline(uint64_t device_id);
@@ -111,6 +116,7 @@ private:
// Signaling handlers
void HandleLogin(void* ws_ptr, const std::string& msg, const std::string& client_ip);
+ void HandleGetSalt(void* ws_ptr, const std::string& msg);
void HandleGetDevices(void* ws_ptr, const std::string& token);
void HandleConnect(void* ws_ptr, const std::string& token, uint64_t device_id);
void HandleDisconnect(void* ws_ptr, const std::string& token, uint64_t requested_device_id = 0);
@@ -141,6 +147,14 @@ private:
bool VerifyPassword(const std::string& input, const WebUser& user);
std::string ComputeHash(const std::string& input);
+ // User management helpers
+ std::string GetUsersFilePath();
+ void LoadUsers();
+ void SaveUsers();
+ 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);
+
// Send to WebSocket
void SendText(void* ws_ptr, const std::string& text);
void SendBinary(void* ws_ptr, const uint8_t* data, size_t len);
@@ -181,6 +195,7 @@ private:
// User accounts (loaded from config)
std::vector m_Users;
+ std::mutex m_UsersMutex;
// Token secret key (generated on startup)
std::string m_SecretKey;
@@ -190,6 +205,7 @@ private:
int m_nTokenExpireSeconds;
bool m_bHideWebSessions; // Whether to hide web-triggered dialogs (default: true)
std::string m_PayloadsDir; // Directory for file downloads (Payloads/)
+ std::string m_ConfigDir; // Directory for config files (users.json, etc.)
// Web-triggered sessions (should be hidden)
std::set m_WebTriggeredDevices;