mirror of
https://github.com/UNITRONIX/BetterDesk.git
synced 2026-09-10 17:45:42 +00:00
e855f5786d
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.
1999 lines
60 KiB
JavaScript
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
|
|
};
|