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:
Aaron Kimbrell
2026-04-22 11:01:41 -05:00
parent d532a9b063
commit e3467465b4
92 changed files with 9133 additions and 77 deletions

View File

@@ -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:

View File

@@ -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 {};
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -21,6 +21,8 @@ set(DDATABASES_DATABASES_MYSQL_TABLES_SOURCES
"Servers.cpp"
"Ugc.cpp"
"UgcModularBuild.cpp"
"DashboardAuditLog.cpp"
"DashboardConfig.cpp"
PARENT_SCOPE
)

View File

@@ -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()
}

View File

@@ -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;
}

View 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 {};
}

View 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);
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}