mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 19:03:43 +00:00
client_sysinfo (migrations mysql 102, sqlite 85) keeps the system description each account's client sent at login, exactly as sent, plus the physical memory read from it. While nothing but the memory in use changes, the account's newest row gets the new time and one more login; otherwise a new row starts. Log pruning deletes rows not seen for a while (eLog::CLIENT_SYSINFO) and deleting an account deletes its rows. Tests on SQLite alone (dDatabaseSqliteTests) and in the MySQL parity tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
229 lines
9.4 KiB
C++
229 lines
9.4 KiB
C++
#include "SQLiteDatabase.h"
|
|
|
|
#include "GeneralUtils.h"
|
|
#include "eGameMasterLevel.h"
|
|
#include "Database.h"
|
|
#include "json.hpp"
|
|
|
|
std::optional<IAccounts::Info> SQLiteDatabase::GetAccountInfo(const std::string_view username) {
|
|
auto [_, result] = ExecuteSelect("SELECT * FROM accounts WHERE name = ? LIMIT 1", username);
|
|
|
|
if (result.eof()) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
IAccounts::Info toReturn;
|
|
toReturn.id = result.getIntField("id");
|
|
toReturn.maxGmLevel = static_cast<eGameMasterLevel>(result.getIntField("gm_level"));
|
|
toReturn.bcryptPassword = result.getStringField("password");
|
|
toReturn.banned = result.getIntField("banned");
|
|
toReturn.locked = result.getIntField("locked");
|
|
toReturn.playKeyId = result.getIntField("play_key_id");
|
|
toReturn.muteExpire = static_cast<uint64_t>(result.getInt64Field("mute_expire"));
|
|
toReturn.banExpires = result.getInt64Field("ban_expires");
|
|
toReturn.banReason = result.fieldIsNull("ban_reason") ? "" : result.getStringField("ban_reason");
|
|
|
|
return toReturn;
|
|
}
|
|
|
|
void SQLiteDatabase::UpdateAccountUnmuteTime(const uint32_t accountId, const uint64_t timeToUnmute) {
|
|
ExecuteUpdate("UPDATE accounts SET mute_expire = ? WHERE id = ?;", timeToUnmute, accountId);
|
|
}
|
|
|
|
void SQLiteDatabase::UpdateAccountBan(const uint32_t accountId, const bool banned) {
|
|
ExecuteUpdate("UPDATE accounts SET banned = ? WHERE id = ?;", banned, accountId);
|
|
}
|
|
|
|
void SQLiteDatabase::UpdateAccountPassword(const uint32_t accountId, const std::string_view bcryptpassword) {
|
|
ExecuteUpdate("UPDATE accounts SET password = ? WHERE id = ?;", bcryptpassword, accountId);
|
|
}
|
|
|
|
void SQLiteDatabase::InsertNewAccount(const std::string_view username, const std::string_view bcryptpassword, const eGameMasterLevel gmLevel) {
|
|
ExecuteInsert("INSERT INTO accounts (name, password, gm_level) VALUES (?, ?, ?);", username, bcryptpassword, static_cast<int32_t>(gmLevel));
|
|
}
|
|
|
|
void SQLiteDatabase::UpdateAccountGmLevel(const uint32_t accountId, const eGameMasterLevel gmLevel) {
|
|
ExecuteUpdate("UPDATE accounts SET gm_level = ? WHERE id = ?;", static_cast<int32_t>(gmLevel), accountId);
|
|
}
|
|
|
|
uint32_t SQLiteDatabase::GetAccountCount() {
|
|
auto [_, res] = ExecuteSelect("SELECT COUNT(*) as count FROM accounts;");
|
|
if (res.eof()) return 0;
|
|
|
|
return res.getIntField("count");
|
|
}
|
|
void SQLiteDatabase::RecordFailedAttempt(const uint32_t accountId) {
|
|
ExecuteUpdate("UPDATE accounts SET failed_attempts = failed_attempts + 1 WHERE id = ?;", accountId);
|
|
}
|
|
|
|
void SQLiteDatabase::ClearFailedAttempts(const uint32_t accountId) {
|
|
ExecuteUpdate("UPDATE accounts SET failed_attempts = 0, lockout_time = NULL, last_login = CURRENT_TIMESTAMP WHERE id = ?;", accountId);
|
|
}
|
|
|
|
void SQLiteDatabase::SetLockout(const uint32_t accountId, const int64_t lockoutUntil) {
|
|
ExecuteUpdate("UPDATE accounts SET lockout_time = datetime(NULLIF(?, 0), 'unixepoch'), failed_attempts = 0 WHERE id = ?;", lockoutUntil, accountId);
|
|
}
|
|
|
|
bool SQLiteDatabase::IsLockedOut(const uint32_t accountId) {
|
|
// lockout_time is stored as UTC text by datetime(?, 'unixepoch'), the same format datetime('now') gives
|
|
auto [_, result] = ExecuteSelect("SELECT 1 AS locked FROM accounts WHERE id = ? AND lockout_time IS NOT NULL AND lockout_time > datetime('now');", accountId);
|
|
return !result.eof();
|
|
}
|
|
|
|
uint32_t SQLiteDatabase::CountActiveAccountsAtGmLevel(const uint8_t gmLevel, const uint32_t excludeAccountId) {
|
|
auto [_, result] = ExecuteSelect("SELECT COUNT(*) AS count FROM accounts WHERE gm_level = ? AND id != ? AND banned = 0 AND locked = 0;", static_cast<int32_t>(gmLevel), excludeAccountId);
|
|
return result.eof() ? 0 : static_cast<uint32_t>(result.getIntField("count"));
|
|
}
|
|
|
|
void SQLiteDatabase::SetAccountLocked(const uint32_t accountId, const bool locked) {
|
|
ExecuteUpdate("UPDATE accounts SET locked = ? WHERE id = ?;", locked, accountId);
|
|
}
|
|
|
|
uint8_t SQLiteDatabase::GetFailedAttempts(const uint32_t accountId) {
|
|
auto [_, result] = ExecuteSelect("SELECT failed_attempts FROM accounts WHERE id = ?;", accountId);
|
|
if (result.eof()) {
|
|
return 0;
|
|
}
|
|
|
|
return result.getIntField("failed_attempts");
|
|
}
|
|
|
|
nlohmann::json SQLiteDatabase::GetAccountsTable(uint32_t start, uint32_t length, const std::string_view search, uint32_t orderColumn, bool orderAsc) {
|
|
// A number in the search box also matches IDs exactly (-1 never matches)
|
|
const int64_t searchId = GeneralUtils::TryParse<int64_t>(std::string(search)).value_or(-1);
|
|
// Build base query
|
|
std::string baseQuery = "SELECT id, name, banned, locked, gm_level, mute_expire, created_at FROM accounts";
|
|
std::string whereClause;
|
|
std::string orderClause;
|
|
|
|
// Add search filter if provided
|
|
if (!search.empty()) {
|
|
whereClause = " WHERE (name LIKE '%' || ? || '%' OR id = ? OR id IN (SELECT account_id FROM charinfo WHERE name LIKE '%' || ? || '%'))";
|
|
}
|
|
|
|
// Map column indices to database columns
|
|
std::string orderColumnName = "id";
|
|
switch (orderColumn) {
|
|
case 0: orderColumnName = "id"; break;
|
|
case 1: orderColumnName = "name"; break;
|
|
case 2: orderColumnName = "banned"; break;
|
|
case 3: orderColumnName = "locked"; break;
|
|
case 4: orderColumnName = "gm_level"; break;
|
|
case 5: orderColumnName = "mute_expire"; break;
|
|
case 6: orderColumnName = "created_at"; break;
|
|
default: orderColumnName = "id";
|
|
}
|
|
|
|
orderClause = " ORDER BY " + orderColumnName + (orderAsc ? " ASC" : " DESC");
|
|
|
|
// Build the main query
|
|
std::string mainQuery = baseQuery + whereClause + orderClause + " LIMIT ? OFFSET ?;";
|
|
|
|
// Get total count
|
|
std::string totalCountQuery = "SELECT COUNT(*) as count FROM accounts;";
|
|
auto [_, totalCountResult] = ExecuteSelect(totalCountQuery);
|
|
uint32_t totalRecords = totalCountResult.eof() ? 0 : totalCountResult.getIntField("count");
|
|
|
|
// Get filtered count
|
|
uint32_t filteredRecords = totalRecords;
|
|
if (!search.empty()) {
|
|
std::string filteredCountQuery = "SELECT COUNT(*) as count FROM accounts WHERE (name LIKE '%' || ? || '%' OR id = ? OR id IN (SELECT account_id FROM charinfo WHERE name LIKE '%' || ? || '%'));";
|
|
auto [__, filteredCountResult] = ExecuteSelect(filteredCountQuery, search, searchId, search);
|
|
filteredRecords = filteredCountResult.eof() ? 0 : filteredCountResult.getIntField("count");
|
|
}
|
|
|
|
// Execute main query
|
|
auto [stmt, result] = !search.empty() ?
|
|
ExecuteSelect(mainQuery, search, searchId, search, length, start) :
|
|
ExecuteSelect(mainQuery, length, start);
|
|
|
|
// Build response JSON
|
|
nlohmann::json accountsArray = nlohmann::json::array();
|
|
|
|
while (!result.eof()) {
|
|
nlohmann::json account = {
|
|
{"id", result.getIntField("id")},
|
|
{"name", result.getStringField("name")},
|
|
{"banned", result.getIntField("banned") != 0},
|
|
{"locked", result.getIntField("locked") != 0},
|
|
{"gm_level", result.getIntField("gm_level")},
|
|
{"mute_expire", result.getInt64Field("mute_expire")},
|
|
{"created_at", result.getStringField("created_at")}
|
|
};
|
|
accountsArray.push_back(account);
|
|
result.nextRow();
|
|
}
|
|
|
|
nlohmann::json response = {
|
|
{"draw", 1},
|
|
{"recordsTotal", totalRecords},
|
|
{"recordsFiltered", filteredRecords},
|
|
{"data", accountsArray}
|
|
};
|
|
|
|
return response;
|
|
}
|
|
nlohmann::json SQLiteDatabase::GetAccountById(uint32_t accountId) {
|
|
try {
|
|
auto [_, result] = ExecuteSelect("SELECT * FROM accounts WHERE id = ? LIMIT 1;", accountId);
|
|
|
|
if (result.eof()) {
|
|
return nlohmann::json{{"error", "Account not found"}};
|
|
}
|
|
|
|
nlohmann::json account = {
|
|
{"id", result.getIntField("id")},
|
|
{"name", result.getStringField("name")},
|
|
{"banned", result.getIntField("banned") != 0},
|
|
{"locked", result.getIntField("locked") != 0},
|
|
{"ban_expires", result.getInt64Field("ban_expires")},
|
|
{"ban_reason", result.fieldIsNull("ban_reason") ? "" : result.getStringField("ban_reason")},
|
|
{"gm_level", result.getIntField("gm_level")},
|
|
{"mute_expire", result.getInt64Field("mute_expire")},
|
|
{"created_at", result.getStringField("created_at")}
|
|
};
|
|
|
|
return account;
|
|
} catch (const CppSQLite3Exception& e) {
|
|
LOG_DEBUG("SQLite Error: %s", e.errorMessage());
|
|
return nlohmann::json{{"error", "Database error"}};
|
|
}
|
|
}
|
|
|
|
void SQLiteDatabase::DeleteAccount(const uint32_t accountId) {
|
|
// All or nothing: a failure part way leaves the account as it was
|
|
DatabaseTransaction transaction(*this);
|
|
std::vector<LWOOBJID> characters;
|
|
{
|
|
auto [_, result] = ExecuteSelect("SELECT id FROM charinfo WHERE account_id = ?;", accountId);
|
|
while (!result.eof()) {
|
|
characters.push_back(result.getInt64Field("id"));
|
|
result.nextRow();
|
|
}
|
|
}
|
|
for (const auto characterId : characters) {
|
|
DeleteCharacter(characterId);
|
|
ExecuteDelete("DELETE FROM character_snapshots WHERE character_id = ?;", characterId);
|
|
}
|
|
// Rows about the account itself. Audit log, chat log and reports stay as the record of what happened.
|
|
for (const auto* table : { "account_tokens", "account_recovery_codes", "account_notes", "account_strikes", "account_login_addresses", "client_sysinfo",
|
|
"dashboard_preferences", "accounts_rewardcodes", "player_cheat_detections" }) {
|
|
ExecuteDelete(std::string("DELETE FROM ") + table + " WHERE account_id = ?;", accountId);
|
|
}
|
|
ExecuteDelete("DELETE FROM accounts WHERE id = ?;", accountId);
|
|
transaction.Commit();
|
|
}
|
|
|
|
nlohmann::json SQLiteDatabase::GetAccountCharacters(uint32_t accountId) {
|
|
auto [_, result] = ExecuteSelect("SELECT id, name, last_login FROM charinfo WHERE account_id = ? ORDER BY last_login DESC;", accountId);
|
|
nlohmann::json chars = nlohmann::json::array();
|
|
while (!result.eof()) {
|
|
chars.push_back({
|
|
{"id", std::to_string(result.getInt64Field("id"))},
|
|
{"name", result.getStringField("name")},
|
|
{"last_login", result.getInt64Field("last_login")}
|
|
});
|
|
result.nextRow();
|
|
}
|
|
return chars;
|
|
} |