Feature: Implement user management feature with role support
This commit is contained in:
@@ -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() {
|
||||
<button id="view-list" class="view-btn" onclick="setViewMode('list')" title="List View">List</button>
|
||||
</div>
|
||||
<button class="refresh-btn" onclick="getDevices()">Refresh</button>
|
||||
<button class="users-btn" id="users-btn" onclick="openUsersModal()">Users</button>
|
||||
<button class="logout-btn" onclick="logout()">Logout</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -871,6 +984,31 @@ inline std::string GetWebPageHTML() {
|
||||
<div class="zoom-indicator" id="zoom-indicator">100%</div>
|
||||
<input type="text" id="mobile-keyboard" style="position:fixed;left:-9999px;opacity:0;" autocomplete="off" autocorrect="off" autocapitalize="off">
|
||||
</div>
|
||||
|
||||
<!-- User Management Modal -->
|
||||
<div class="modal-overlay" id="users-modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>User Management</h3>
|
||||
<button class="modal-close" onclick="closeUsersModal()">×</button>
|
||||
</div>
|
||||
<div id="user-msg"></div>
|
||||
<div class="user-form">
|
||||
<h4>Create New User</h4>
|
||||
<input type="text" id="new-username" placeholder="Username" autocomplete="off">
|
||||
<input type="password" id="new-password" placeholder="Password" autocomplete="new-password">
|
||||
<select id="new-role">
|
||||
<option value="viewer">Viewer (read-only)</option>
|
||||
<option value="admin">Admin (full access)</option>
|
||||
</select>
|
||||
<button onclick="createUser()">Create User</button>
|
||||
</div>
|
||||
<div class="user-list">
|
||||
<h4>Existing Users</h4>
|
||||
<div id="users-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)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 = '<div style="color:#666;padding:12px;">No users</div>';
|
||||
return;
|
||||
}
|
||||
container.innerHTML = users.map(u => {
|
||||
const isAdmin = u.role === 'admin';
|
||||
const canDelete = u.username !== 'admin'; // Cannot delete built-in admin
|
||||
return '<div class="user-item">' +
|
||||
'<div class="user-info">' +
|
||||
'<div class="username">' + escapeHtml(u.username) + '</div>' +
|
||||
'<div class="role ' + (isAdmin ? 'admin' : '') + '">' + u.role + '</div>' +
|
||||
'</div>' +
|
||||
(canDelete ? '<button class="delete-btn" onclick="deleteUser(\'' + escapeHtml(u.username) + '\')">Delete</button>' : '') +
|
||||
'</div>';
|
||||
}).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();
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
#include "SimpleWebSocket.h"
|
||||
#include "common/commands.h"
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <shlobj.h>
|
||||
|
||||
// Algorithm constants (same as ScreenSpyDlg.cpp)
|
||||
#define ALGORITHM_H264 2
|
||||
@@ -20,6 +22,16 @@ static std::map<void*, std::string> s_ClientNonces;
|
||||
static std::mutex s_NonceMutex;
|
||||
static std::atomic<bool> 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<uint64_t> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<Json::StreamWriter> 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<std::mutex> 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<std::mutex> 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<std::pair<std::string, std::string>> CWebService::ListUsers() {
|
||||
std::lock_guard<std::mutex> lock(m_UsersMutex);
|
||||
std::vector<std::pair<std::string, std::string>> result;
|
||||
for (const auto& u : m_Users) {
|
||||
result.push_back({u.username, u.role});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// JSON Helpers
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -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<std::pair<std::string, std::string>> 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<WebUser> 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<uint64_t> m_WebTriggeredDevices;
|
||||
|
||||
Reference in New Issue
Block a user