mirror of
https://github.com/kunkundi/crossdesk-server.git
synced 2026-10-26 01:34:33 +08:00
738 lines
22 KiB
C++
738 lines
22 KiB
C++
#include "device_db_manager.h"
|
|
|
|
#include <openssl/sha.h>
|
|
|
|
#include <chrono>
|
|
#include <filesystem>
|
|
#include <iomanip>
|
|
#include <iostream>
|
|
#include <limits>
|
|
#include <random>
|
|
#include <sstream>
|
|
#include <stdexcept>
|
|
#include <unordered_map>
|
|
|
|
#include "log.h"
|
|
|
|
namespace {
|
|
|
|
std::string ColumnText(sqlite3_stmt* stmt, int column) {
|
|
const unsigned char* text = sqlite3_column_text(stmt, column);
|
|
return text ? reinterpret_cast<const char*>(text) : "";
|
|
}
|
|
|
|
std::string SqliteExecError(sqlite3* db, char* err_msg) {
|
|
return err_msg ? std::string(err_msg) : std::string(sqlite3_errmsg(db));
|
|
}
|
|
|
|
std::string EscapeLikePattern(const std::string& value) {
|
|
std::string escaped;
|
|
escaped.reserve(value.size());
|
|
for (char ch : value) {
|
|
if (ch == '%' || ch == '_' || ch == '\\') {
|
|
escaped.push_back('\\');
|
|
}
|
|
escaped.push_back(ch);
|
|
}
|
|
return escaped;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
DeviceDBManager::DeviceDBManager(const std::string& db_path) : db_(nullptr) {
|
|
try {
|
|
std::filesystem::path path(db_path);
|
|
if (!path.parent_path().empty()) {
|
|
std::filesystem::create_directories(path.parent_path());
|
|
}
|
|
} catch (const std::exception& e) {
|
|
throw std::runtime_error("Failed to create parent directory for DB: " +
|
|
std::string(e.what()));
|
|
}
|
|
|
|
int rc = sqlite3_open(db_path.c_str(), &db_);
|
|
if (rc != SQLITE_OK) {
|
|
std::string error =
|
|
db_ ? sqlite3_errmsg(db_) : std::string(sqlite3_errstr(rc));
|
|
LOG_ERROR("Failed to open database, {}", error);
|
|
if (db_) {
|
|
sqlite3_close(db_);
|
|
}
|
|
db_ = nullptr;
|
|
throw std::runtime_error("Failed to open database: " + error);
|
|
}
|
|
try {
|
|
InitDB();
|
|
} catch (...) {
|
|
sqlite3_close(db_);
|
|
db_ = nullptr;
|
|
throw;
|
|
}
|
|
}
|
|
|
|
DeviceDBManager::~DeviceDBManager() {
|
|
if (db_) sqlite3_close(db_);
|
|
}
|
|
|
|
void DeviceDBManager::InitDB() {
|
|
std::lock_guard<std::recursive_mutex> lock(db_mutex_);
|
|
if (db_ == nullptr) {
|
|
LOG_ERROR("Database is not initialized in InitDB.");
|
|
throw std::runtime_error("Database is not initialized in InitDB.");
|
|
}
|
|
|
|
const char* sql_devices =
|
|
"CREATE TABLE IF NOT EXISTS devices ("
|
|
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
|
|
"device_id TEXT UNIQUE NOT NULL,"
|
|
"password_salt TEXT NOT NULL,"
|
|
"password_hash TEXT NOT NULL);";
|
|
|
|
const char* sql_seq =
|
|
"CREATE TABLE IF NOT EXISTS device_id_seq ("
|
|
"next_id INTEGER NOT NULL);";
|
|
|
|
const char* sql_seq_init =
|
|
"INSERT INTO device_id_seq (next_id) "
|
|
"SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM device_id_seq);";
|
|
|
|
const char* sql_presence =
|
|
"CREATE TABLE IF NOT EXISTS device_presence ("
|
|
"device_id TEXT PRIMARY KEY,"
|
|
"online INTEGER NOT NULL,"
|
|
"updated_at INTEGER NOT NULL"
|
|
");";
|
|
|
|
const char* sql_user_devices =
|
|
"CREATE TABLE IF NOT EXISTS user_devices ("
|
|
"user_id TEXT NOT NULL,"
|
|
"device_id TEXT NOT NULL,"
|
|
"PRIMARY KEY (user_id, device_id)"
|
|
");";
|
|
|
|
char* err_msg = nullptr;
|
|
|
|
if (sqlite3_exec(db_, sql_devices, nullptr, nullptr, &err_msg) != SQLITE_OK) {
|
|
std::string error = SqliteExecError(db_, err_msg);
|
|
LOG_ERROR("Failed to create devices table: {}", error);
|
|
sqlite3_free(err_msg);
|
|
throw std::runtime_error("Failed to create devices table: " + error);
|
|
}
|
|
|
|
if (sqlite3_exec(db_, sql_seq, nullptr, nullptr, &err_msg) != SQLITE_OK) {
|
|
std::string error = SqliteExecError(db_, err_msg);
|
|
LOG_ERROR("Failed to create device_id_seq table: {}", error);
|
|
sqlite3_free(err_msg);
|
|
throw std::runtime_error("Failed to create device_id_seq table: " + error);
|
|
}
|
|
|
|
if (sqlite3_exec(db_, sql_seq_init, nullptr, nullptr, &err_msg) !=
|
|
SQLITE_OK) {
|
|
std::string error = SqliteExecError(db_, err_msg);
|
|
LOG_ERROR("Failed to initialize device_id_seq: {}", error);
|
|
sqlite3_free(err_msg);
|
|
throw std::runtime_error("Failed to initialize device_id_seq: " + error);
|
|
}
|
|
|
|
if (sqlite3_exec(db_, sql_presence, nullptr, nullptr, &err_msg) !=
|
|
SQLITE_OK) {
|
|
std::string error = SqliteExecError(db_, err_msg);
|
|
LOG_ERROR("Failed to create device_presence table: {}", error);
|
|
sqlite3_free(err_msg);
|
|
throw std::runtime_error("Failed to create device_presence table: " +
|
|
error);
|
|
}
|
|
|
|
if (sqlite3_exec(db_, sql_user_devices, nullptr, nullptr, &err_msg) !=
|
|
SQLITE_OK) {
|
|
std::string error = SqliteExecError(db_, err_msg);
|
|
LOG_ERROR("Failed to create user_devices table: {}", error);
|
|
sqlite3_free(err_msg);
|
|
throw std::runtime_error("Failed to create user_devices table: " + error);
|
|
}
|
|
}
|
|
|
|
std::string DeviceDBManager::Sha256(const std::string& str) {
|
|
unsigned char hash[SHA256_DIGEST_LENGTH];
|
|
SHA256(reinterpret_cast<const unsigned char*>(str.c_str()), str.size(), hash);
|
|
|
|
std::stringstream ss;
|
|
for (int i = 0; i < SHA256_DIGEST_LENGTH; ++i)
|
|
ss << std::hex << std::setw(2) << std::setfill('0') << (int)hash[i];
|
|
|
|
return ss.str();
|
|
}
|
|
|
|
std::string DeviceDBManager::GenerateSalt() {
|
|
static const char charset[] = "0123456789ABCDEF";
|
|
static std::mt19937 rng(static_cast<unsigned>(
|
|
std::chrono::steady_clock::now().time_since_epoch().count()));
|
|
std::uniform_int_distribution<int> dist(0, 15);
|
|
|
|
std::string salt;
|
|
for (int i = 0; i < 16; ++i) {
|
|
salt += charset[dist(rng)];
|
|
}
|
|
return salt;
|
|
}
|
|
|
|
std::string DeviceDBManager::HashPasswordWithSalt(const std::string& salt,
|
|
const std::string& password) {
|
|
return Sha256(salt + password);
|
|
}
|
|
|
|
bool DeviceDBManager::DeviceIdExists(const std::string& device_id) {
|
|
std::lock_guard<std::recursive_mutex> lock(db_mutex_);
|
|
if (db_ == nullptr || device_id.empty()) {
|
|
return false;
|
|
}
|
|
|
|
const char* sql = "SELECT 1 FROM devices WHERE device_id = ? LIMIT 1;";
|
|
sqlite3_stmt* stmt = nullptr;
|
|
|
|
if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
|
|
return false;
|
|
}
|
|
|
|
sqlite3_bind_text(stmt, 1, device_id.c_str(), -1, SQLITE_TRANSIENT);
|
|
bool exists = (sqlite3_step(stmt) == SQLITE_ROW);
|
|
sqlite3_finalize(stmt);
|
|
|
|
return exists;
|
|
}
|
|
|
|
std::string DeviceDBManager::GenerateDeviceId() {
|
|
std::lock_guard<std::recursive_mutex> lock(db_mutex_);
|
|
if (db_ == nullptr) {
|
|
LOG_ERROR("Database is not initialized in GenerateDeviceId.");
|
|
return {};
|
|
}
|
|
|
|
const int MIN_ID = 100000000;
|
|
const int MAX_ID = 999999999;
|
|
const int MAX_RETRIES = 100;
|
|
|
|
std::random_device rd;
|
|
std::mt19937 rng(rd());
|
|
std::uniform_int_distribution<int> dist(MIN_ID, MAX_ID);
|
|
|
|
// try to generate unique ID
|
|
for (int attempt = 0; attempt < MAX_RETRIES; ++attempt) {
|
|
int obfuscated_id = dist(rng);
|
|
|
|
char buf[10] = {0};
|
|
snprintf(buf, sizeof(buf), "%09d", obfuscated_id);
|
|
std::string device_id(buf);
|
|
|
|
// check if ID already exists
|
|
if (!DeviceIdExists(device_id)) {
|
|
return device_id;
|
|
}
|
|
}
|
|
|
|
LOG_ERROR("Failed to generate unique device ID after {} attempts.",
|
|
MAX_RETRIES);
|
|
return {};
|
|
}
|
|
|
|
std::string DeviceDBManager::GeneratePassword() {
|
|
static const char charset[] =
|
|
"0123456789"
|
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
"abcdefghijklmnopqrstuvwxyz";
|
|
static std::mt19937 rng(static_cast<unsigned>(
|
|
std::chrono::steady_clock::now().time_since_epoch().count()));
|
|
std::uniform_int_distribution<int> dist(0,
|
|
sizeof(charset) - 2); // exclude '\0'
|
|
|
|
std::string pwd;
|
|
for (int i = 0; i < 6; ++i) {
|
|
pwd += charset[dist(rng)];
|
|
}
|
|
return pwd;
|
|
}
|
|
|
|
DeviceCredential DeviceDBManager::AddDevice(const std::string& device_id,
|
|
const std::string& password) {
|
|
std::lock_guard<std::recursive_mutex> lock(db_mutex_);
|
|
if (db_ == nullptr) {
|
|
LOG_ERROR("Database is not initialized.");
|
|
return {};
|
|
}
|
|
|
|
if (!device_id.empty() && device_id != "web") {
|
|
const char* select_sql =
|
|
"SELECT password_salt, password_hash FROM devices WHERE device_id = ?;";
|
|
sqlite3_stmt* stmt = nullptr;
|
|
int rc = sqlite3_prepare_v2(db_, select_sql, -1, &stmt, nullptr);
|
|
if (rc != SQLITE_OK) {
|
|
LOG_ERROR("Failed to prepare select statement.");
|
|
return {};
|
|
}
|
|
|
|
sqlite3_bind_text(stmt, 1, device_id.c_str(), -1, SQLITE_TRANSIENT);
|
|
|
|
rc = sqlite3_step(stmt);
|
|
if (rc == SQLITE_ROW) {
|
|
// Device exists
|
|
std::string salt = ColumnText(stmt, 0);
|
|
std::string stored_hash = ColumnText(stmt, 1);
|
|
std::string hash = HashPasswordWithSalt(salt, password);
|
|
|
|
sqlite3_finalize(stmt);
|
|
if (stored_hash != hash) {
|
|
// Update password
|
|
const char* update_sql =
|
|
"UPDATE devices SET password_hash = ?, password_salt = ? WHERE "
|
|
"device_id = ?;";
|
|
if (sqlite3_prepare_v2(db_, update_sql, -1, &stmt, nullptr) !=
|
|
SQLITE_OK) {
|
|
LOG_ERROR("Failed to prepare update statement.");
|
|
return {};
|
|
}
|
|
sqlite3_bind_text(stmt, 1, hash.c_str(), -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 2, salt.c_str(), -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 3, device_id.c_str(), -1, SQLITE_TRANSIENT);
|
|
rc = sqlite3_step(stmt);
|
|
sqlite3_finalize(stmt);
|
|
if (rc != SQLITE_DONE) {
|
|
LOG_ERROR("Failed to update password.");
|
|
return {};
|
|
}
|
|
return {device_id, "", true}; // password updated
|
|
} else {
|
|
return {device_id, "", false}; // same password
|
|
}
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
}
|
|
|
|
// Device not exists or device_id is empty — generate new
|
|
const int MAX_RETRIES = 10;
|
|
for (int i = 0; i < MAX_RETRIES; ++i) {
|
|
std::string new_id;
|
|
if (device_id == "web") {
|
|
std::string generated_id = GenerateDeviceId();
|
|
if (generated_id.empty()) {
|
|
LOG_ERROR("Failed to generate device ID for web client.");
|
|
return {};
|
|
}
|
|
new_id = device_id + "-" + generated_id;
|
|
} else {
|
|
new_id = GenerateDeviceId();
|
|
if (new_id.empty()) {
|
|
LOG_ERROR("Failed to generate device ID.");
|
|
return {};
|
|
}
|
|
}
|
|
|
|
// Check if the generated ID (including web- prefix) already exists
|
|
if (DeviceIdExists(new_id)) {
|
|
LOG_WARN("Generated ID {} already exists, retrying...", new_id);
|
|
continue;
|
|
}
|
|
|
|
std::string new_pwd = GeneratePassword();
|
|
if (new_pwd.empty()) {
|
|
LOG_ERROR("Failed to generate password.");
|
|
return {};
|
|
}
|
|
|
|
std::string salt = GenerateSalt();
|
|
std::string hash = HashPasswordWithSalt(salt, new_pwd);
|
|
|
|
// Use transaction to reduce race condition
|
|
sqlite3_exec(db_, "BEGIN TRANSACTION;", nullptr, nullptr, nullptr);
|
|
|
|
// Double-check ID uniqueness within transaction
|
|
if (DeviceIdExists(new_id)) {
|
|
sqlite3_exec(db_, "ROLLBACK;", nullptr, nullptr, nullptr);
|
|
LOG_WARN("Generated ID {} already exists, retrying...", new_id);
|
|
continue;
|
|
}
|
|
|
|
const char* insert_sql =
|
|
"INSERT INTO devices (device_id, password_hash, password_salt) VALUES "
|
|
"(?, ?, ?);";
|
|
|
|
sqlite3_stmt* stmt = nullptr;
|
|
if (sqlite3_prepare_v2(db_, insert_sql, -1, &stmt, nullptr) != SQLITE_OK) {
|
|
sqlite3_exec(db_, "ROLLBACK;", nullptr, nullptr, nullptr);
|
|
LOG_ERROR("Failed to prepare insert statement.");
|
|
return {};
|
|
}
|
|
|
|
sqlite3_bind_text(stmt, 1, new_id.c_str(), -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 2, hash.c_str(), -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 3, salt.c_str(), -1, SQLITE_TRANSIENT);
|
|
int rc = sqlite3_step(stmt);
|
|
sqlite3_finalize(stmt);
|
|
|
|
if (rc == SQLITE_DONE) {
|
|
sqlite3_exec(db_, "COMMIT;", nullptr, nullptr, nullptr);
|
|
// For web clients, return empty password
|
|
if (device_id == "web") {
|
|
return {new_id, "", false};
|
|
}
|
|
return {new_id, new_pwd, false};
|
|
} else if (rc == SQLITE_CONSTRAINT) {
|
|
sqlite3_exec(db_, "ROLLBACK;", nullptr, nullptr, nullptr);
|
|
LOG_WARN(
|
|
"Insert failed due to constraint (ID may have been inserted "
|
|
"concurrently): {}",
|
|
new_id);
|
|
continue;
|
|
} else {
|
|
sqlite3_exec(db_, "ROLLBACK;", nullptr, nullptr, nullptr);
|
|
LOG_ERROR("Insert device failed: {}", sqlite3_errmsg(db_));
|
|
return {};
|
|
}
|
|
}
|
|
|
|
LOG_ERROR("Failed to generate unique device_id after {} attempts.",
|
|
MAX_RETRIES);
|
|
return {};
|
|
}
|
|
|
|
int DeviceDBManager::VerifyDevice(const std::string& device_id,
|
|
const std::string& password) {
|
|
std::lock_guard<std::recursive_mutex> lock(db_mutex_);
|
|
if (db_ == nullptr) {
|
|
LOG_ERROR("Database is not initialized in VerifyDevice.");
|
|
return -1;
|
|
}
|
|
|
|
const char* sql =
|
|
"SELECT password_salt, password_hash FROM devices WHERE device_id = ?;";
|
|
|
|
sqlite3_stmt* stmt = nullptr;
|
|
if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
|
|
return -1;
|
|
}
|
|
|
|
sqlite3_bind_text(stmt, 1, device_id.c_str(), -1, SQLITE_TRANSIENT);
|
|
|
|
// Check if device exists
|
|
int result = -2;
|
|
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
std::string salt = ColumnText(stmt, 0);
|
|
std::string stored_hash = ColumnText(stmt, 1);
|
|
|
|
std::string hash = HashPasswordWithSalt(salt, password);
|
|
if (hash == stored_hash) {
|
|
// Password is correct
|
|
result = 0;
|
|
} else {
|
|
// Password is incorrect
|
|
result = -1;
|
|
}
|
|
}
|
|
|
|
sqlite3_finalize(stmt);
|
|
return result;
|
|
}
|
|
|
|
bool DeviceDBManager::UpdatePassword(const std::string& device_id,
|
|
const std::string& new_password) {
|
|
std::lock_guard<std::recursive_mutex> lock(db_mutex_);
|
|
if (db_ == nullptr) {
|
|
LOG_ERROR("Database is not initialized in UpdatePassword.");
|
|
return false;
|
|
}
|
|
|
|
std::string salt = GenerateSalt();
|
|
std::string hash = HashPasswordWithSalt(salt, new_password);
|
|
|
|
const char* sql =
|
|
"UPDATE devices SET password_salt = ?, password_hash = ? WHERE device_id "
|
|
"= ?;";
|
|
|
|
sqlite3_stmt* stmt = nullptr;
|
|
if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
|
|
return false;
|
|
}
|
|
|
|
sqlite3_bind_text(stmt, 1, salt.c_str(), -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 2, hash.c_str(), -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 3, device_id.c_str(), -1, SQLITE_TRANSIENT);
|
|
|
|
bool success = (sqlite3_step(stmt) == SQLITE_DONE);
|
|
sqlite3_finalize(stmt);
|
|
return success;
|
|
}
|
|
|
|
bool DeviceDBManager::RemoveDevice(const std::string& device_id) {
|
|
std::lock_guard<std::recursive_mutex> lock(db_mutex_);
|
|
if (db_ == nullptr) {
|
|
LOG_ERROR("Database is not initialized in RemoveDevice.");
|
|
return false;
|
|
}
|
|
|
|
const char* sql = "DELETE FROM devices WHERE device_id = ?;";
|
|
|
|
sqlite3_stmt* stmt = nullptr;
|
|
if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
|
|
return false;
|
|
}
|
|
|
|
sqlite3_bind_text(stmt, 1, device_id.c_str(), -1, SQLITE_TRANSIENT);
|
|
bool success = (sqlite3_step(stmt) == SQLITE_DONE);
|
|
sqlite3_finalize(stmt);
|
|
return success;
|
|
}
|
|
|
|
bool DeviceDBManager::SetDeviceOnline(const std::string& device_id,
|
|
bool online) {
|
|
std::lock_guard<std::recursive_mutex> lock(db_mutex_);
|
|
if (db_ == nullptr) {
|
|
LOG_ERROR("Database is not initialized in SetDeviceOnline.");
|
|
return false;
|
|
}
|
|
|
|
const char* sql =
|
|
"INSERT INTO device_presence (device_id, online, updated_at) "
|
|
"VALUES (?, ?, strftime('%s','now')) "
|
|
"ON CONFLICT(device_id) DO UPDATE SET online=excluded.online, "
|
|
"updated_at=excluded.updated_at;";
|
|
|
|
sqlite3_stmt* stmt = nullptr;
|
|
if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
|
|
return false;
|
|
}
|
|
sqlite3_bind_text(stmt, 1, device_id.c_str(), -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_int(stmt, 2, online ? 1 : 0);
|
|
bool ok = (sqlite3_step(stmt) == SQLITE_DONE);
|
|
sqlite3_finalize(stmt);
|
|
return ok;
|
|
}
|
|
|
|
int DeviceDBManager::GetOnlineDeviceCount() {
|
|
return CountOnlineDevices();
|
|
}
|
|
|
|
int DeviceDBManager::CountOnlineDevices(const std::string& search) {
|
|
std::lock_guard<std::recursive_mutex> lock(db_mutex_);
|
|
if (db_ == nullptr) {
|
|
LOG_ERROR("Database is not initialized in CountOnlineDevices.");
|
|
return 0;
|
|
}
|
|
|
|
std::string sql =
|
|
"SELECT COUNT(*) FROM device_presence "
|
|
"WHERE online = 1 "
|
|
"AND device_id NOT LIKE 'web-%' "
|
|
"AND device_id NOT LIKE 'C-%' ";
|
|
if (!search.empty()) {
|
|
sql += "AND device_id LIKE ? ESCAPE '\\' ";
|
|
}
|
|
sql += ";";
|
|
|
|
sqlite3_stmt* stmt = nullptr;
|
|
if (sqlite3_prepare_v2(db_, sql.c_str(), -1, &stmt, nullptr) !=
|
|
SQLITE_OK) {
|
|
return 0;
|
|
}
|
|
if (!search.empty()) {
|
|
std::string pattern = "%" + EscapeLikePattern(search) + "%";
|
|
sqlite3_bind_text(stmt, 1, pattern.c_str(), -1, SQLITE_TRANSIENT);
|
|
}
|
|
|
|
int count = 0;
|
|
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
count = sqlite3_column_int(stmt, 0);
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
return count;
|
|
}
|
|
|
|
std::vector<OnlineDeviceInfo> DeviceDBManager::ListOnlineDevices() {
|
|
return ListOnlineDevices(static_cast<size_t>(std::numeric_limits<int>::max()),
|
|
0, "");
|
|
}
|
|
|
|
std::vector<OnlineDeviceInfo> DeviceDBManager::ListOnlineDevices(
|
|
size_t limit, size_t offset, const std::string& search) {
|
|
std::lock_guard<std::recursive_mutex> lock(db_mutex_);
|
|
std::vector<OnlineDeviceInfo> result;
|
|
if (db_ == nullptr) {
|
|
LOG_ERROR("Database is not initialized in ListOnlineDevices.");
|
|
return result;
|
|
}
|
|
|
|
std::string sql =
|
|
"SELECT device_id, online, updated_at FROM device_presence "
|
|
"WHERE online = 1 "
|
|
"AND device_id NOT LIKE 'web-%' "
|
|
"AND device_id NOT LIKE 'C-%' ";
|
|
if (!search.empty()) {
|
|
sql += "AND device_id LIKE ? ESCAPE '\\' ";
|
|
}
|
|
sql += "ORDER BY updated_at DESC, device_id ASC LIMIT ? OFFSET ?;";
|
|
|
|
sqlite3_stmt* stmt = nullptr;
|
|
if (sqlite3_prepare_v2(db_, sql.c_str(), -1, &stmt, nullptr) !=
|
|
SQLITE_OK) {
|
|
return result;
|
|
}
|
|
int bind_index = 1;
|
|
if (!search.empty()) {
|
|
std::string pattern = "%" + EscapeLikePattern(search) + "%";
|
|
sqlite3_bind_text(stmt, bind_index++, pattern.c_str(), -1,
|
|
SQLITE_TRANSIENT);
|
|
}
|
|
sqlite3_bind_int64(stmt, bind_index++, static_cast<sqlite3_int64>(limit));
|
|
sqlite3_bind_int64(stmt, bind_index++, static_cast<sqlite3_int64>(offset));
|
|
|
|
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
OnlineDeviceInfo info;
|
|
info.device_id = ColumnText(stmt, 0);
|
|
info.online = sqlite3_column_int(stmt, 1) != 0;
|
|
info.updated_at = sqlite3_column_int64(stmt, 2);
|
|
result.push_back(info);
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
|
|
return result;
|
|
}
|
|
|
|
std::vector<std::pair<std::string, bool>> DeviceDBManager::BatchQueryOnline(
|
|
const std::vector<std::string>& device_ids) {
|
|
std::lock_guard<std::recursive_mutex> lock(db_mutex_);
|
|
std::vector<std::pair<std::string, bool>> result;
|
|
if (db_ == nullptr) {
|
|
LOG_ERROR("Database is not initialized in BatchQueryOnline.");
|
|
return result;
|
|
}
|
|
if (device_ids.empty()) {
|
|
return result;
|
|
}
|
|
|
|
std::stringstream ss;
|
|
ss << "SELECT device_id, online FROM device_presence WHERE device_id IN (";
|
|
for (size_t i = 0; i < device_ids.size(); ++i) {
|
|
ss << (i == 0 ? "?" : ",?");
|
|
}
|
|
ss << ");";
|
|
std::string sql = ss.str();
|
|
|
|
sqlite3_stmt* stmt = nullptr;
|
|
if (sqlite3_prepare_v2(db_, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) {
|
|
return result;
|
|
}
|
|
for (size_t i = 0; i < device_ids.size(); ++i) {
|
|
sqlite3_bind_text(stmt, static_cast<int>(i + 1), device_ids[i].c_str(), -1,
|
|
SQLITE_TRANSIENT);
|
|
}
|
|
|
|
std::unordered_map<std::string, bool> map;
|
|
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
std::string id = ColumnText(stmt, 0);
|
|
int online = sqlite3_column_int(stmt, 1);
|
|
map[id] = (online != 0);
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
|
|
for (const auto& id : device_ids) {
|
|
auto it = map.find(id);
|
|
bool online = (it != map.end()) ? it->second : false;
|
|
result.emplace_back(id, online);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
bool DeviceDBManager::SetUserDevices(
|
|
const std::string& user_id, const std::vector<std::string>& device_ids) {
|
|
std::lock_guard<std::recursive_mutex> lock(db_mutex_);
|
|
if (db_ == nullptr) {
|
|
LOG_ERROR("Database is not initialized in SetUserDevices.");
|
|
return false;
|
|
}
|
|
|
|
char* err_msg = nullptr;
|
|
if (sqlite3_exec(db_, "BEGIN TRANSACTION;", nullptr, nullptr, &err_msg) !=
|
|
SQLITE_OK) {
|
|
LOG_ERROR("Failed to begin SetUserDevices transaction: {}",
|
|
err_msg ? err_msg : sqlite3_errmsg(db_));
|
|
sqlite3_free(err_msg);
|
|
return false;
|
|
}
|
|
|
|
const char* delete_sql = "DELETE FROM user_devices WHERE user_id = ?;";
|
|
sqlite3_stmt* stmt = nullptr;
|
|
if (sqlite3_prepare_v2(db_, delete_sql, -1, &stmt, nullptr) != SQLITE_OK) {
|
|
sqlite3_exec(db_, "ROLLBACK;", nullptr, nullptr, nullptr);
|
|
return false;
|
|
}
|
|
sqlite3_bind_text(stmt, 1, user_id.c_str(), -1, SQLITE_TRANSIENT);
|
|
bool ok = (sqlite3_step(stmt) == SQLITE_DONE);
|
|
sqlite3_finalize(stmt);
|
|
|
|
if (!ok) {
|
|
sqlite3_exec(db_, "ROLLBACK;", nullptr, nullptr, nullptr);
|
|
return false;
|
|
}
|
|
|
|
const char* insert_sql =
|
|
"INSERT OR IGNORE INTO user_devices (user_id, device_id) VALUES (?, ?);";
|
|
if (sqlite3_prepare_v2(db_, insert_sql, -1, &stmt, nullptr) != SQLITE_OK) {
|
|
sqlite3_exec(db_, "ROLLBACK;", nullptr, nullptr, nullptr);
|
|
return false;
|
|
}
|
|
|
|
for (const auto& device_id : device_ids) {
|
|
if (device_id.empty()) {
|
|
continue;
|
|
}
|
|
sqlite3_reset(stmt);
|
|
sqlite3_clear_bindings(stmt);
|
|
sqlite3_bind_text(stmt, 1, user_id.c_str(), -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 2, device_id.c_str(), -1, SQLITE_TRANSIENT);
|
|
if (sqlite3_step(stmt) != SQLITE_DONE) {
|
|
ok = false;
|
|
break;
|
|
}
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
|
|
if (!ok) {
|
|
sqlite3_exec(db_, "ROLLBACK;", nullptr, nullptr, nullptr);
|
|
return false;
|
|
}
|
|
|
|
if (sqlite3_exec(db_, "COMMIT;", nullptr, nullptr, &err_msg) != SQLITE_OK) {
|
|
LOG_ERROR("Failed to commit SetUserDevices transaction: {}",
|
|
err_msg ? err_msg : sqlite3_errmsg(db_));
|
|
sqlite3_free(err_msg);
|
|
sqlite3_exec(db_, "ROLLBACK;", nullptr, nullptr, nullptr);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
std::vector<std::string> DeviceDBManager::GetUserDevices(
|
|
const std::string& user_id) {
|
|
std::lock_guard<std::recursive_mutex> lock(db_mutex_);
|
|
std::vector<std::string> result;
|
|
if (db_ == nullptr) {
|
|
LOG_ERROR("Database is not initialized in GetUserDevices.");
|
|
return result;
|
|
}
|
|
|
|
const char* sql =
|
|
"SELECT device_id FROM user_devices WHERE user_id = ? ORDER BY "
|
|
"device_id;";
|
|
sqlite3_stmt* stmt = nullptr;
|
|
if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
|
|
return result;
|
|
}
|
|
|
|
sqlite3_bind_text(stmt, 1, user_id.c_str(), -1, SQLITE_TRANSIENT);
|
|
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
result.push_back(ColumnText(stmt, 0));
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
|
|
return result;
|
|
}
|