#include "MySQLDatabase.h" #include "GeneralUtils.h" #include "eGameMasterLevel.h" #include "json.hpp" std::optional MySQLDatabase::GetAccountInfo(const std::string_view username) { auto result = ExecuteSelect("SELECT id, password, banned, locked, play_key_id, gm_level, mute_expire, ban_expires, ban_reason FROM accounts WHERE name = ? LIMIT 1;", username); if (!result->next()) { return std::nullopt; } IAccounts::Info toReturn; toReturn.id = result->getUInt("id"); toReturn.maxGmLevel = static_cast(result->getInt("gm_level")); toReturn.bcryptPassword = result->getString("password").c_str(); toReturn.banned = result->getBoolean("banned"); toReturn.locked = result->getBoolean("locked"); toReturn.playKeyId = result->getUInt("play_key_id"); toReturn.muteExpire = result->getUInt64("mute_expire"); toReturn.banExpires = result->getInt64("ban_expires"); toReturn.banReason = result->isNull("ban_reason") ? "" : result->getString("ban_reason").c_str(); return toReturn; } void MySQLDatabase::UpdateAccountUnmuteTime(const uint32_t accountId, const uint64_t timeToUnmute) { ExecuteUpdate("UPDATE accounts SET mute_expire = ? WHERE id = ?;", timeToUnmute, accountId); } void MySQLDatabase::UpdateAccountBan(const uint32_t accountId, const bool banned) { ExecuteUpdate("UPDATE accounts SET banned = ? WHERE id = ?;", banned, accountId); } void MySQLDatabase::UpdateAccountPassword(const uint32_t accountId, const std::string_view bcryptpassword) { ExecuteUpdate("UPDATE accounts SET password = ? WHERE id = ?;", bcryptpassword, accountId); } void MySQLDatabase::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(gmLevel)); } void MySQLDatabase::UpdateAccountGmLevel(const uint32_t accountId, const eGameMasterLevel gmLevel) { ExecuteUpdate("UPDATE accounts SET gm_level = ? WHERE id = ?;", static_cast(gmLevel), accountId); } uint32_t MySQLDatabase::GetAccountCount() { auto res = ExecuteSelect("SELECT COUNT(*) as count FROM accounts;"); return res->next() ? res->getUInt("count") : 0; } void MySQLDatabase::RecordFailedAttempt(const uint32_t accountId) { ExecuteUpdate("UPDATE accounts SET failed_attempts = failed_attempts + 1 WHERE id = ?;", accountId); } void MySQLDatabase::ClearFailedAttempts(const uint32_t accountId) { ExecuteUpdate("UPDATE accounts SET failed_attempts = 0, lockout_time = NULL, last_login = NOW() WHERE id = ?;", accountId); } void MySQLDatabase::SetLockout(const uint32_t accountId, const int64_t lockoutUntil) { ExecuteUpdate("UPDATE accounts SET lockout_time = FROM_UNIXTIME(NULLIF(?, 0)), failed_attempts = 0 WHERE id = ?;", lockoutUntil, accountId); } bool MySQLDatabase::IsLockedOut(const uint32_t accountId) { auto result = ExecuteSelect("SELECT 1 AS locked FROM accounts WHERE id = ? AND lockout_time IS NOT NULL AND lockout_time > NOW();", accountId); return result->next(); } uint32_t MySQLDatabase::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(gmLevel), excludeAccountId); return result->next() ? result->getUInt("count") : 0; } void MySQLDatabase::SetAccountLocked(const uint32_t accountId, const bool locked) { ExecuteUpdate("UPDATE accounts SET locked = ? WHERE id = ?;", locked, accountId); } uint8_t MySQLDatabase::GetFailedAttempts(const uint32_t accountId) { auto result = ExecuteSelect("SELECT failed_attempts FROM accounts WHERE id = ?;", accountId); if (!result->next()) { return 0; } return result->getUInt("failed_attempts"); } nlohmann::json MySQLDatabase::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(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 CONCAT('%', ?, '%') OR id = ? OR id IN (SELECT account_id FROM charinfo WHERE name LIKE CONCAT('%', ?, '%')))"; } // 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 ?, ?;"; // Get total count std::string totalCountQuery = "SELECT COUNT(*) as count FROM accounts;"; auto totalCountResult = ExecuteSelect(totalCountQuery); uint32_t totalRecords = totalCountResult->next() ? totalCountResult->getUInt("count") : 0; // Get filtered count uint32_t filteredRecords = totalRecords; if (!search.empty()) { std::string filteredCountQuery = "SELECT COUNT(*) as count FROM accounts WHERE (name LIKE CONCAT('%', ?, '%') OR id = ? OR id IN (SELECT account_id FROM charinfo WHERE name LIKE CONCAT('%', ?, '%')));"; auto filteredCountResult = ExecuteSelect(filteredCountQuery, search, searchId, search); filteredRecords = filteredCountResult->next() ? filteredCountResult->getUInt("count") : 0; } // Execute main query auto result = !search.empty() ? ExecuteSelect(mainQuery, search, searchId, search, start, length) : ExecuteSelect(mainQuery, start, length); // Build response JSON nlohmann::json accountsArray = nlohmann::json::array(); while (result->next()) { nlohmann::json account = { {"id", result->getUInt("id")}, {"name", result->getString("name")}, {"banned", result->getBoolean("banned")}, {"locked", result->getBoolean("locked")}, {"gm_level", result->getInt("gm_level")}, {"mute_expire", result->getUInt64("mute_expire")}, {"created_at", result->getString("created_at")} }; accountsArray.push_back(account); } nlohmann::json response = { {"draw", 1}, {"recordsTotal", totalRecords}, {"recordsFiltered", filteredRecords}, {"data", accountsArray} }; return response; } nlohmann::json MySQLDatabase::GetAccountById(uint32_t accountId) { try { const std::string query = "SELECT id, name, banned, locked, gm_level, mute_expire, created_at, ban_expires, ban_reason FROM accounts WHERE id = ?;"; auto result = ExecuteSelect(query, accountId); if (!result->next()) { return nlohmann::json{{"error", "Account not found"}}; } nlohmann::json account = { {"id", result->getUInt("id")}, {"name", result->getString("name")}, {"banned", result->getBoolean("banned")}, {"locked", result->getBoolean("locked")}, {"ban_expires", result->getInt64("ban_expires")}, {"ban_reason", result->isNull("ban_reason") ? std::string() : std::string(result->getString("ban_reason").c_str())}, {"gm_level", result->getInt("gm_level")}, {"mute_expire", result->getUInt64("mute_expire")}, {"created_at", result->getString("created_at")} }; return account; } catch (const sql::SQLException& e) { LOG_DEBUG("SQL Error: %s", e.what()); return nlohmann::json{{"error", "Database error"}}; } } void MySQLDatabase::DeleteAccount(const uint32_t accountId) { // All or nothing: a failure part way leaves the account as it was DatabaseTransaction transaction(*this); std::vector characters; { auto result = ExecuteSelect("SELECT id FROM charinfo WHERE account_id = ?;", accountId); while (result->next()) characters.push_back(result->getInt64("id")); } 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", "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 MySQLDatabase::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->next()) { chars.push_back({ {"id", std::to_string(result->getInt64("id"))}, {"name", result->getString("name")}, {"last_login", result->getUInt64("last_login")} }); } return chars; }