Files
BetterDesk/web-nodejs/services/database.js
T
UNITRONIX e855f5786d Add Go server and security audit; update web console
Add a new betterdesk-server Go codebase (server, api, auth, db, relay, signal, metrics, audit, ratelimit, proto, tools, tests) and related deployment/migration scripts. Add a comprehensive SECURITY_AUDIT_2026-03-01 report and .gitattributes; update copilot-instructions (ALL-IN-ONE v2.4.0), README, VERSION, Dockerfiles, scripts, docker-compose and entrypoint. Large updates to web-nodejs (translations, routes, services, frontend assets and middleware) and numerous new utilities; remove legacy Flask web files and archive hbbs-patch-v2 artifacts. Prepares repository for PostgreSQL support, DB migration tooling and the new Go server as the production backend.
2026-03-02 00:43:04 +01:00

1999 lines
60 KiB
JavaScript

/**
* BetterDesk Console - Database Service
* SQLite3 wrapper using better-sqlite3 (synchronous, fast)
*/
const Database = require('better-sqlite3');
const path = require('path');
const config = require('../config/config');
let db = null;
let authDb = null;
/**
* Escape special characters in LIKE patterns to prevent SQL injection.
* Characters % and _ have special meaning in SQL LIKE clauses.
* @param {string} str - The string to escape
* @returns {string} - Escaped string safe for LIKE patterns
*/
function escapeLikePattern(str) {
if (!str) return '';
return str.replace(/[%_\\]/g, '\\$&');
}
/**
* Get the main RustDesk database connection
*/
function getDb() {
if (!db) {
db = new Database(config.dbPath, {
readonly: false,
fileMustExist: false
});
db.pragma('journal_mode = WAL');
db.pragma('foreign_keys = ON');
// Ensure peer table exists with all columns
ensurePeerTable(db);
}
return db;
}
/**
* Get the auth database connection (separate from RustDesk data)
*/
function getAuthDb() {
if (!authDb) {
const authDbPath = path.join(config.dataDir, 'auth.db');
authDb = new Database(authDbPath, {
readonly: false,
fileMustExist: false
});
authDb.pragma('journal_mode = WAL');
// Initialize auth tables
initAuthTables(authDb);
}
return authDb;
}
/**
* Ensure peer table has all required columns
*/
function ensurePeerTable(db) {
// Create table if not exists
db.exec(`
CREATE TABLE IF NOT EXISTS peer (
id TEXT PRIMARY KEY,
uuid TEXT DEFAULT '',
pk BLOB,
note TEXT DEFAULT '',
created_at TEXT DEFAULT (datetime('now')),
status_online INTEGER DEFAULT 0,
last_online TEXT,
is_deleted INTEGER DEFAULT 0,
info TEXT DEFAULT '',
ip TEXT DEFAULT '',
user TEXT DEFAULT '',
is_banned INTEGER DEFAULT 0,
banned_at TEXT,
banned_reason TEXT DEFAULT ''
)
`);
// Add missing columns if they don't exist
const columns = [
{ name: 'status_online', sql: 'INTEGER DEFAULT 0' },
{ name: 'last_online', sql: 'TEXT' },
{ name: 'is_deleted', sql: 'INTEGER DEFAULT 0' },
{ name: 'user', sql: 'TEXT DEFAULT \'\'' },
{ name: 'is_banned', sql: 'INTEGER DEFAULT 0' },
{ name: 'banned_at', sql: 'TEXT' },
{ name: 'banned_reason', sql: 'TEXT DEFAULT \'\'' },
{ name: 'folder_id', sql: 'INTEGER DEFAULT NULL' }
];
const tableInfo = db.prepare("PRAGMA table_info(peer)").all();
const existingColumns = new Set(tableInfo.map(c => c.name));
for (const col of columns) {
if (!existingColumns.has(col.name)) {
try {
db.exec(`ALTER TABLE peer ADD COLUMN ${col.name} ${col.sql}`);
console.log(`Added column ${col.name} to peer table`);
} catch (err) {
// Column might already exist
}
}
}
}
/**
* Initialize authentication tables
*/
function initAuthTables(db) {
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
role TEXT DEFAULT 'admin',
created_at TEXT DEFAULT (datetime('now')),
last_login TEXT
)
`);
db.exec(`
CREATE TABLE IF NOT EXISTS audit_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
action TEXT NOT NULL,
details TEXT,
ip_address TEXT,
created_at TEXT DEFAULT (datetime('now'))
)
`);
// Add TOTP columns if they don't exist
const userColumns = db.prepare("PRAGMA table_info(users)").all();
const existingUserCols = new Set(userColumns.map(c => c.name));
const totpColumns = [
{ name: 'totp_secret', sql: 'TEXT DEFAULT NULL' },
{ name: 'totp_enabled', sql: 'INTEGER DEFAULT 0' },
{ name: 'totp_recovery_codes', sql: 'TEXT DEFAULT NULL' }
];
for (const col of totpColumns) {
if (!existingUserCols.has(col.name)) {
try {
db.exec(`ALTER TABLE users ADD COLUMN ${col.name} ${col.sql}`);
console.log(`Added column ${col.name} to users table`);
} catch (err) {
// Column might already exist
}
}
}
// Access tokens table for RustDesk client API (port 21121)
db.exec(`
CREATE TABLE IF NOT EXISTS access_tokens (
id INTEGER PRIMARY KEY AUTOINCREMENT,
token TEXT UNIQUE NOT NULL,
user_id INTEGER NOT NULL,
client_id TEXT DEFAULT '',
client_uuid TEXT DEFAULT '',
created_at TEXT DEFAULT (datetime('now')),
expires_at TEXT NOT NULL,
last_used TEXT,
ip_address TEXT DEFAULT '',
revoked INTEGER DEFAULT 0,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)
`);
// Login attempts tracking (brute-force protection)
db.exec(`
CREATE TABLE IF NOT EXISTS login_attempts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL,
ip_address TEXT DEFAULT '',
success INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now'))
)
`);
// Account lockout table
db.exec(`
CREATE TABLE IF NOT EXISTS account_lockouts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
locked_until TEXT NOT NULL,
attempt_count INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now'))
)
`);
// Device folders table
db.exec(`
CREATE TABLE IF NOT EXISTS folders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
color TEXT DEFAULT '#6366f1',
icon TEXT DEFAULT 'folder',
sort_order INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now'))
)
`);
// Device-to-folder assignments (stored in auth.db so it works with any backend)
db.exec(`
CREATE TABLE IF NOT EXISTS device_folder_assignments (
device_id TEXT PRIMARY KEY NOT NULL,
folder_id INTEGER NOT NULL,
assigned_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (folder_id) REFERENCES folders(id) ON DELETE CASCADE
)
`);
// Address books table for RustDesk client sync
db.exec(`
CREATE TABLE IF NOT EXISTS address_books (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
ab_type TEXT DEFAULT 'legacy',
data TEXT DEFAULT '{}',
updated_at TEXT DEFAULT (datetime('now')),
UNIQUE(user_id, ab_type),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)
`);
// Console settings key-value store (server backend choice, etc.)
db.exec(`
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at TEXT DEFAULT (datetime('now'))
)
`);
// ==================== RustDesk Client Integration Tables ====================
// Peer sysinfo — hardware/software details reported by RustDesk client
db.exec(`
CREATE TABLE IF NOT EXISTS peer_sysinfo (
peer_id TEXT PRIMARY KEY,
hostname TEXT DEFAULT '',
username TEXT DEFAULT '',
platform TEXT DEFAULT '',
version TEXT DEFAULT '',
cpu_name TEXT DEFAULT '',
cpu_cores INTEGER DEFAULT 0,
cpu_freq_ghz REAL DEFAULT 0,
memory_gb REAL DEFAULT 0,
os_full TEXT DEFAULT '',
displays TEXT DEFAULT '[]',
encoding TEXT DEFAULT '[]',
features TEXT DEFAULT '{}',
platform_additions TEXT DEFAULT '{}',
raw_json TEXT DEFAULT '{}',
updated_at TEXT DEFAULT (datetime('now'))
)
`);
// Peer metrics — heartbeat telemetry (cpu/memory/disk usage)
db.exec(`
CREATE TABLE IF NOT EXISTS peer_metrics (
id INTEGER PRIMARY KEY AUTOINCREMENT,
peer_id TEXT NOT NULL,
cpu_usage REAL DEFAULT 0,
memory_usage REAL DEFAULT 0,
disk_usage REAL DEFAULT 0,
created_at TEXT DEFAULT (datetime('now'))
)
`);
// Index for efficient time-range queries
db.exec(`CREATE INDEX IF NOT EXISTS idx_peer_metrics_peer_time ON peer_metrics (peer_id, created_at)`);
// Audit: connection events — who connected where and when
db.exec(`
CREATE TABLE IF NOT EXISTS audit_connections (
id INTEGER PRIMARY KEY AUTOINCREMENT,
host_id TEXT NOT NULL,
host_uuid TEXT DEFAULT '',
peer_id TEXT DEFAULT '',
peer_name TEXT DEFAULT '',
action TEXT NOT NULL,
conn_type INTEGER DEFAULT 0,
session_id TEXT DEFAULT '',
ip TEXT DEFAULT '',
created_at TEXT DEFAULT (datetime('now'))
)
`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_audit_conn_host ON audit_connections (host_id, created_at)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_audit_conn_peer ON audit_connections (peer_id, created_at)`);
// Audit: file transfer events — what files were transferred
db.exec(`
CREATE TABLE IF NOT EXISTS audit_files (
id INTEGER PRIMARY KEY AUTOINCREMENT,
host_id TEXT NOT NULL,
host_uuid TEXT DEFAULT '',
peer_id TEXT DEFAULT '',
direction INTEGER DEFAULT 0,
path TEXT DEFAULT '',
is_file INTEGER DEFAULT 1,
num_files INTEGER DEFAULT 0,
files_json TEXT DEFAULT '[]',
ip TEXT DEFAULT '',
peer_name TEXT DEFAULT '',
created_at TEXT DEFAULT (datetime('now'))
)
`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_audit_files_host ON audit_files (host_id, created_at)`);
// Audit: security alarms — failed access, brute-force, IP violations
db.exec(`
CREATE TABLE IF NOT EXISTS audit_alarms (
id INTEGER PRIMARY KEY AUTOINCREMENT,
alarm_type INTEGER NOT NULL,
alarm_name TEXT DEFAULT '',
host_id TEXT DEFAULT '',
peer_id TEXT DEFAULT '',
ip TEXT DEFAULT '',
details TEXT DEFAULT '{}',
created_at TEXT DEFAULT (datetime('now'))
)
`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_audit_alarms_type ON audit_alarms (alarm_type, created_at)`);
// User groups — RustDesk-compatible group management
db.exec(`
CREATE TABLE IF NOT EXISTS user_groups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
guid TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
note TEXT DEFAULT '',
team_id TEXT DEFAULT '',
created_at TEXT DEFAULT (datetime('now'))
)
`);
// Device groups — group devices by function/location/team
db.exec(`
CREATE TABLE IF NOT EXISTS device_groups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
guid TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
note TEXT DEFAULT '',
team_id TEXT DEFAULT '',
created_at TEXT DEFAULT (datetime('now'))
)
`);
// Device-to-group membership (many-to-many)
db.exec(`
CREATE TABLE IF NOT EXISTS device_group_members (
device_group_id INTEGER NOT NULL,
peer_id TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now')),
PRIMARY KEY (device_group_id, peer_id),
FOREIGN KEY (device_group_id) REFERENCES device_groups(id) ON DELETE CASCADE
)
`);
// Access strategies / policies — RustDesk-compatible permission rules
db.exec(`
CREATE TABLE IF NOT EXISTS strategies (
id INTEGER PRIMARY KEY AUTOINCREMENT,
guid TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
user_group_guid TEXT DEFAULT '',
device_group_guid TEXT DEFAULT '',
enabled INTEGER DEFAULT 1,
permissions TEXT DEFAULT '{}',
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
)
`);
// Seed default groups if empty
const ugCount = db.prepare('SELECT COUNT(*) as c FROM user_groups').get().c;
if (ugCount === 0) {
const crypto = require('crypto');
db.prepare('INSERT INTO user_groups (guid, name, note) VALUES (?, ?, ?)').run(
crypto.randomUUID(), 'Default', 'Default user group'
);
}
const dgCount = db.prepare('SELECT COUNT(*) as c FROM device_groups').get().c;
if (dgCount === 0) {
const crypto = require('crypto');
db.prepare('INSERT INTO device_groups (guid, name, note) VALUES (?, ?, ?)').run(
crypto.randomUUID(), 'Default', 'Default device group'
);
}
// Cleanup old metrics (keep last 7 days) — scheduled in housekeeping
db.exec(`
DELETE FROM peer_metrics WHERE created_at < datetime('now', '-7 days')
`);
}
// ==================== Device Operations ====================
/**
* Parse info JSON and extract useful fields
*/
function parseDeviceInfo(device) {
if (!device) return device;
let info = {};
if (device.info) {
try {
info = JSON.parse(device.info);
} catch (e) {
// Not valid JSON
}
}
return {
id: device.id,
hostname: device.note || info.hostname || '',
username: typeof device.user === 'string' ? device.user : '',
platform: info.os || info.platform || '',
ip: info.ip || '',
note: device.note || '',
online: device.status_online === 1,
banned: device.is_banned === 1,
created_at: device.created_at,
last_online: device.last_online,
ban_reason: device.ban_reason || device.banned_reason || '',
folder_id: device.folder_id || null,
pk: device.pk ? Buffer.from(device.pk).toString('base64') : ''
};
}
/**
* Get all devices with optional filtering
*/
function getAllDevices(filters = {}) {
const db = getDb();
let sql = 'SELECT * FROM peer WHERE is_deleted = 0';
const params = [];
// Search filter (escape % and _ to prevent LIKE injection)
if (filters.search) {
sql += " AND (id LIKE ? ESCAPE '\\' OR user LIKE ? ESCAPE '\\' OR note LIKE ? ESCAPE '\\')";
const escaped = escapeLikePattern(filters.search);
const search = `%${escaped}%`;
params.push(search, search, search);
}
// Status filter
if (filters.status === 'online') {
sql += ' AND status_online = 1';
} else if (filters.status === 'offline') {
sql += ' AND status_online = 0 AND is_banned = 0';
} else if (filters.status === 'banned') {
sql += ' AND is_banned = 1';
}
// Notes filter
if (filters.hasNotes) {
sql += " AND note IS NOT NULL AND note != ''";
}
// Sorting
const sortColumn = filters.sortBy || 'last_online';
const sortOrder = filters.sortOrder === 'asc' ? 'ASC' : 'DESC';
const allowedColumns = ['id', 'user', 'created_at', 'last_online', 'status_online'];
if (allowedColumns.includes(sortColumn)) {
sql += ` ORDER BY ${sortColumn} ${sortOrder} NULLS LAST`;
} else {
sql += ' ORDER BY last_online DESC NULLS LAST';
}
// Note: No pagination in SQL - we load all and paginate client-side for filtering
const rawDevices = db.prepare(sql).all(...params);
// Transform to consistent format
return rawDevices.map(parseDeviceInfo);
}
/**
* Get device by ID
*/
function getDeviceById(id) {
const device = getDb().prepare('SELECT * FROM peer WHERE id = ? AND is_deleted = 0').get(id);
return parseDeviceInfo(device);
}
/**
* Update device (user name, note)
*/
function updateDevice(id, data) {
const fields = [];
const params = [];
if (data.user !== undefined) {
fields.push('user = ?');
params.push(data.user);
}
if (data.note !== undefined) {
fields.push('note = ?');
params.push(data.note);
}
if (fields.length === 0) return { changes: 0 };
params.push(id);
return getDb().prepare(`UPDATE peer SET ${fields.join(', ')} WHERE id = ?`).run(...params);
}
/**
* Soft delete device
*/
function deleteDevice(id) {
return getDb().prepare('UPDATE peer SET is_deleted = 1 WHERE id = ?').run(id);
}
/**
* Ban/unban device
*/
function setBanStatus(id, banned, reason = '') {
if (banned) {
return getDb().prepare(
'UPDATE peer SET is_banned = 1, banned_at = datetime(\'now\'), banned_reason = ? WHERE id = ?'
).run(reason, id);
} else {
return getDb().prepare(
'UPDATE peer SET is_banned = 0, banned_at = NULL, banned_reason = \'\' WHERE id = ?'
).run(id);
}
}
/**
* Get device statistics
*/
function getStats() {
const db = getDb();
const total = db.prepare('SELECT COUNT(*) as count FROM peer WHERE is_deleted = 0').get().count;
const online = db.prepare('SELECT COUNT(*) as count FROM peer WHERE is_deleted = 0 AND status_online = 1').get().count;
const banned = db.prepare('SELECT COUNT(*) as count FROM peer WHERE is_deleted = 0 AND is_banned = 1').get().count;
const withNotes = db.prepare("SELECT COUNT(*) as count FROM peer WHERE is_deleted = 0 AND note IS NOT NULL AND note != ''").get().count;
return {
total,
online,
offline: total - online,
banned,
withNotes
};
}
/**
* Count devices matching filters (for pagination)
*/
function countDevices(filters = {}) {
const db = getDb();
let sql = 'SELECT COUNT(*) as count FROM peer WHERE is_deleted = 0';
const params = [];
if (filters.search) {
sql += " AND (id LIKE ? ESCAPE '\\' OR user LIKE ? ESCAPE '\\' OR note LIKE ? ESCAPE '\\' OR ip LIKE ? ESCAPE '\\')";
const escaped = escapeLikePattern(filters.search);
const search = `%${escaped}%`;
params.push(search, search, search, search);
}
if (filters.status === 'online') {
sql += ' AND status_online = 1';
} else if (filters.status === 'offline') {
sql += ' AND status_online = 0';
} else if (filters.status === 'banned') {
sql += ' AND is_banned = 1';
}
if (filters.hasNotes) {
sql += " AND note IS NOT NULL AND note != ''";
}
return db.prepare(sql).get(...params).count;
}
// ==================== User Operations ====================
/**
* Get user by username
*/
function getUserByUsername(username) {
return getAuthDb().prepare('SELECT * FROM users WHERE username = ?').get(username);
}
/**
* Get user by ID
*/
function getUserById(id) {
return getAuthDb().prepare('SELECT * FROM users WHERE id = ?').get(id);
}
/**
* Create user
*/
function createUser(username, passwordHash, role = 'admin') {
return getAuthDb().prepare(
'INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)'
).run(username, passwordHash, role);
}
/**
* Update user password
*/
function updateUserPassword(id, passwordHash) {
return getAuthDb().prepare('UPDATE users SET password_hash = ? WHERE id = ?').run(passwordHash, id);
}
// ==================== TOTP Operations ====================
/**
* Save TOTP secret for user
*/
function saveTotpSecret(userId, secret) {
return getAuthDb().prepare('UPDATE users SET totp_secret = ? WHERE id = ?').run(secret, userId);
}
/**
* Enable TOTP for user
*/
function enableTotp(userId, recoveryCodes) {
return getAuthDb().prepare(
'UPDATE users SET totp_enabled = 1, totp_recovery_codes = ? WHERE id = ?'
).run(JSON.stringify(recoveryCodes), userId);
}
/**
* Disable TOTP for user
*/
function disableTotp(userId) {
return getAuthDb().prepare(
'UPDATE users SET totp_enabled = 0, totp_secret = NULL, totp_recovery_codes = NULL WHERE id = ?'
).run(userId);
}
/**
* Use a recovery code (mark as used)
*/
function useRecoveryCode(userId, updatedCodes) {
return getAuthDb().prepare(
'UPDATE users SET totp_recovery_codes = ? WHERE id = ?'
).run(JSON.stringify(updatedCodes), userId);
}
/**
* Update last login
*/
function updateLastLogin(id) {
return getAuthDb().prepare("UPDATE users SET last_login = datetime('now') WHERE id = ?").run(id);
}
/**
* Check if any users exist
*/
function hasUsers() {
return getAuthDb().prepare('SELECT COUNT(*) as count FROM users').get().count > 0;
}
// ==================== Audit Log ====================
/**
* Log an action
*/
function logAction(userId, action, details, ipAddress) {
return getAuthDb().prepare(
'INSERT INTO audit_log (user_id, action, details, ip_address) VALUES (?, ?, ?, ?)'
).run(userId, action, details, ipAddress);
}
/**
* Get recent audit logs
*/
function getAuditLogs(limit = 100) {
return getAuthDb().prepare(
'SELECT * FROM audit_log ORDER BY created_at DESC LIMIT ?'
).all(limit);
}
// ==================== Extended User Operations ====================
/**
* Get all users
*/
function getAllUsers() {
return getAuthDb().prepare('SELECT * FROM users ORDER BY created_at DESC').all();
}
/**
* Update user role
*/
function updateUserRole(id, role) {
return getAuthDb().prepare('UPDATE users SET role = ? WHERE id = ?').run(role, id);
}
/**
* Delete user
*/
function deleteUser(id) {
return getAuthDb().prepare('DELETE FROM users WHERE id = ?').run(id);
}
/**
* Count admins
*/
function countAdmins() {
return getAuthDb().prepare("SELECT COUNT(*) as count FROM users WHERE role = 'admin'").get().count;
}
/**
* Force reset admin password (for installation scripts)
*/
function resetAdminPassword(passwordHash) {
const admin = getAuthDb().prepare("SELECT * FROM users WHERE role = 'admin' ORDER BY id ASC LIMIT 1").get();
if (admin) {
return getAuthDb().prepare('UPDATE users SET password_hash = ? WHERE id = ?').run(passwordHash, admin.id);
}
return null;
}
/**
* Delete all users (for fresh install)
*/
function deleteAllUsers() {
return getAuthDb().prepare('DELETE FROM users').run();
}
// ==================== Folder Operations ====================
/**
* Get all folders
*/
function getAllFolders() {
const db = getAuthDb();
const folders = db.prepare('SELECT * FROM folders ORDER BY sort_order ASC, name ASC').all();
// Get device count per folder from auth.db assignments table
return folders.map(folder => {
const count = db.prepare(
'SELECT COUNT(*) as count FROM device_folder_assignments WHERE folder_id = ?'
).get(folder.id);
return {
...folder,
device_count: count?.count || 0
};
});
}
/**
* Get folder by ID
*/
function getFolderById(id) {
return getAuthDb().prepare('SELECT * FROM folders WHERE id = ?').get(id);
}
/**
* Create folder
*/
function createFolder(name, color, icon) {
return getAuthDb().prepare(
'INSERT INTO folders (name, color, icon) VALUES (?, ?, ?)'
).run(name, color, icon);
}
/**
* Update folder
*/
function updateFolder(id, updates) {
const sets = [];
const params = [];
if (updates.name !== undefined) {
sets.push('name = ?');
params.push(updates.name);
}
if (updates.color !== undefined) {
sets.push('color = ?');
params.push(updates.color);
}
if (updates.icon !== undefined) {
sets.push('icon = ?');
params.push(updates.icon);
}
if (updates.sort_order !== undefined) {
sets.push('sort_order = ?');
params.push(updates.sort_order);
}
if (sets.length === 0) return;
params.push(id);
return getAuthDb().prepare(
`UPDATE folders SET ${sets.join(', ')} WHERE id = ?`
).run(...params);
}
/**
* Delete folder
*/
function deleteFolder(id) {
return getAuthDb().prepare('DELETE FROM folders WHERE id = ?').run(id);
}
/**
* Assign single device to folder
*/
function assignDeviceToFolder(deviceId, folderId) {
const db = getAuthDb();
if (folderId === null || folderId === undefined) {
// Unassign: remove from assignments table
return db.prepare('DELETE FROM device_folder_assignments WHERE device_id = ?').run(deviceId);
}
// Upsert assignment in auth.db
return db.prepare(
'INSERT INTO device_folder_assignments (device_id, folder_id) VALUES (?, ?) ON CONFLICT(device_id) DO UPDATE SET folder_id = ?, assigned_at = datetime(\'now\')'
).run(deviceId, folderId, folderId);
}
/**
* Assign multiple devices to folder
*/
function assignDevicesToFolder(deviceIds, folderId) {
const db = getAuthDb();
if (folderId === null || folderId === undefined) {
const stmt = db.prepare('DELETE FROM device_folder_assignments WHERE device_id = ?');
const unassignAll = db.transaction((ids) => {
for (const id of ids) {
stmt.run(id);
}
});
return unassignAll(deviceIds);
}
const stmt = db.prepare(
'INSERT INTO device_folder_assignments (device_id, folder_id) VALUES (?, ?) ON CONFLICT(device_id) DO UPDATE SET folder_id = ?, assigned_at = datetime(\'now\')'
);
const assignAll = db.transaction((ids) => {
for (const id of ids) {
stmt.run(id, folderId, folderId);
}
});
return assignAll(deviceIds);
}
/**
* Unassign all devices from folder
*/
function unassignDevicesFromFolder(folderId) {
return getAuthDb().prepare('DELETE FROM device_folder_assignments WHERE folder_id = ?').run(folderId);
}
/**
* Get unassigned device count
* Note: This requires total device count passed externally when using Go backend.
* Falls back to local peer table count when available.
*/
function getUnassignedDeviceCount() {
try {
const mainDb = getDb();
const total = mainDb.prepare(
'SELECT COUNT(*) as count FROM peer WHERE is_deleted = 0'
).get().count;
const assigned = getAuthDb().prepare(
'SELECT COUNT(*) as count FROM device_folder_assignments'
).get().count;
return Math.max(0, total - assigned);
} catch {
// If main DB is unavailable (BetterDesk mode), return -1 to signal unknown
return -1;
}
}
/**
* Get all device folder assignments as a map { device_id: folder_id }
*/
function getAllFolderAssignments() {
const rows = getAuthDb().prepare('SELECT device_id, folder_id FROM device_folder_assignments').all();
const map = {};
for (const row of rows) {
map[row.device_id] = row.folder_id;
}
return map;
}
// ==================== Access Token Operations ====================
/**
* Create an access token for RustDesk client API
*/
function createAccessToken(token, userId, clientId, clientUuid, expiresAt, ipAddress) {
return getAuthDb().prepare(
'INSERT INTO access_tokens (token, user_id, client_id, client_uuid, expires_at, ip_address) VALUES (?, ?, ?, ?, ?, ?)'
).run(token, userId, clientId || '', clientUuid || '', expiresAt, ipAddress || '');
}
/**
* Get access token record (non-revoked, non-expired)
*/
function getAccessToken(token) {
return getAuthDb().prepare(
"SELECT * FROM access_tokens WHERE token = ? AND revoked = 0 AND expires_at > datetime('now')"
).get(token);
}
/**
* Update last_used timestamp for token
*/
function touchAccessToken(token) {
return getAuthDb().prepare(
"UPDATE access_tokens SET last_used = datetime('now') WHERE token = ?"
).run(token);
}
/**
* Revoke a specific token
*/
function revokeAccessToken(token) {
return getAuthDb().prepare(
'UPDATE access_tokens SET revoked = 1 WHERE token = ?'
).run(token);
}
/**
* Revoke all tokens for a user + client combo
*/
function revokeUserClientTokens(userId, clientId, clientUuid) {
return getAuthDb().prepare(
'UPDATE access_tokens SET revoked = 1 WHERE user_id = ? AND client_id = ? AND client_uuid = ?'
).run(userId, clientId || '', clientUuid || '');
}
/**
* Revoke all tokens for a user
*/
function revokeAllUserTokens(userId) {
return getAuthDb().prepare(
'UPDATE access_tokens SET revoked = 1 WHERE user_id = ?'
).run(userId);
}
/**
* Cleanup expired tokens (housekeeping)
*/
function cleanupExpiredTokens() {
return getAuthDb().prepare(
"DELETE FROM access_tokens WHERE expires_at < datetime('now') OR revoked = 1"
).run();
}
// ==================== Login Attempt Tracking ====================
/**
* Record a login attempt
*/
function recordLoginAttempt(username, ipAddress, success) {
return getAuthDb().prepare(
'INSERT INTO login_attempts (username, ip_address, success) VALUES (?, ?, ?)'
).run(username, ipAddress || '', success ? 1 : 0);
}
/**
* Count recent failed attempts for a username (within window)
*/
function countRecentFailedAttempts(username, windowMinutes) {
const result = getAuthDb().prepare(
"SELECT COUNT(*) as count FROM login_attempts WHERE username = ? AND success = 0 AND created_at > datetime('now', ? || ' minutes')"
).get(username, `-${windowMinutes}`);
return result ? result.count : 0;
}
/**
* Count recent failed attempts from an IP (within window)
*/
function countRecentFailedAttemptsFromIp(ipAddress, windowMinutes) {
const result = getAuthDb().prepare(
"SELECT COUNT(*) as count FROM login_attempts WHERE ip_address = ? AND success = 0 AND created_at > datetime('now', ? || ' minutes')"
).get(ipAddress, `-${windowMinutes}`);
return result ? result.count : 0;
}
/**
* Lock an account
*/
function lockAccount(username, lockedUntil, attemptCount) {
return getAuthDb().prepare(
'INSERT OR REPLACE INTO account_lockouts (username, locked_until, attempt_count) VALUES (?, ?, ?)'
).run(username, lockedUntil, attemptCount);
}
/**
* Check if account is locked
*/
function getAccountLockout(username) {
return getAuthDb().prepare(
"SELECT * FROM account_lockouts WHERE username = ? AND locked_until > datetime('now')"
).get(username);
}
/**
* Clear account lockout
*/
function clearAccountLockout(username) {
return getAuthDb().prepare(
'DELETE FROM account_lockouts WHERE username = ?'
).run(username);
}
/**
* Cleanup old login attempts (older than 24h)
*/
function cleanupOldLoginAttempts() {
return getAuthDb().prepare(
"DELETE FROM login_attempts WHERE created_at < datetime('now', '-24 hours')"
).run();
}
// ==================== Address Book Operations ====================
/**
* Get address book data for a user
* @param {number} userId
* @param {string} abType - 'legacy' or 'personal'
* @returns {string} JSON string of address book data
*/
function getAddressBook(userId, abType = 'legacy') {
const row = getAuthDb().prepare(
'SELECT data FROM address_books WHERE user_id = ? AND ab_type = ?'
).get(userId, abType);
return row ? row.data : '{}';
}
/**
* Save address book data for a user
* @param {number} userId
* @param {string} data - JSON string
* @param {string} abType - 'legacy' or 'personal'
*/
function saveAddressBook(userId, data, abType = 'legacy') {
return getAuthDb().prepare(`
INSERT INTO address_books (user_id, ab_type, data, updated_at)
VALUES (?, ?, ?, datetime('now'))
ON CONFLICT(user_id, ab_type)
DO UPDATE SET data = excluded.data, updated_at = datetime('now')
`).run(userId, abType, data);
}
/**
* Get address book tags for a user
* @param {number} userId
* @returns {string[]} Array of tags
*/
function getAddressBookTags(userId) {
const data = getAddressBook(userId, 'legacy');
try {
const parsed = JSON.parse(data);
return parsed.tags || [];
} catch {
return [];
}
}
// ==================== Console Settings ====================
/**
* Get a console setting by key
* @param {string} key
* @param {string} [defaultValue] - Fallback if key not found
* @returns {string|null}
*/
function getSetting(key, defaultValue = null) {
const row = getAuthDb().prepare('SELECT value FROM settings WHERE key = ?').get(key);
return row ? row.value : defaultValue;
}
/**
* Save a console setting (upsert)
* @param {string} key
* @param {string} value
*/
function setSetting(key, value) {
return getAuthDb().prepare(`
INSERT INTO settings (key, value, updated_at)
VALUES (?, ?, datetime('now'))
ON CONFLICT(key)
DO UPDATE SET value = excluded.value, updated_at = datetime('now')
`).run(key, value);
}
/**
* Get all console settings
* @returns {Object} key-value pairs
*/
function getAllSettings() {
const rows = getAuthDb().prepare('SELECT key, value FROM settings').all();
const result = {};
for (const row of rows) {
result[row.key] = row.value;
}
return result;
}
// ==================== Pending Registrations (LAN Discovery) ====================
function ensureRegistrationTable() {
const mainDb = getDb();
mainDb.exec(`
CREATE TABLE IF NOT EXISTS pending_registrations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
device_id TEXT NOT NULL,
hostname TEXT DEFAULT '',
platform TEXT DEFAULT '',
version TEXT DEFAULT '',
ip_address TEXT DEFAULT '',
public_key TEXT DEFAULT '',
uuid TEXT DEFAULT '',
status TEXT DEFAULT 'pending',
approved_by TEXT DEFAULT NULL,
approved_at TEXT DEFAULT NULL,
rejected_reason TEXT DEFAULT '',
access_token TEXT DEFAULT NULL,
console_url TEXT DEFAULT NULL,
server_address TEXT DEFAULT NULL,
server_key TEXT DEFAULT NULL,
created_at DATETIME DEFAULT (datetime('now')),
updated_at DATETIME DEFAULT (datetime('now')),
UNIQUE(device_id)
);
CREATE INDEX IF NOT EXISTS idx_pending_reg_status ON pending_registrations (status);
CREATE INDEX IF NOT EXISTS idx_pending_reg_device ON pending_registrations (device_id);
`);
}
// Call on module load to ensure table exists
try { ensureRegistrationTable(); } catch (_) { /* db not ready yet */ }
function getPendingRegistrations(filters = {}) {
ensureRegistrationTable();
const mainDb = getDb();
let sql = 'SELECT * FROM pending_registrations WHERE 1=1';
const params = [];
if (filters.status) {
sql += ' AND status = ?';
params.push(filters.status);
}
if (filters.search) {
sql += " AND (device_id LIKE ? ESCAPE '\\' OR hostname LIKE ? ESCAPE '\\' OR ip_address LIKE ? ESCAPE '\\')";
const escaped = escapeLikePattern(filters.search);
const s = `%${escaped}%`;
params.push(s, s, s);
}
sql += ' ORDER BY created_at DESC';
return mainDb.prepare(sql).all(...params);
}
function getPendingRegistrationById(id) {
ensureRegistrationTable();
return getDb().prepare('SELECT * FROM pending_registrations WHERE id = ?').get(id);
}
function getPendingRegistrationByDeviceId(deviceId) {
ensureRegistrationTable();
return getDb().prepare('SELECT * FROM pending_registrations WHERE device_id = ?').get(deviceId);
}
function createPendingRegistration(data) {
ensureRegistrationTable();
const mainDb = getDb();
// Check if already exists
const existing = mainDb.prepare('SELECT * FROM pending_registrations WHERE device_id = ?').get(data.device_id);
if (existing) {
if (existing.status === 'approved') {
return existing; // Already approved — return as-is
}
// Update existing (reset to pending if rejected, or update fields)
mainDb.prepare(`
UPDATE pending_registrations
SET hostname = ?, platform = ?, version = ?, ip_address = ?,
public_key = ?, uuid = ?, status = 'pending',
rejected_reason = '', updated_at = datetime('now')
WHERE device_id = ?
`).run(
data.hostname || '', data.platform || '', data.version || '',
data.ip_address || '', data.public_key || '', data.uuid || '',
data.device_id
);
return mainDb.prepare('SELECT * FROM pending_registrations WHERE device_id = ?').get(data.device_id);
}
// Create new
const result = mainDb.prepare(`
INSERT INTO pending_registrations (device_id, hostname, platform, version, ip_address, public_key, uuid)
VALUES (?, ?, ?, ?, ?, ?, ?)
`).run(
data.device_id, data.hostname || '', data.platform || '',
data.version || '', data.ip_address || '', data.public_key || '',
data.uuid || ''
);
return mainDb.prepare('SELECT * FROM pending_registrations WHERE id = ?').get(result.lastInsertRowid);
}
function approvePendingRegistration(id, approvedBy, serverConfig) {
ensureRegistrationTable();
const mainDb = getDb();
mainDb.prepare(`
UPDATE pending_registrations
SET status = 'approved', approved_by = ?, approved_at = datetime('now'),
access_token = ?, console_url = ?, server_address = ?, server_key = ?,
updated_at = datetime('now')
WHERE id = ?
`).run(
approvedBy,
serverConfig.access_token || '',
serverConfig.console_url || '',
serverConfig.server_address || '',
serverConfig.server_key || '',
id
);
return mainDb.prepare('SELECT * FROM pending_registrations WHERE id = ?').get(id);
}
function rejectPendingRegistration(id, reason) {
ensureRegistrationTable();
const mainDb = getDb();
mainDb.prepare(`
UPDATE pending_registrations
SET status = 'rejected', rejected_reason = ?, updated_at = datetime('now')
WHERE id = ?
`).run(reason || '', id);
return mainDb.prepare('SELECT * FROM pending_registrations WHERE id = ?').get(id);
}
function deletePendingRegistration(id) {
ensureRegistrationTable();
getDb().prepare('DELETE FROM pending_registrations WHERE id = ?').run(id);
}
function getPendingRegistrationCount() {
ensureRegistrationTable();
const row = getDb().prepare("SELECT COUNT(*) as count FROM pending_registrations WHERE status = 'pending'").get();
return row ? row.count : 0;
}
// ==================== Peer Sysinfo Operations ====================
/**
* Upsert peer system information (reported via POST /api/sysinfo)
* @param {string} peerId - RustDesk device ID
* @param {Object} data - Sysinfo payload from RustDesk client
*/
function upsertPeerSysinfo(peerId, data) {
return getAuthDb().prepare(`
INSERT INTO peer_sysinfo (peer_id, hostname, username, platform, version,
cpu_name, cpu_cores, cpu_freq_ghz, memory_gb, os_full,
displays, encoding, features, platform_additions, raw_json, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
ON CONFLICT(peer_id) DO UPDATE SET
hostname = excluded.hostname, username = excluded.username,
platform = excluded.platform, version = excluded.version,
cpu_name = excluded.cpu_name, cpu_cores = excluded.cpu_cores,
cpu_freq_ghz = excluded.cpu_freq_ghz, memory_gb = excluded.memory_gb,
os_full = excluded.os_full, displays = excluded.displays,
encoding = excluded.encoding, features = excluded.features,
platform_additions = excluded.platform_additions,
raw_json = excluded.raw_json, updated_at = datetime('now')
`).run(
peerId,
data.hostname || '',
data.username || '',
data.platform || '',
data.version || '',
data.cpu_name || '',
data.cpu_cores || 0,
data.cpu_freq_ghz || 0,
data.memory_gb || 0,
data.os_full || '',
JSON.stringify(data.displays || []),
JSON.stringify(data.encoding || []),
JSON.stringify(data.features || {}),
JSON.stringify(data.platform_additions || {}),
JSON.stringify(data)
);
}
/**
* Get sysinfo for a single peer
*/
function getPeerSysinfo(peerId) {
const row = getAuthDb().prepare('SELECT * FROM peer_sysinfo WHERE peer_id = ?').get(peerId);
if (!row) return null;
return parseSysinfoRow(row);
}
/**
* Get sysinfo for all peers
*/
function getAllPeerSysinfo() {
const rows = getAuthDb().prepare('SELECT * FROM peer_sysinfo').all();
return rows.map(parseSysinfoRow);
}
/**
* Parse sysinfo row — deserialize JSON fields
*/
function parseSysinfoRow(row) {
return {
peer_id: row.peer_id,
hostname: row.hostname,
username: row.username,
platform: row.platform,
version: row.version,
cpu_name: row.cpu_name,
cpu_cores: row.cpu_cores,
cpu_freq_ghz: row.cpu_freq_ghz,
memory_gb: row.memory_gb,
os_full: row.os_full,
displays: safeJsonParse(row.displays, []),
encoding: safeJsonParse(row.encoding, []),
features: safeJsonParse(row.features, {}),
platform_additions: safeJsonParse(row.platform_additions, {}),
updated_at: row.updated_at
};
}
/**
* Safe JSON parse with fallback
*/
function safeJsonParse(str, fallback) {
try {
return JSON.parse(str);
} catch {
return fallback;
}
}
// ==================== Peer Metrics Operations ====================
/**
* Update peer online status and last_online timestamp in the shared peer table.
* Used by heartbeat handler to mark a device as online.
*/
function updatePeerOnlineStatus(peerId) {
return getDb().prepare(
"UPDATE peer SET status_online = 1, last_online = datetime('now'), last_heartbeat = datetime('now'), heartbeat_count = COALESCE(heartbeat_count, 0) + 1 WHERE id = ?"
).run(peerId);
}
/**
* Mark peers as offline if their last_heartbeat is older than the given threshold.
* This prevents stale "online" status when devices stop sending heartbeats.
* @param {number} thresholdSeconds - seconds of inactivity before marking offline
* @returns {{ changes: number }} number of peers marked offline
*/
function cleanupStaleOnlinePeers(thresholdSeconds = 90) {
return getDb().prepare(
`UPDATE peer SET status_online = 0
WHERE status_online = 1
AND last_heartbeat IS NOT NULL
AND last_heartbeat < datetime('now', '-' || ? || ' seconds')`
).run(thresholdSeconds);
}
/**
* Insert a heartbeat metric data point
*/
function insertPeerMetric(peerId, cpuUsage, memoryUsage, diskUsage) {
return getAuthDb().prepare(
'INSERT INTO peer_metrics (peer_id, cpu_usage, memory_usage, disk_usage) VALUES (?, ?, ?, ?)'
).run(peerId, cpuUsage || 0, memoryUsage || 0, diskUsage || 0);
}
/**
* Get recent metrics for a peer
* @param {string} peerId
* @param {number} limit - Max records to return (default 100)
*/
function getPeerMetrics(peerId, limit = 100) {
return getAuthDb().prepare(
'SELECT * FROM peer_metrics WHERE peer_id = ? ORDER BY created_at DESC LIMIT ?'
).all(peerId, limit);
}
/**
* Get latest metric for a peer (most recent heartbeat)
*/
function getLatestPeerMetric(peerId) {
return getAuthDb().prepare(
'SELECT * FROM peer_metrics WHERE peer_id = ? ORDER BY created_at DESC LIMIT 1'
).get(peerId);
}
/**
* Cleanup old metrics (older than N days)
* @param {number} days - Minimum 1, default 7
*/
function cleanupOldMetrics(days = 7) {
const safeDays = Math.max(1, parseInt(days, 10) || 7);
return getAuthDb().prepare(
"DELETE FROM peer_metrics WHERE created_at < datetime('now', ? || ' days')"
).run(`-${safeDays}`);
}
// ==================== Audit Connection Operations ====================
/**
* Insert a connection audit event
* @param {Object} data - { host_id, host_uuid, peer_id, peer_name, action, conn_type, session_id, ip }
*/
function insertAuditConnection(data) {
return getAuthDb().prepare(`
INSERT INTO audit_connections (host_id, host_uuid, peer_id, peer_name, action, conn_type, session_id, ip)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`).run(
data.host_id || '',
data.host_uuid || '',
data.peer_id || '',
data.peer_name || '',
data.action || '',
data.conn_type || 0,
data.session_id || '',
data.ip || ''
);
}
/**
* Query audit connections with filters
* @param {Object} filters - { host_id, peer_id, action, limit, offset }
*/
function getAuditConnections(filters = {}) {
let sql = 'SELECT * FROM audit_connections WHERE 1=1';
const params = [];
if (filters.host_id) {
sql += ' AND host_id = ?';
params.push(filters.host_id);
}
if (filters.peer_id) {
sql += ' AND peer_id = ?';
params.push(filters.peer_id);
}
if (filters.action) {
sql += ' AND action = ?';
params.push(filters.action);
}
sql += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
params.push(filters.limit || 100);
params.push(filters.offset || 0);
return getAuthDb().prepare(sql).all(...params);
}
/**
* Count audit connections (for pagination)
*/
function countAuditConnections(filters = {}) {
let sql = 'SELECT COUNT(*) as count FROM audit_connections WHERE 1=1';
const params = [];
if (filters.host_id) {
sql += ' AND host_id = ?';
params.push(filters.host_id);
}
if (filters.peer_id) {
sql += ' AND peer_id = ?';
params.push(filters.peer_id);
}
if (filters.action) {
sql += ' AND action = ?';
params.push(filters.action);
}
return getAuthDb().prepare(sql).get(...params).count;
}
// ==================== Audit File Transfer Operations ====================
/**
* Insert a file transfer audit event
* @param {Object} data - { host_id, host_uuid, peer_id, direction, path, is_file, num_files, files_json, ip, peer_name }
*/
function insertAuditFile(data) {
return getAuthDb().prepare(`
INSERT INTO audit_files (host_id, host_uuid, peer_id, direction, path, is_file, num_files, files_json, ip, peer_name)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
data.host_id || '',
data.host_uuid || '',
data.peer_id || '',
data.direction || 0,
data.path || '',
data.is_file !== undefined ? (data.is_file ? 1 : 0) : 1,
data.num_files || 0,
JSON.stringify(data.files || []),
data.ip || '',
data.peer_name || ''
);
}
/**
* Query file transfer audit events
*/
function getAuditFiles(filters = {}) {
let sql = 'SELECT * FROM audit_files WHERE 1=1';
const params = [];
if (filters.host_id) {
sql += ' AND host_id = ?';
params.push(filters.host_id);
}
if (filters.peer_id) {
sql += ' AND peer_id = ?';
params.push(filters.peer_id);
}
sql += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
params.push(filters.limit || 100);
params.push(filters.offset || 0);
return getAuthDb().prepare(sql).all(...params);
}
/**
* Count file transfer audit events
*/
function countAuditFiles(filters = {}) {
let sql = 'SELECT COUNT(*) as count FROM audit_files WHERE 1=1';
const params = [];
if (filters.host_id) {
sql += ' AND host_id = ?';
params.push(filters.host_id);
}
if (filters.peer_id) {
sql += ' AND peer_id = ?';
params.push(filters.peer_id);
}
return getAuthDb().prepare(sql).get(...params).count;
}
// ==================== Audit Alarm Operations ====================
/**
* Insert a security alarm event
* Alarm types: 0=AccessAttempt, 1=BruteForce, 2=IPViolation, 3=Unauthorized,
* 4=PortScan, 5=MaliciousFile, 6=Custom
* @param {Object} data - { alarm_type, alarm_name, host_id, peer_id, ip, details }
*/
function insertAuditAlarm(data) {
return getAuthDb().prepare(`
INSERT INTO audit_alarms (alarm_type, alarm_name, host_id, peer_id, ip, details)
VALUES (?, ?, ?, ?, ?, ?)
`).run(
data.alarm_type || 0,
data.alarm_name || '',
data.host_id || '',
data.peer_id || '',
data.ip || '',
typeof data.details === 'string' ? data.details : JSON.stringify(data.details || {})
);
}
/**
* Query security alarm events
*/
function getAuditAlarms(filters = {}) {
let sql = 'SELECT * FROM audit_alarms WHERE 1=1';
const params = [];
if (filters.alarm_type !== undefined) {
sql += ' AND alarm_type = ?';
params.push(filters.alarm_type);
}
if (filters.host_id) {
sql += ' AND host_id = ?';
params.push(filters.host_id);
}
sql += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
params.push(filters.limit || 100);
params.push(filters.offset || 0);
return getAuthDb().prepare(sql).all(...params);
}
/**
* Count alarm events
*/
function countAuditAlarms(filters = {}) {
let sql = 'SELECT COUNT(*) as count FROM audit_alarms WHERE 1=1';
const params = [];
if (filters.alarm_type !== undefined) {
sql += ' AND alarm_type = ?';
params.push(filters.alarm_type);
}
if (filters.host_id) {
sql += ' AND host_id = ?';
params.push(filters.host_id);
}
return getAuthDb().prepare(sql).get(...params).count;
}
// ==================== User Group Operations ====================
/**
* Get all user groups
*/
function getAllUserGroups() {
return getAuthDb().prepare('SELECT * FROM user_groups ORDER BY name ASC').all();
}
/**
* Get user group by GUID
*/
function getUserGroupByGuid(guid) {
return getAuthDb().prepare('SELECT * FROM user_groups WHERE guid = ?').get(guid);
}
/**
* Create a user group
*/
function createUserGroup(data) {
const crypto = require('crypto');
const guid = data.guid || crypto.randomUUID();
getAuthDb().prepare(
'INSERT INTO user_groups (guid, name, note, team_id) VALUES (?, ?, ?, ?)'
).run(guid, data.name, data.note || '', data.team_id || '');
return getAuthDb().prepare('SELECT * FROM user_groups WHERE guid = ?').get(guid);
}
/**
* Update a user group
*/
function updateUserGroup(guid, data) {
const sets = [];
const params = [];
if (data.name !== undefined) { sets.push('name = ?'); params.push(data.name); }
if (data.note !== undefined) { sets.push('note = ?'); params.push(data.note); }
if (data.team_id !== undefined) { sets.push('team_id = ?'); params.push(data.team_id); }
if (sets.length === 0) return null;
params.push(guid);
getAuthDb().prepare(`UPDATE user_groups SET ${sets.join(', ')} WHERE guid = ?`).run(...params);
return getAuthDb().prepare('SELECT * FROM user_groups WHERE guid = ?').get(guid);
}
/**
* Delete a user group
*/
function deleteUserGroup(guid) {
return getAuthDb().prepare('DELETE FROM user_groups WHERE guid = ?').run(guid);
}
// ==================== Device Group Operations ====================
/**
* Get all device groups with member counts
*/
function getAllDeviceGroups() {
const groups = getAuthDb().prepare('SELECT * FROM device_groups ORDER BY name ASC').all();
for (const g of groups) {
g.member_count = getAuthDb().prepare(
'SELECT COUNT(*) as c FROM device_group_members WHERE device_group_id = ?'
).get(g.id).c;
}
return groups;
}
/**
* Get device group by GUID
*/
function getDeviceGroupByGuid(guid) {
return getAuthDb().prepare('SELECT * FROM device_groups WHERE guid = ?').get(guid);
}
/**
* Create a device group
*/
function createDeviceGroup(data) {
const crypto = require('crypto');
const guid = data.guid || crypto.randomUUID();
getAuthDb().prepare(
'INSERT INTO device_groups (guid, name, note, team_id) VALUES (?, ?, ?, ?)'
).run(guid, data.name, data.note || '', data.team_id || '');
return getAuthDb().prepare('SELECT * FROM device_groups WHERE guid = ?').get(guid);
}
/**
* Update a device group
*/
function updateDeviceGroup(guid, data) {
const sets = [];
const params = [];
if (data.name !== undefined) { sets.push('name = ?'); params.push(data.name); }
if (data.note !== undefined) { sets.push('note = ?'); params.push(data.note); }
if (data.team_id !== undefined) { sets.push('team_id = ?'); params.push(data.team_id); }
if (sets.length === 0) return null;
params.push(guid);
getAuthDb().prepare(`UPDATE device_groups SET ${sets.join(', ')} WHERE guid = ?`).run(...params);
return getAuthDb().prepare('SELECT * FROM device_groups WHERE guid = ?').get(guid);
}
/**
* Delete a device group (cascades to memberships)
*/
function deleteDeviceGroup(guid) {
const group = getAuthDb().prepare('SELECT id FROM device_groups WHERE guid = ?').get(guid);
if (!group) return { changes: 0 };
return getAuthDb().prepare('DELETE FROM device_groups WHERE guid = ?').run(guid);
}
/**
* Add device to group
*/
function addDeviceToGroup(groupGuid, peerId) {
const group = getAuthDb().prepare('SELECT id FROM device_groups WHERE guid = ?').get(groupGuid);
if (!group) return null;
return getAuthDb().prepare(
'INSERT OR IGNORE INTO device_group_members (device_group_id, peer_id) VALUES (?, ?)'
).run(group.id, peerId);
}
/**
* Remove device from group
*/
function removeDeviceFromGroup(groupGuid, peerId) {
const group = getAuthDb().prepare('SELECT id FROM device_groups WHERE guid = ?').get(groupGuid);
if (!group) return null;
return getAuthDb().prepare(
'DELETE FROM device_group_members WHERE device_group_id = ? AND peer_id = ?'
).run(group.id, peerId);
}
/**
* Get all members of a device group
*/
function getDeviceGroupMembers(groupGuid) {
const group = getAuthDb().prepare('SELECT id FROM device_groups WHERE guid = ?').get(groupGuid);
if (!group) return [];
return getAuthDb().prepare(
'SELECT peer_id FROM device_group_members WHERE device_group_id = ?'
).all(group.id).map(r => r.peer_id);
}
/**
* Get device groups for a specific peer
*/
function getDeviceGroupsForPeer(peerId) {
return getAuthDb().prepare(`
SELECT dg.* FROM device_groups dg
INNER JOIN device_group_members dgm ON dg.id = dgm.device_group_id
WHERE dgm.peer_id = ?
ORDER BY dg.name ASC
`).all(peerId);
}
// ==================== Strategy / Policy Operations ====================
/**
* Get all strategies
*/
function getAllStrategies() {
const rows = getAuthDb().prepare('SELECT * FROM strategies ORDER BY name ASC').all();
return rows.map(r => ({
...r,
permissions: safeJsonParse(r.permissions, {})
}));
}
/**
* Get strategy by GUID
*/
function getStrategyByGuid(guid) {
const row = getAuthDb().prepare('SELECT * FROM strategies WHERE guid = ?').get(guid);
if (!row) return null;
return { ...row, permissions: safeJsonParse(row.permissions, {}) };
}
/**
* Create a strategy
*/
function createStrategy(data) {
const crypto = require('crypto');
const guid = data.guid || crypto.randomUUID();
getAuthDb().prepare(`
INSERT INTO strategies (guid, name, user_group_guid, device_group_guid, enabled, permissions)
VALUES (?, ?, ?, ?, ?, ?)
`).run(
guid,
data.name,
data.user_group_guid || '',
data.device_group_guid || '',
data.enabled !== undefined ? (data.enabled ? 1 : 0) : 1,
JSON.stringify(data.permissions || {})
);
return getStrategyByGuid(guid);
}
/**
* Update a strategy
*/
function updateStrategy(guid, data) {
const sets = [];
const params = [];
if (data.name !== undefined) { sets.push('name = ?'); params.push(data.name); }
if (data.user_group_guid !== undefined) { sets.push('user_group_guid = ?'); params.push(data.user_group_guid); }
if (data.device_group_guid !== undefined) { sets.push('device_group_guid = ?'); params.push(data.device_group_guid); }
if (data.enabled !== undefined) { sets.push('enabled = ?'); params.push(data.enabled ? 1 : 0); }
if (data.permissions !== undefined) { sets.push('permissions = ?'); params.push(JSON.stringify(data.permissions)); }
if (sets.length === 0) return null;
sets.push("updated_at = datetime('now')");
params.push(guid);
getAuthDb().prepare(`UPDATE strategies SET ${sets.join(', ')} WHERE guid = ?`).run(...params);
return getStrategyByGuid(guid);
}
/**
* Delete a strategy
*/
function deleteStrategy(guid) {
return getAuthDb().prepare('DELETE FROM strategies WHERE guid = ?').run(guid);
}
// ==================== Housekeeping Extensions ====================
/**
* Run extended housekeeping tasks for RustDesk integration tables
*/
function runIntegrationHousekeeping() {
const authDb = getAuthDb();
// Cleanup metrics older than 7 days
authDb.prepare("DELETE FROM peer_metrics WHERE created_at < datetime('now', '-7 days')").run();
// Cleanup audit events older than 90 days
authDb.prepare("DELETE FROM audit_connections WHERE created_at < datetime('now', '-90 days')").run();
authDb.prepare("DELETE FROM audit_files WHERE created_at < datetime('now', '-90 days')").run();
authDb.prepare("DELETE FROM audit_alarms WHERE created_at < datetime('now', '-90 days')").run();
}
// ==================== Close connections ====================
function closeAll() {
if (db) {
db.close();
db = null;
}
if (authDb) {
authDb.close();
authDb = null;
}
}
module.exports = {
getDb,
getAuthDb,
// Devices
getAllDevices,
getDeviceById,
getDevice: getDeviceById, // Alias for compatibility
updateDevice,
deleteDevice,
setBanStatus,
getStats,
countDevices,
// Users
getUserByUsername,
getUserById,
createUser,
updateUserPassword,
updateLastLogin,
hasUsers,
getAllUsers,
updateUserRole,
deleteUser,
countAdmins,
// TOTP
saveTotpSecret,
enableTotp,
disableTotp,
useRecoveryCode,
resetAdminPassword,
deleteAllUsers,
// Folders
getAllFolders,
getFolderById,
createFolder,
updateFolder,
deleteFolder,
assignDeviceToFolder,
assignDevicesToFolder,
unassignDevicesFromFolder,
getUnassignedDeviceCount,
getAllFolderAssignments,
// Audit
logAction,
getAuditLogs,
// Access tokens (RustDesk client API)
createAccessToken,
getAccessToken,
touchAccessToken,
revokeAccessToken,
revokeUserClientTokens,
revokeAllUserTokens,
cleanupExpiredTokens,
// Login attempt tracking
recordLoginAttempt,
countRecentFailedAttempts,
countRecentFailedAttemptsFromIp,
lockAccount,
getAccountLockout,
clearAccountLockout,
cleanupOldLoginAttempts,
// Address books
getAddressBook,
saveAddressBook,
getAddressBookTags,
// Console settings
getSetting,
setSetting,
getAllSettings,
// Pending Registrations (LAN Discovery)
getPendingRegistrations,
getPendingRegistrationById,
getPendingRegistrationByDeviceId,
createPendingRegistration,
approvePendingRegistration,
rejectPendingRegistration,
deletePendingRegistration,
getPendingRegistrationCount,
// Peer Sysinfo (RustDesk client integration)
upsertPeerSysinfo,
getPeerSysinfo,
getAllPeerSysinfo,
// Peer Metrics (heartbeat telemetry)
updatePeerOnlineStatus,
cleanupStaleOnlinePeers,
insertPeerMetric,
getPeerMetrics,
getLatestPeerMetric,
cleanupOldMetrics,
// Audit: connections
insertAuditConnection,
getAuditConnections,
countAuditConnections,
// Audit: file transfers
insertAuditFile,
getAuditFiles,
countAuditFiles,
// Audit: security alarms
insertAuditAlarm,
getAuditAlarms,
countAuditAlarms,
// User groups
getAllUserGroups,
getUserGroupByGuid,
createUserGroup,
updateUserGroup,
deleteUserGroup,
// Device groups
getAllDeviceGroups,
getDeviceGroupByGuid,
createDeviceGroup,
updateDeviceGroup,
deleteDeviceGroup,
addDeviceToGroup,
removeDeviceFromGroup,
getDeviceGroupMembers,
getDeviceGroupsForPeer,
// Strategies / policies
getAllStrategies,
getStrategyByGuid,
createStrategy,
updateStrategy,
deleteStrategy,
// Housekeeping extensions
runIntegrationHousekeeping,
// Cleanup
closeAll
};