mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-05-15 03:45:04 +00:00
Add dashboard audit log and configuration management
- Implemented dashboard audit logging with InsertAuditLog, GetRecentAuditLogs, GetAuditLogsByIP, and CleanupOldAuditLogs methods. - Created dashboard configuration management with GetDashboardConfig and SetDashboardConfig methods. - Added new tables for dashboard_audit_log and dashboard_config in both MySQL and SQLite migrations. - Updated CMakeLists to include Crow and ASIO for dashboard server functionality. - Enhanced existing database classes to support new dashboard features, including character, play key, and property management. - Added new methods for retrieving and managing play keys, properties, and pet names. - Updated TestSQLDatabase to include stubs for new dashboard-related methods. - Modified shared and dashboard configuration files for new settings.
This commit is contained in:
@@ -47,6 +47,9 @@ public:
|
||||
void AddFriend(const LWOOBJID playerAccountId, const LWOOBJID friendAccountId) override;
|
||||
void RemoveFriend(const LWOOBJID playerAccountId, const LWOOBJID friendAccountId) override;
|
||||
void UpdateActivityLog(const LWOOBJID characterId, const eActivityType activityType, const LWOMAPID mapId) override;
|
||||
std::vector<IActivityLog::Entry> GetRecentActivity(const uint32_t limit) override;
|
||||
uint32_t GetActivityLogCount() override;
|
||||
std::vector<IActivityLog::Entry> GetActivityLogPaginated(uint32_t offset, uint32_t limit, const std::string& orderColumn, const std::string& orderDir) override;
|
||||
void DeleteUgcModelData(const LWOOBJID& modelId) override;
|
||||
void UpdateUgcModelData(const LWOOBJID& modelId, std::stringstream& lxfml) override;
|
||||
std::vector<IUgc::Model> GetAllUgcModels() override;
|
||||
@@ -58,6 +61,14 @@ public:
|
||||
std::string GetCharacterXml(const LWOOBJID accountId) override;
|
||||
void UpdateCharacterXml(const LWOOBJID characterId, const std::string_view lxfml) override;
|
||||
std::optional<IAccounts::Info> GetAccountInfo(const std::string_view username) override;
|
||||
|
||||
// Account dashboard details
|
||||
std::optional<IAccounts::DetailedInfo> GetAccountById(const uint32_t accountId) override;
|
||||
void UpdateAccountEmail(const uint32_t accountId, const std::string_view email) override;
|
||||
void DeleteAccount(const uint32_t accountId) override;
|
||||
std::vector<IAccounts::ListInfo> GetAllAccounts() override;
|
||||
void UpdateAccountLock(const uint32_t accountId, const bool locked) override;
|
||||
std::vector<IAccounts::SessionInfo> GetAccountSessions(const uint32_t accountId, uint32_t limit = 50) override;
|
||||
void InsertNewCharacter(const ICharInfo::Info info) override;
|
||||
void InsertCharacterXml(const LWOOBJID accountId, const std::string_view lxfml) override;
|
||||
std::vector<LWOOBJID> GetAccountCharacterIds(LWOOBJID accountId) override;
|
||||
@@ -67,6 +78,12 @@ public:
|
||||
void UpdateLastLoggedInCharacter(const LWOOBJID characterId) override;
|
||||
void SetPetNameModerationStatus(const LWOOBJID& petId, const IPetNames::Info& info) override;
|
||||
std::optional<IPetNames::Info> GetPetNameInfo(const LWOOBJID& petId) override;
|
||||
|
||||
// Pet name moderation
|
||||
std::vector<IPetNames::DetailedInfo> GetAllPetNames() override;
|
||||
std::vector<IPetNames::DetailedInfo> GetPetNamesByStatus(int32_t status) override;
|
||||
void SetPetApprovalStatus(const LWOOBJID& petId, int32_t status) override;
|
||||
uint32_t GetPendingPetNamesCount() override;
|
||||
std::optional<IProperty::Info> GetPropertyInfo(const LWOMAPID mapId, const LWOCLONEID cloneId) override;
|
||||
void UpdatePropertyModerationInfo(const IProperty::Info& info) override;
|
||||
void UpdatePropertyDetails(const IProperty::Info& info) override;
|
||||
@@ -79,6 +96,15 @@ public:
|
||||
void RemoveModel(const LWOOBJID& modelId) override;
|
||||
void UpdatePerformanceCost(const LWOZONEID& zoneId, const float performanceCost) override;
|
||||
void InsertNewBugReport(const IBugReports::Info& info) override;
|
||||
|
||||
// Bug reports (dashboard)
|
||||
std::vector<IBugReports::DetailedInfo> GetAllBugReports() override;
|
||||
std::vector<IBugReports::DetailedInfo> GetUnresolvedBugReports() override;
|
||||
std::vector<IBugReports::DetailedInfo> GetResolvedBugReports() override;
|
||||
std::optional<IBugReports::DetailedInfo> GetBugReportById(const uint64_t reportId) override;
|
||||
void ResolveBugReport(const uint64_t reportId, const uint32_t resolvedById, const std::string_view resolution) override;
|
||||
uint32_t GetBugReportCount() override;
|
||||
uint32_t GetUnresolvedBugReportCount() override;
|
||||
void InsertCheatDetection(const IPlayerCheatDetections::Info& info) override;
|
||||
void InsertNewMail(const MailInfo& mail) override;
|
||||
void InsertNewUgcModel(
|
||||
@@ -93,6 +119,7 @@ public:
|
||||
void DeleteMail(const uint64_t mailId) override;
|
||||
void ClaimMailItem(const uint64_t mailId) override;
|
||||
void InsertSlashCommandUsage(const LWOOBJID characterId, const std::string_view command) override;
|
||||
std::vector<ICommandLog::Entry> GetCommandLogs(uint32_t limit = 100) override;
|
||||
void UpdateAccountUnmuteTime(const uint32_t accountId, const uint64_t timeToUnmute) override;
|
||||
void UpdateAccountBan(const uint32_t accountId, const bool banned) override;
|
||||
void UpdateAccountPassword(const uint32_t accountId, const std::string_view bcryptpassword) override;
|
||||
@@ -103,6 +130,16 @@ public:
|
||||
void InsertDefaultPersistentId() override;
|
||||
std::optional<uint32_t> GetDonationTotal(const uint32_t activityId) override;
|
||||
std::optional<bool> IsPlaykeyActive(const int32_t playkeyId) override;
|
||||
|
||||
// Play keys management
|
||||
std::vector<IPlayKeys::Info> GetAllPlayKeys() override;
|
||||
std::optional<IPlayKeys::Info> GetPlayKeyById(const uint32_t playkeyId) override;
|
||||
std::optional<IPlayKeys::Info> GetPlayKeyByString(const std::string_view key_string) override;
|
||||
bool ConsumePlayKeyUsage(const uint32_t playkeyId) override;
|
||||
void CreatePlayKey(const std::string_view key_string, uint32_t uses, const std::string_view notes) override;
|
||||
void UpdatePlayKey(const uint32_t playkeyId, uint32_t uses, bool active, const std::string_view notes) override;
|
||||
void DeletePlayKey(const uint32_t playkeyId) override;
|
||||
uint32_t GetPlayKeyCount() override;
|
||||
std::vector<IUgc::Model> GetUgcModels(const LWOOBJID& propertyId) override;
|
||||
void AddIgnore(const LWOOBJID playerId, const LWOOBJID ignoredPlayerId) override;
|
||||
void RemoveIgnore(const LWOOBJID playerId, const LWOOBJID ignoredPlayerId) override;
|
||||
@@ -113,6 +150,7 @@ public:
|
||||
std::string GetBehavior(const LWOOBJID behaviorId) override;
|
||||
void RemoveBehavior(const LWOOBJID characterId) override;
|
||||
void UpdateAccountGmLevel(const uint32_t accountId, const eGameMasterLevel gmLevel) override;
|
||||
void UpdateAccountPlayKey(const uint32_t accountId, const uint32_t playKeyId) override;
|
||||
std::optional<IProperty::PropertyEntranceResult> GetProperties(const IProperty::PropertyLookup& params) override;
|
||||
std::vector<ILeaderboard::Entry> GetDescendingLeaderboard(const uint32_t activityId) override;
|
||||
std::vector<ILeaderboard::Entry> GetAscendingLeaderboard(const uint32_t activityId) override;
|
||||
@@ -126,10 +164,46 @@ public:
|
||||
void InsertUgcBuild(const std::string& modules, const LWOOBJID bigId, const std::optional<LWOOBJID> characterId) override;
|
||||
void DeleteUgcBuild(const LWOOBJID bigId) override;
|
||||
uint32_t GetAccountCount() override;
|
||||
uint32_t GetBannedAccountCount() override;
|
||||
uint32_t GetLockedAccountCount() override;
|
||||
bool IsNameInUse(const std::string_view name) override;
|
||||
uint32_t GetCharacterCount() override;
|
||||
std::vector<ICharInfo::Info> GetAllCharactersPaginated(uint32_t offset, uint32_t limit, const std::string& orderColumn, const std::string& orderDir) override;
|
||||
std::vector<ICharInfo::Info> GetCharactersWithPendingNames() override;
|
||||
|
||||
// Character management additions
|
||||
void UpdateCharacterPermissions(const LWOOBJID characterId, ePermissionMap permissions) override;
|
||||
void SetCharacterNeedsRename(const LWOOBJID characterId, bool needsRename) override;
|
||||
std::optional<ICharInfo::Stats> GetCharacterStats(const LWOOBJID characterId) override;
|
||||
std::vector<ICharInfo::InventoryItem> GetCharacterInventory(const LWOOBJID characterId) override;
|
||||
std::vector<ICharInfo::Activity> GetCharacterActivity(const LWOOBJID characterId, uint32_t limit = 50) override;
|
||||
void RescueCharacter(const LWOOBJID characterId, uint32_t zoneId) override;
|
||||
std::optional<IPropertyContents::Model> GetModel(const LWOOBJID modelID) override;
|
||||
std::optional<IUgc::Model> GetUgcModel(const LWOOBJID ugcId) override;
|
||||
std::optional<IProperty::Info> GetPropertyInfo(const LWOOBJID id) override;
|
||||
|
||||
// Property listing/approval (dashboard)
|
||||
std::vector<IProperty::Info> GetAllProperties() override;
|
||||
std::vector<IProperty::Info> GetPropertiesByApprovalStatus(uint32_t approved) override;
|
||||
uint32_t GetPropertyCount() override;
|
||||
uint32_t GetUnapprovedPropertyCount() override;
|
||||
|
||||
// Dashboard Audit Log
|
||||
void InsertAuditLog(const std::string_view ip_address, const std::string_view endpoint,
|
||||
const std::string_view method, const std::string_view user_agent, int32_t response_code) override;
|
||||
std::vector<IDashboardAuditLog::AuditLogEntry> GetRecentAuditLogs(uint32_t limit) override;
|
||||
std::vector<IDashboardAuditLog::AuditLogEntry> GetAuditLogsByIP(const std::string_view ip_address, uint32_t limit) override;
|
||||
void CleanupOldAuditLogs(uint32_t days_to_keep) override;
|
||||
void InsertAdminActionLog(uint32_t adminAccountId, const std::string_view action,
|
||||
const std::string_view targetType, uint64_t targetId,
|
||||
const std::string_view details) override;
|
||||
std::vector<IDashboardAuditLog::AdminActionLog> GetAuditLogs(uint32_t limit = 100) override;
|
||||
|
||||
// Dashboard Config
|
||||
std::optional<std::string> GetDashboardConfig(const std::string_view config_key) override;
|
||||
void SetDashboardConfig(const std::string_view config_key, const std::string_view config_value) override;
|
||||
|
||||
|
||||
sql::PreparedStatement* CreatePreppedStmt(const std::string& query);
|
||||
private:
|
||||
|
||||
|
||||
@@ -41,7 +41,85 @@ void MySQLDatabase::UpdateAccountGmLevel(const uint32_t accountId, const eGameMa
|
||||
ExecuteUpdate("UPDATE accounts SET gm_level = ? WHERE id = ?;", static_cast<int32_t>(gmLevel), accountId);
|
||||
}
|
||||
|
||||
void MySQLDatabase::UpdateAccountPlayKey(const uint32_t accountId, const uint32_t playKeyId) {
|
||||
ExecuteUpdate("UPDATE accounts SET play_key_id = ? WHERE id = ?;", playKeyId, accountId);
|
||||
}
|
||||
|
||||
uint32_t MySQLDatabase::GetAccountCount() {
|
||||
auto res = ExecuteSelect("SELECT COUNT(*) as count FROM accounts;");
|
||||
return res->next() ? res->getUInt("count") : 0;
|
||||
}
|
||||
|
||||
uint32_t MySQLDatabase::GetBannedAccountCount() {
|
||||
auto res = ExecuteSelect("SELECT COUNT(*) as count FROM accounts WHERE banned = 1;");
|
||||
return res->next() ? res->getUInt("count") : 0;
|
||||
}
|
||||
|
||||
uint32_t MySQLDatabase::GetLockedAccountCount() {
|
||||
auto res = ExecuteSelect("SELECT COUNT(*) as count FROM accounts WHERE locked = 1;");
|
||||
return res->next() ? res->getUInt("count") : 0;
|
||||
}
|
||||
|
||||
std::vector<IAccounts::ListInfo> MySQLDatabase::GetAllAccounts() {
|
||||
std::vector<IAccounts::ListInfo> out;
|
||||
auto res = ExecuteSelect("SELECT id, name, gm_level, banned, locked, mute_expire, play_key_id FROM accounts ORDER BY id ASC;");
|
||||
|
||||
while (res->next()) {
|
||||
IAccounts::ListInfo info;
|
||||
info.id = res->getUInt("id");
|
||||
info.name = res->getString("name").c_str();
|
||||
info.gm_level = static_cast<eGameMasterLevel>(res->getInt("gm_level"));
|
||||
info.banned = res->getBoolean("banned");
|
||||
info.locked = res->getBoolean("locked");
|
||||
info.mute_expire = res->getUInt64("mute_expire");
|
||||
info.play_key_id = res->getUInt("play_key_id");
|
||||
out.push_back(info);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
void MySQLDatabase::UpdateAccountLock(const uint32_t accountId, const bool locked) {
|
||||
ExecuteUpdate("UPDATE accounts SET locked = ? WHERE id = ?;", locked, accountId);
|
||||
}
|
||||
|
||||
std::optional<IAccounts::DetailedInfo> MySQLDatabase::GetAccountById(const uint32_t accountId) {
|
||||
auto result = ExecuteSelect(
|
||||
"SELECT id, name, email, gm_level, banned, locked, mute_expire, play_key_id, created_at FROM accounts WHERE id = ? LIMIT 1;",
|
||||
accountId
|
||||
);
|
||||
|
||||
if (!result->next()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
IAccounts::DetailedInfo info;
|
||||
info.id = result->getUInt("id");
|
||||
info.name = result->getString("name").c_str();
|
||||
info.email = result->getString("email").c_str();
|
||||
info.gm_level = static_cast<eGameMasterLevel>(result->getInt("gm_level"));
|
||||
info.banned = result->getBoolean("banned");
|
||||
info.locked = result->getBoolean("locked");
|
||||
info.mute_expire = result->getUInt64("mute_expire");
|
||||
info.play_key_id = result->getUInt("play_key_id");
|
||||
info.created_at = result->getUInt64("created_at");
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
void MySQLDatabase::UpdateAccountEmail(const uint32_t accountId, const std::string_view email) {
|
||||
ExecuteUpdate("UPDATE accounts SET email = ? WHERE id = ?;", email, accountId);
|
||||
}
|
||||
|
||||
void MySQLDatabase::DeleteAccount(const uint32_t accountId) {
|
||||
// Delete all associated data first
|
||||
// Characters and their data will be handled by CASCADE or manual deletion
|
||||
ExecuteDelete("DELETE FROM char_info WHERE account_id = ?;", accountId);
|
||||
ExecuteDelete("DELETE FROM accounts WHERE id = ?;", accountId);
|
||||
}
|
||||
|
||||
std::vector<IAccounts::SessionInfo> MySQLDatabase::GetAccountSessions(const uint32_t accountId, uint32_t limit) {
|
||||
// account_sessions table doesn't exist in the current schema
|
||||
// Session tracking would need to be implemented separately
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -4,3 +4,60 @@ void MySQLDatabase::UpdateActivityLog(const LWOOBJID characterId, const eActivit
|
||||
ExecuteInsert("INSERT INTO activity_log (character_id, activity, time, map_id) VALUES (?, ?, ?, ?);",
|
||||
characterId, static_cast<uint32_t>(activityType), static_cast<uint32_t>(time(NULL)), mapId);
|
||||
}
|
||||
|
||||
std::vector<IActivityLog::Entry> MySQLDatabase::GetRecentActivity(const uint32_t limit) {
|
||||
std::vector<IActivityLog::Entry> out;
|
||||
|
||||
auto res = ExecuteSelect("SELECT character_id, activity, time, map_id FROM activity_log ORDER BY time DESC LIMIT ?;", limit);
|
||||
|
||||
while (res->next()) {
|
||||
IActivityLog::Entry e;
|
||||
e.characterId = static_cast<LWOOBJID>(res->getUInt64("character_id"));
|
||||
e.activity = static_cast<eActivityType>(res->getInt("activity"));
|
||||
e.timestamp = static_cast<uint32_t>(res->getUInt("time"));
|
||||
e.mapId = static_cast<LWOMAPID>(res->getUInt("map_id"));
|
||||
out.push_back(e);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
uint32_t MySQLDatabase::GetActivityLogCount() {
|
||||
auto res = ExecuteSelect("SELECT COUNT(*) as count FROM activity_log;");
|
||||
return res->next() ? res->getUInt("count") : 0;
|
||||
}
|
||||
|
||||
std::vector<IActivityLog::Entry> MySQLDatabase::GetActivityLogPaginated(
|
||||
uint32_t offset,
|
||||
uint32_t limit,
|
||||
const std::string& orderColumn,
|
||||
const std::string& orderDir
|
||||
) {
|
||||
std::vector<IActivityLog::Entry> out;
|
||||
|
||||
// Validate orderColumn to prevent SQL injection
|
||||
std::string validColumn = "time";
|
||||
if (orderColumn == "character_id" || orderColumn == "activity" || orderColumn == "map_id" || orderColumn == "time") {
|
||||
validColumn = orderColumn;
|
||||
}
|
||||
|
||||
// Validate orderDir
|
||||
std::string validDir = (orderDir == "ASC" || orderDir == "asc") ? "ASC" : "DESC";
|
||||
|
||||
// Build query - can't use prepared statement for ORDER BY clause
|
||||
std::string query = "SELECT character_id, activity, time, map_id FROM activity_log ORDER BY " +
|
||||
validColumn + " " + validDir + " LIMIT ? OFFSET ?;";
|
||||
|
||||
auto res = ExecuteSelect(query, limit, offset);
|
||||
|
||||
while (res->next()) {
|
||||
IActivityLog::Entry e;
|
||||
e.characterId = static_cast<LWOOBJID>(res->getUInt64("character_id"));
|
||||
e.activity = static_cast<eActivityType>(res->getInt("activity"));
|
||||
e.timestamp = static_cast<uint32_t>(res->getUInt("time"));
|
||||
e.mapId = static_cast<LWOMAPID>(res->getUInt("map_id"));
|
||||
out.push_back(e);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -4,3 +4,125 @@ void MySQLDatabase::InsertNewBugReport(const IBugReports::Info& info) {
|
||||
ExecuteInsert("INSERT INTO `bug_reports`(body, client_version, other_player_id, selection, reporter_id) VALUES (?, ?, ?, ?, ?)",
|
||||
info.body, info.clientVersion, info.otherPlayer, info.selection, info.characterId);
|
||||
}
|
||||
|
||||
std::vector<IBugReports::DetailedInfo> MySQLDatabase::GetAllBugReports() {
|
||||
std::vector<IBugReports::DetailedInfo> out;
|
||||
auto res = ExecuteSelect(
|
||||
"SELECT id, body, client_version, other_player_id, selection, reporter_id, "
|
||||
"UNIX_TIMESTAMP(submitted) as submitted, UNIX_TIMESTAMP(resolved_time) as resolved_time, "
|
||||
"resolved_by_id, resolution FROM bug_reports ORDER BY id DESC;"
|
||||
);
|
||||
|
||||
while (res->next()) {
|
||||
IBugReports::DetailedInfo info;
|
||||
info.id = res->getUInt64("id");
|
||||
info.body = res->getString("body").c_str();
|
||||
info.clientVersion = res->getString("client_version").c_str();
|
||||
info.otherPlayer = res->getString("other_player_id").c_str();
|
||||
info.selection = res->getString("selection").c_str();
|
||||
info.characterId = res->getUInt64("reporter_id");
|
||||
info.submitted = res->getUInt64("submitted");
|
||||
info.resolved_time = res->getUInt64("resolved_time");
|
||||
info.resolved_by_id = res->getUInt("resolved_by_id");
|
||||
info.resolution = res->getString("resolution").c_str();
|
||||
out.push_back(info);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<IBugReports::DetailedInfo> MySQLDatabase::GetUnresolvedBugReports() {
|
||||
std::vector<IBugReports::DetailedInfo> out;
|
||||
auto res = ExecuteSelect(
|
||||
"SELECT id, body, client_version, other_player_id, selection, reporter_id, "
|
||||
"UNIX_TIMESTAMP(submitted) as submitted, UNIX_TIMESTAMP(resolved_time) as resolved_time, "
|
||||
"resolved_by_id, resolution FROM bug_reports WHERE resolved_time IS NULL ORDER BY id DESC;"
|
||||
);
|
||||
|
||||
while (res->next()) {
|
||||
IBugReports::DetailedInfo info;
|
||||
info.id = res->getUInt64("id");
|
||||
info.body = res->getString("body").c_str();
|
||||
info.clientVersion = res->getString("client_version").c_str();
|
||||
info.otherPlayer = res->getString("other_player_id").c_str();
|
||||
info.selection = res->getString("selection").c_str();
|
||||
info.characterId = res->getUInt64("reporter_id");
|
||||
info.submitted = res->getUInt64("submitted");
|
||||
info.resolved_time = 0;
|
||||
info.resolved_by_id = 0;
|
||||
info.resolution = "";
|
||||
out.push_back(info);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<IBugReports::DetailedInfo> MySQLDatabase::GetResolvedBugReports() {
|
||||
std::vector<IBugReports::DetailedInfo> out;
|
||||
auto res = ExecuteSelect(
|
||||
"SELECT id, body, client_version, other_player_id, selection, reporter_id, "
|
||||
"UNIX_TIMESTAMP(submitted) as submitted, UNIX_TIMESTAMP(resolved_time) as resolved_time, "
|
||||
"resolved_by_id, resolution FROM bug_reports WHERE resolved_time IS NOT NULL ORDER BY id DESC;"
|
||||
);
|
||||
|
||||
while (res->next()) {
|
||||
IBugReports::DetailedInfo info;
|
||||
info.id = res->getUInt64("id");
|
||||
info.body = res->getString("body").c_str();
|
||||
info.clientVersion = res->getString("client_version").c_str();
|
||||
info.otherPlayer = res->getString("other_player_id").c_str();
|
||||
info.selection = res->getString("selection").c_str();
|
||||
info.characterId = res->getUInt64("reporter_id");
|
||||
info.submitted = res->getUInt64("submitted");
|
||||
info.resolved_time = res->getUInt64("resolved_time");
|
||||
info.resolved_by_id = res->getUInt("resolved_by_id");
|
||||
info.resolution = res->getString("resolution").c_str();
|
||||
out.push_back(info);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
std::optional<IBugReports::DetailedInfo> MySQLDatabase::GetBugReportById(const uint64_t reportId) {
|
||||
auto result = ExecuteSelect(
|
||||
"SELECT id, body, client_version, other_player_id, selection, reporter_id, "
|
||||
"UNIX_TIMESTAMP(submitted) as submitted, UNIX_TIMESTAMP(resolved_time) as resolved_time, "
|
||||
"resolved_by_id, resolution FROM bug_reports WHERE id = ? LIMIT 1;",
|
||||
reportId
|
||||
);
|
||||
|
||||
if (!result->next()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
IBugReports::DetailedInfo info;
|
||||
info.id = result->getUInt64("id");
|
||||
info.body = result->getString("body").c_str();
|
||||
info.clientVersion = result->getString("client_version").c_str();
|
||||
info.otherPlayer = result->getString("other_player_id").c_str();
|
||||
info.selection = result->getString("selection").c_str();
|
||||
info.characterId = result->getUInt64("reporter_id");
|
||||
info.submitted = result->getUInt64("submitted");
|
||||
info.resolved_time = result->isNull("resolved_time") ? 0 : result->getUInt64("resolved_time");
|
||||
info.resolved_by_id = result->isNull("resolved_by_id") ? 0 : result->getUInt("resolved_by_id");
|
||||
info.resolution = result->getString("resolution").c_str();
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
void MySQLDatabase::ResolveBugReport(const uint64_t reportId, const uint32_t resolvedById, const std::string_view resolution) {
|
||||
ExecuteUpdate(
|
||||
"UPDATE bug_reports SET resolved_time = NOW(), resolved_by_id = ?, resolution = ? WHERE id = ?;",
|
||||
resolvedById, resolution, reportId
|
||||
);
|
||||
}
|
||||
|
||||
uint32_t MySQLDatabase::GetBugReportCount() {
|
||||
auto res = ExecuteSelect("SELECT COUNT(*) as count FROM bug_reports;");
|
||||
return res->next() ? res->getUInt("count") : 0;
|
||||
}
|
||||
|
||||
uint32_t MySQLDatabase::GetUnresolvedBugReportCount() {
|
||||
auto res = ExecuteSelect("SELECT COUNT(*) as count FROM bug_reports WHERE resolved_time IS NULL;");
|
||||
return res->next() ? res->getUInt("count") : 0;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ set(DDATABASES_DATABASES_MYSQL_TABLES_SOURCES
|
||||
"Servers.cpp"
|
||||
"Ugc.cpp"
|
||||
"UgcModularBuild.cpp"
|
||||
"DashboardAuditLog.cpp"
|
||||
"DashboardConfig.cpp"
|
||||
PARENT_SCOPE
|
||||
)
|
||||
|
||||
|
||||
@@ -82,3 +82,112 @@ bool MySQLDatabase::IsNameInUse(const std::string_view name) {
|
||||
|
||||
return result->next();
|
||||
}
|
||||
|
||||
uint32_t MySQLDatabase::GetCharacterCount() {
|
||||
auto res = ExecuteSelect("SELECT COUNT(*) as count FROM charinfo;");
|
||||
return res->next() ? res->getUInt("count") : 0;
|
||||
}
|
||||
|
||||
std::vector<ICharInfo::Info> MySQLDatabase::GetAllCharactersPaginated(
|
||||
uint32_t offset,
|
||||
uint32_t limit,
|
||||
const std::string& orderColumn,
|
||||
const std::string& orderDir
|
||||
) {
|
||||
std::vector<ICharInfo::Info> out;
|
||||
|
||||
// Validate orderColumn to prevent SQL injection
|
||||
std::string validColumn = "id";
|
||||
if (orderColumn == "name" || orderColumn == "account_id" || orderColumn == "id" || orderColumn == "last_login") {
|
||||
validColumn = orderColumn;
|
||||
}
|
||||
|
||||
// Validate orderDir
|
||||
std::string validDir = (orderDir == "ASC" || orderDir == "asc") ? "ASC" : "DESC";
|
||||
|
||||
// Build query
|
||||
std::string query = "SELECT name, pending_name, needs_rename, prop_clone_id, permission_map, id, account_id FROM charinfo ORDER BY " +
|
||||
validColumn + " " + validDir + " LIMIT ? OFFSET ?;";
|
||||
|
||||
auto res = ExecuteSelect(query, limit, offset);
|
||||
|
||||
while (res->next()) {
|
||||
ICharInfo::Info info;
|
||||
info.id = res->getInt64("id");
|
||||
info.name = res->getString("name").c_str();
|
||||
info.pendingName = res->getString("pending_name").c_str();
|
||||
info.needsRename = res->getBoolean("needs_rename");
|
||||
info.cloneId = res->getUInt64("prop_clone_id");
|
||||
info.accountId = res->getUInt("account_id");
|
||||
info.permissionMap = static_cast<ePermissionMap>(res->getUInt("permission_map"));
|
||||
out.push_back(info);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<ICharInfo::Info> MySQLDatabase::GetCharactersWithPendingNames() {
|
||||
std::vector<ICharInfo::Info> out;
|
||||
|
||||
auto res = ExecuteSelect("SELECT name, pending_name, needs_rename, prop_clone_id, permission_map, id, account_id FROM charinfo WHERE pending_name != '' ORDER BY id ASC;");
|
||||
|
||||
while (res->next()) {
|
||||
ICharInfo::Info info;
|
||||
info.id = res->getInt64("id");
|
||||
info.name = res->getString("name").c_str();
|
||||
info.pendingName = res->getString("pending_name").c_str();
|
||||
info.needsRename = res->getBoolean("needs_rename");
|
||||
info.cloneId = res->getUInt64("prop_clone_id");
|
||||
info.accountId = res->getUInt("account_id");
|
||||
info.permissionMap = static_cast<ePermissionMap>(res->getUInt("permission_map"));
|
||||
out.push_back(info);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
void MySQLDatabase::UpdateCharacterPermissions(const LWOOBJID characterId, ePermissionMap permissions) {
|
||||
ExecuteUpdate("UPDATE charinfo SET permission_map = ? WHERE id = ? LIMIT 1;", static_cast<uint64_t>(permissions), characterId);
|
||||
}
|
||||
|
||||
void MySQLDatabase::SetCharacterNeedsRename(const LWOOBJID characterId, bool needsRename) {
|
||||
ExecuteUpdate("UPDATE charinfo SET needs_rename = ? WHERE id = ? LIMIT 1;", needsRename, characterId);
|
||||
}
|
||||
|
||||
std::optional<ICharInfo::Stats> MySQLDatabase::GetCharacterStats(const LWOOBJID characterId) {
|
||||
// char_stats table doesn't exist in the current schema
|
||||
// Stats would need to be parsed from charxml or a new table created
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::vector<ICharInfo::InventoryItem> MySQLDatabase::GetCharacterInventory(const LWOOBJID characterId) {
|
||||
// Inventory data is stored in charxml, not a separate table
|
||||
// Would need to parse the XML to extract inventory items
|
||||
// Returning empty for now - implement XML parsing if needed
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<ICharInfo::Activity> MySQLDatabase::GetCharacterActivity(const LWOOBJID characterId, uint32_t limit) {
|
||||
auto res = ExecuteSelect(
|
||||
"SELECT time, activity, map_id FROM activity_log WHERE character_id = ? ORDER BY time DESC LIMIT ?;",
|
||||
characterId, limit
|
||||
);
|
||||
|
||||
std::vector<ICharInfo::Activity> activities;
|
||||
while (res->next()) {
|
||||
ICharInfo::Activity activity;
|
||||
activity.timestamp = res->getUInt64("time");
|
||||
activity.activity = static_cast<eActivityType>(res->getUInt("activity"));
|
||||
activity.mapId = res->getUInt("map_id");
|
||||
activities.push_back(activity);
|
||||
}
|
||||
|
||||
return activities;
|
||||
}
|
||||
|
||||
void MySQLDatabase::RescueCharacter(const LWOOBJID characterId, uint32_t zoneId) {
|
||||
// The rescue is now handled by the chat server API which kicks the player
|
||||
// and modifies the XML after the player data is saved
|
||||
// This database method is intentionally a no-op as the actual work
|
||||
// is done via DashboardHelpers::RescueCharacter()
|
||||
}
|
||||
|
||||
@@ -3,3 +3,19 @@
|
||||
void MySQLDatabase::InsertSlashCommandUsage(const LWOOBJID characterId, const std::string_view command) {
|
||||
ExecuteInsert("INSERT INTO command_log (character_id, command) VALUES (?, ?);", characterId, command);
|
||||
}
|
||||
|
||||
std::vector<ICommandLog::Entry> MySQLDatabase::GetCommandLogs(uint32_t limit) {
|
||||
std::vector<ICommandLog::Entry> logs;
|
||||
auto res = ExecuteSelect("SELECT id, character_id, command FROM command_log ORDER BY id DESC LIMIT ?;", limit);
|
||||
|
||||
while (res->next()) {
|
||||
ICommandLog::Entry entry;
|
||||
entry.timestamp = 0; // Timestamp column doesn't exist in command_log table
|
||||
entry.characterId = res->getInt64("character_id");
|
||||
entry.command = res->getString("command").c_str();
|
||||
entry.arguments = ""; // Arguments not currently stored in DB
|
||||
logs.push_back(entry);
|
||||
}
|
||||
|
||||
return logs;
|
||||
}
|
||||
|
||||
66
dDatabase/GameDatabase/MySQL/Tables/DashboardAuditLog.cpp
Normal file
66
dDatabase/GameDatabase/MySQL/Tables/DashboardAuditLog.cpp
Normal file
@@ -0,0 +1,66 @@
|
||||
#include "MySQLDatabase.h"
|
||||
#include <ctime>
|
||||
|
||||
void MySQLDatabase::InsertAuditLog(const std::string_view ip, const std::string_view endpoint,
|
||||
const std::string_view method, const std::string_view user_agent, int32_t response_code) {
|
||||
uint64_t now = static_cast<uint64_t>(std::time(nullptr));
|
||||
ExecuteInsert("INSERT INTO dashboard_audit_log (timestamp, ip_address, endpoint, method, user_agent, response_code) VALUES (?, ?, ?, ?, ?, ?);",
|
||||
now, ip, endpoint, method, user_agent, response_code);
|
||||
}
|
||||
|
||||
std::vector<IDashboardAuditLog::AuditLogEntry> MySQLDatabase::GetRecentAuditLogs(uint32_t limit) {
|
||||
std::vector<IDashboardAuditLog::AuditLogEntry> logs;
|
||||
auto res = ExecuteSelect("SELECT id, timestamp, ip_address, endpoint, method, user_agent, response_code FROM dashboard_audit_log ORDER BY timestamp DESC LIMIT ?;", limit);
|
||||
|
||||
while (res->next()) {
|
||||
IDashboardAuditLog::AuditLogEntry entry;
|
||||
entry.id = res->getUInt64("id");
|
||||
entry.timestamp = res->getUInt64("timestamp");
|
||||
entry.ip_address = res->getString("ip_address").c_str();
|
||||
entry.endpoint = res->getString("endpoint").c_str();
|
||||
entry.method = res->getString("method").c_str();
|
||||
entry.user_agent = res->getString("user_agent").c_str();
|
||||
entry.response_code = res->getInt("response_code");
|
||||
logs.push_back(entry);
|
||||
}
|
||||
|
||||
return logs;
|
||||
}
|
||||
|
||||
std::vector<IDashboardAuditLog::AuditLogEntry> MySQLDatabase::GetAuditLogsByIP(const std::string_view ip, uint32_t limit) {
|
||||
std::vector<IDashboardAuditLog::AuditLogEntry> logs;
|
||||
auto res = ExecuteSelect("SELECT id, timestamp, ip_address, endpoint, method, user_agent, response_code FROM dashboard_audit_log WHERE ip_address = ? ORDER BY timestamp DESC LIMIT ?;", ip, limit);
|
||||
|
||||
while (res->next()) {
|
||||
IDashboardAuditLog::AuditLogEntry entry;
|
||||
entry.id = res->getUInt64("id");
|
||||
entry.timestamp = res->getUInt64("timestamp");
|
||||
entry.ip_address = res->getString("ip_address").c_str();
|
||||
entry.endpoint = res->getString("endpoint").c_str();
|
||||
entry.method = res->getString("method").c_str();
|
||||
entry.user_agent = res->getString("user_agent").c_str();
|
||||
entry.response_code = res->getInt("response_code");
|
||||
logs.push_back(entry);
|
||||
}
|
||||
|
||||
return logs;
|
||||
}
|
||||
|
||||
void MySQLDatabase::CleanupOldAuditLogs(uint32_t days_to_keep) {
|
||||
uint64_t cutoff_time = static_cast<uint64_t>(std::time(nullptr)) - (static_cast<uint64_t>(days_to_keep) * 86400ULL);
|
||||
ExecuteDelete("DELETE FROM dashboard_audit_log WHERE timestamp < ?;", cutoff_time);
|
||||
}
|
||||
|
||||
void MySQLDatabase::InsertAdminActionLog(uint32_t adminAccountId, const std::string_view action,
|
||||
const std::string_view targetType, uint64_t targetId,
|
||||
const std::string_view details) {
|
||||
// dashboard_admin_action_log table doesn't exist
|
||||
// Admin actions could be logged to dashboard_audit_log or a new table needs to be created
|
||||
// For now, this is a no-op
|
||||
}
|
||||
|
||||
std::vector<IDashboardAuditLog::AdminActionLog> MySQLDatabase::GetAuditLogs(uint32_t limit) {
|
||||
// dashboard_admin_action_log table doesn't exist
|
||||
// Would need to create a new table or use dashboard_audit_log with additional columns
|
||||
return {};
|
||||
}
|
||||
14
dDatabase/GameDatabase/MySQL/Tables/DashboardConfig.cpp
Normal file
14
dDatabase/GameDatabase/MySQL/Tables/DashboardConfig.cpp
Normal file
@@ -0,0 +1,14 @@
|
||||
#include "MySQLDatabase.h"
|
||||
|
||||
std::optional<std::string> MySQLDatabase::GetDashboardConfig(const std::string_view key) {
|
||||
auto res = ExecuteSelect("SELECT config_value FROM dashboard_config WHERE config_key = ? LIMIT 1;", key);
|
||||
if (res->next()) {
|
||||
return res->getString("config_value").c_str();
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void MySQLDatabase::SetDashboardConfig(const std::string_view key, const std::string_view value) {
|
||||
// Use INSERT ... ON DUPLICATE KEY UPDATE for MySQL
|
||||
ExecuteInsert("INSERT INTO dashboard_config (config_key, config_value, updated_at) VALUES (?, ?, UNIX_TIMESTAMP()) ON DUPLICATE KEY UPDATE config_value = VALUES(config_value), updated_at = VALUES(updated_at);", key, value);
|
||||
}
|
||||
@@ -24,3 +24,44 @@ std::optional<IPetNames::Info> MySQLDatabase::GetPetNameInfo(const LWOOBJID& pet
|
||||
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
std::vector<IPetNames::DetailedInfo> MySQLDatabase::GetAllPetNames() {
|
||||
std::vector<IPetNames::DetailedInfo> out;
|
||||
auto res = ExecuteSelect("SELECT id, pet_name, approved, owner_id FROM pet_names ORDER BY id DESC;");
|
||||
|
||||
while (res->next()) {
|
||||
IPetNames::DetailedInfo info;
|
||||
info.id = res->getUInt64("id");
|
||||
info.petName = res->getString("pet_name").c_str();
|
||||
info.approvalStatus = res->getInt("approved");
|
||||
info.ownerId = res->getUInt64("owner_id");
|
||||
out.push_back(info);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<IPetNames::DetailedInfo> MySQLDatabase::GetPetNamesByStatus(int32_t status) {
|
||||
std::vector<IPetNames::DetailedInfo> out;
|
||||
auto res = ExecuteSelect("SELECT id, pet_name, approved, owner_id FROM pet_names WHERE approved = ? ORDER BY id DESC;", status);
|
||||
|
||||
while (res->next()) {
|
||||
IPetNames::DetailedInfo info;
|
||||
info.id = res->getUInt64("id");
|
||||
info.petName = res->getString("pet_name").c_str();
|
||||
info.approvalStatus = res->getInt("approved");
|
||||
info.ownerId = res->getUInt64("owner_id");
|
||||
out.push_back(info);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
void MySQLDatabase::SetPetApprovalStatus(const LWOOBJID& petId, int32_t status) {
|
||||
ExecuteUpdate("UPDATE pet_names SET approved = ? WHERE id = ?;", status, petId);
|
||||
}
|
||||
|
||||
uint32_t MySQLDatabase::GetPendingPetNamesCount() {
|
||||
auto res = ExecuteSelect("SELECT COUNT(*) as count FROM pet_names WHERE approved = 1;");
|
||||
return res->next() ? res->getUInt("count") : 0;
|
||||
}
|
||||
|
||||
@@ -9,3 +9,117 @@ std::optional<bool> MySQLDatabase::IsPlaykeyActive(const int32_t playkeyId) {
|
||||
|
||||
return keyCheckRes->getBoolean("active");
|
||||
}
|
||||
|
||||
std::vector<IPlayKeys::Info> MySQLDatabase::GetAllPlayKeys() {
|
||||
std::vector<IPlayKeys::Info> out;
|
||||
auto res = ExecuteSelect(
|
||||
"SELECT id, key_string, key_uses, times_used, active, notes, UNIX_TIMESTAMP(created_at) as created_at "
|
||||
"FROM play_keys ORDER BY id DESC;"
|
||||
);
|
||||
|
||||
while (res->next()) {
|
||||
IPlayKeys::Info info;
|
||||
info.id = res->getUInt("id");
|
||||
info.key_string = res->getString("key_string").c_str();
|
||||
info.key_uses = res->getUInt("key_uses");
|
||||
info.times_used = res->getUInt("times_used");
|
||||
info.active = res->getBoolean("active");
|
||||
info.notes = res->getString("notes").c_str();
|
||||
info.created_at = res->getUInt64("created_at");
|
||||
out.push_back(info);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
std::optional<IPlayKeys::Info> MySQLDatabase::GetPlayKeyById(const uint32_t playkeyId) {
|
||||
auto result = ExecuteSelect(
|
||||
"SELECT id, key_string, key_uses, times_used, active, notes, UNIX_TIMESTAMP(created_at) as created_at "
|
||||
"FROM play_keys WHERE id = ? LIMIT 1;",
|
||||
playkeyId
|
||||
);
|
||||
|
||||
if (!result->next()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
IPlayKeys::Info info;
|
||||
info.id = result->getUInt("id");
|
||||
info.key_string = result->getString("key_string").c_str();
|
||||
info.key_uses = result->getUInt("key_uses");
|
||||
info.times_used = result->getUInt("times_used");
|
||||
info.active = result->getBoolean("active");
|
||||
info.notes = result->getString("notes").c_str();
|
||||
info.created_at = result->getUInt64("created_at");
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
std::optional<IPlayKeys::Info> MySQLDatabase::GetPlayKeyByString(const std::string_view key_string) {
|
||||
auto result = ExecuteSelect(
|
||||
"SELECT id, key_string, key_uses, times_used, active, notes, UNIX_TIMESTAMP(created_at) as created_at "
|
||||
"FROM play_keys WHERE key_string = ? LIMIT 1;",
|
||||
key_string
|
||||
);
|
||||
|
||||
if (!result->next()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
IPlayKeys::Info info;
|
||||
info.id = result->getUInt("id");
|
||||
info.key_string = result->getString("key_string").c_str();
|
||||
info.key_uses = result->getUInt("key_uses");
|
||||
info.times_used = result->getUInt("times_used");
|
||||
info.active = result->getBoolean("active");
|
||||
info.notes = result->getString("notes").c_str();
|
||||
info.created_at = result->getUInt64("created_at");
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
bool MySQLDatabase::ConsumePlayKeyUsage(const uint32_t playkeyId) {
|
||||
// Check current state
|
||||
auto res = ExecuteSelect("SELECT key_uses, times_used, active FROM play_keys WHERE id = ? LIMIT 1;", playkeyId);
|
||||
if (!res->next()) return false;
|
||||
|
||||
int key_uses = res->getUInt("key_uses");
|
||||
int times_used = res->getUInt("times_used");
|
||||
bool active = res->getBoolean("active");
|
||||
|
||||
if (!active) return false;
|
||||
if (times_used >= key_uses) return false;
|
||||
|
||||
// Increment times_used
|
||||
ExecuteUpdate("UPDATE play_keys SET times_used = times_used + 1 WHERE id = ?;", playkeyId);
|
||||
|
||||
// Deactivate if used up
|
||||
if (times_used + 1 >= key_uses) {
|
||||
ExecuteUpdate("UPDATE play_keys SET active = 0 WHERE id = ?;", playkeyId);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void MySQLDatabase::CreatePlayKey(const std::string_view key_string, uint32_t uses, const std::string_view notes) {
|
||||
ExecuteInsert(
|
||||
"INSERT INTO play_keys (key_string, key_uses, times_used, active, notes) VALUES (?, ?, 0, 1, ?);",
|
||||
key_string, uses, notes
|
||||
);
|
||||
}
|
||||
|
||||
void MySQLDatabase::UpdatePlayKey(const uint32_t playkeyId, uint32_t uses, bool active, const std::string_view notes) {
|
||||
ExecuteUpdate(
|
||||
"UPDATE play_keys SET key_uses = ?, active = ?, notes = ? WHERE id = ?;",
|
||||
uses, active, notes, playkeyId
|
||||
);
|
||||
}
|
||||
|
||||
void MySQLDatabase::DeletePlayKey(const uint32_t playkeyId) {
|
||||
ExecuteDelete("DELETE FROM play_keys WHERE id = ?;", playkeyId);
|
||||
}
|
||||
|
||||
uint32_t MySQLDatabase::GetPlayKeyCount() {
|
||||
auto res = ExecuteSelect("SELECT COUNT(*) as count FROM play_keys;");
|
||||
return res->next() ? res->getUInt("count") : 0;
|
||||
}
|
||||
|
||||
@@ -198,3 +198,44 @@ std::optional<IProperty::Info> MySQLDatabase::GetPropertyInfo(const LWOOBJID id)
|
||||
|
||||
return ReadPropertyInfo(propertyEntry);
|
||||
}
|
||||
|
||||
std::vector<IProperty::Info> MySQLDatabase::GetAllProperties() {
|
||||
std::vector<IProperty::Info> out;
|
||||
auto res = ExecuteSelect(
|
||||
"SELECT id, owner_id, clone_id, name, description, privacy_option, rejection_reason, "
|
||||
"last_updated, time_claimed, reputation, mod_approved, performance_cost "
|
||||
"FROM properties ORDER BY id DESC;"
|
||||
);
|
||||
|
||||
while (res->next()) {
|
||||
out.push_back(ReadPropertyInfo(res));
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<IProperty::Info> MySQLDatabase::GetPropertiesByApprovalStatus(uint32_t approved) {
|
||||
std::vector<IProperty::Info> out;
|
||||
auto res = ExecuteSelect(
|
||||
"SELECT id, owner_id, clone_id, name, description, privacy_option, rejection_reason, "
|
||||
"last_updated, time_claimed, reputation, mod_approved, performance_cost "
|
||||
"FROM properties WHERE mod_approved = ? ORDER BY id DESC;",
|
||||
approved
|
||||
);
|
||||
|
||||
while (res->next()) {
|
||||
out.push_back(ReadPropertyInfo(res));
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
uint32_t MySQLDatabase::GetPropertyCount() {
|
||||
auto res = ExecuteSelect("SELECT COUNT(*) as count FROM properties;");
|
||||
return res->next() ? res->getUInt("count") : 0;
|
||||
}
|
||||
|
||||
uint32_t MySQLDatabase::GetUnapprovedPropertyCount() {
|
||||
auto res = ExecuteSelect("SELECT COUNT(*) as count FROM properties WHERE mod_approved = 0;");
|
||||
return res->next() ? res->getUInt("count") : 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user