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

@@ -42,9 +42,91 @@ void SQLiteDatabase::UpdateAccountGmLevel(const uint32_t accountId, const eGameM
ExecuteUpdate("UPDATE accounts SET gm_level = ? WHERE id = ?;", static_cast<int32_t>(gmLevel), accountId);
}
void SQLiteDatabase::UpdateAccountPlayKey(const uint32_t accountId, const uint32_t playKeyId) {
ExecuteUpdate("UPDATE accounts SET play_key_id = ? WHERE id = ?;", playKeyId, accountId);
}
uint32_t SQLiteDatabase::GetAccountCount() {
auto [_, res] = ExecuteSelect("SELECT COUNT(*) as count FROM accounts;");
if (res.eof()) return 0;
return res.getIntField("count");
}
uint32_t SQLiteDatabase::GetBannedAccountCount() {
auto [_, res] = ExecuteSelect("SELECT COUNT(*) as count FROM accounts WHERE banned = 1;");
if (res.eof()) return 0;
return res.getIntField("count");
}
uint32_t SQLiteDatabase::GetLockedAccountCount() {
auto [_, res] = ExecuteSelect("SELECT COUNT(*) as count FROM accounts WHERE locked = 1;");
if (res.eof()) return 0;
return res.getIntField("count");
}
std::vector<IAccounts::ListInfo> SQLiteDatabase::GetAllAccounts() {
std::vector<IAccounts::ListInfo> out;
auto [stmt, res] = ExecuteSelect("SELECT id, name, gm_level, banned, locked, mute_expire, play_key_id FROM accounts ORDER BY id ASC;");
while (!res.eof()) {
IAccounts::ListInfo info;
info.id = res.getIntField("id");
info.name = res.getStringField("name");
info.gm_level = static_cast<eGameMasterLevel>(res.getIntField("gm_level"));
info.banned = res.getIntField("banned");
info.locked = res.getIntField("locked");
info.mute_expire = static_cast<uint64_t>(res.getInt64Field("mute_expire"));
info.play_key_id = res.getIntField("play_key_id");
out.push_back(info);
res.nextRow();
}
return out;
}
void SQLiteDatabase::UpdateAccountLock(const uint32_t accountId, const bool locked) {
ExecuteUpdate("UPDATE accounts SET locked = ? WHERE id = ?;", locked, accountId);
}
std::optional<IAccounts::DetailedInfo> SQLiteDatabase::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.eof()) {
return std::nullopt;
}
IAccounts::DetailedInfo info;
info.id = result.getIntField("id");
info.name = result.getStringField("name");
info.email = result.getStringField("email");
info.gm_level = static_cast<eGameMasterLevel>(result.getIntField("gm_level"));
info.banned = result.getIntField("banned") != 0;
info.locked = result.getIntField("locked") != 0;
info.mute_expire = static_cast<uint64_t>(result.getInt64Field("mute_expire"));
info.play_key_id = result.getIntField("play_key_id");
info.created_at = static_cast<uint64_t>(result.getInt64Field("created_at"));
return info;
}
void SQLiteDatabase::UpdateAccountEmail(const uint32_t accountId, const std::string_view email) {
ExecuteUpdate("UPDATE accounts SET email = ? WHERE id = ?;", email, accountId);
}
void SQLiteDatabase::DeleteAccount(const uint32_t accountId) {
// Delete all associated data first
ExecuteDelete("DELETE FROM char_info WHERE account_id = ?;", accountId);
ExecuteDelete("DELETE FROM accounts WHERE id = ?;", accountId);
}
std::vector<IAccounts::SessionInfo> SQLiteDatabase::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,63 @@ void SQLiteDatabase::UpdateActivityLog(const LWOOBJID characterId, const eActivi
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> SQLiteDatabase::GetRecentActivity(const uint32_t limit) {
std::vector<IActivityLog::Entry> out;
auto [stmt, res] = ExecuteSelect("SELECT character_id, activity, time, map_id FROM activity_log ORDER BY time DESC LIMIT ?;", limit);
while (!res.eof()) {
IActivityLog::Entry e;
e.characterId = static_cast<LWOOBJID>(res.getInt64Field("character_id"));
e.activity = static_cast<eActivityType>(res.getIntField("activity"));
e.timestamp = static_cast<uint32_t>(res.getIntField("time"));
e.mapId = static_cast<LWOMAPID>(res.getIntField("map_id"));
out.push_back(e);
res.nextRow();
}
return out;
}
uint32_t SQLiteDatabase::GetActivityLogCount() {
auto [_, res] = ExecuteSelect("SELECT COUNT(*) as count FROM activity_log;");
if (res.eof()) return 0;
return res.getIntField("count");
}
std::vector<IActivityLog::Entry> SQLiteDatabase::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 [stmt, res] = ExecuteSelect(query, limit, offset);
while (!res.eof()) {
IActivityLog::Entry e;
e.characterId = static_cast<LWOOBJID>(res.getInt64Field("character_id"));
e.activity = static_cast<eActivityType>(res.getIntField("activity"));
e.timestamp = static_cast<uint32_t>(res.getIntField("time"));
e.mapId = static_cast<LWOMAPID>(res.getIntField("map_id"));
out.push_back(e);
res.nextRow();
}
return out;
}

View File

@@ -4,3 +4,127 @@ void SQLiteDatabase::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> SQLiteDatabase::GetAllBugReports() {
std::vector<IBugReports::DetailedInfo> out;
auto [stmt, res] = ExecuteSelect(
"SELECT id, body, client_version, other_player_id, selection, reporter_id, "
"strftime('%s', submitted) as submitted, strftime('%s', resolved_time) as resolved_time, "
"resolved_by_id, resolution FROM bug_reports ORDER BY id DESC;"
);
while (!res.eof()) {
IBugReports::DetailedInfo info;
info.id = static_cast<uint64_t>(res.getInt64Field("id"));
info.body = res.getStringField("body");
info.clientVersion = res.getStringField("client_version");
info.otherPlayer = res.getStringField("other_player_id");
info.selection = res.getStringField("selection");
info.characterId = static_cast<LWOOBJID>(res.getInt64Field("reporter_id"));
info.submitted = static_cast<uint64_t>(res.getInt64Field("submitted"));
info.resolved_time = res.fieldIsNull("resolved_time") ? 0 : static_cast<uint64_t>(res.getInt64Field("resolved_time"));
info.resolved_by_id = res.fieldIsNull("resolved_by_id") ? 0 : res.getIntField("resolved_by_id");
info.resolution = res.getStringField("resolution");
out.push_back(info);
res.nextRow();
}
return out;
}
std::vector<IBugReports::DetailedInfo> SQLiteDatabase::GetUnresolvedBugReports() {
std::vector<IBugReports::DetailedInfo> out;
auto [stmt, res] = ExecuteSelect(
"SELECT id, body, client_version, other_player_id, selection, reporter_id, "
"strftime('%s', submitted) as submitted FROM bug_reports WHERE resolved_time IS NULL ORDER BY id DESC;"
);
while (!res.eof()) {
IBugReports::DetailedInfo info;
info.id = static_cast<uint64_t>(res.getInt64Field("id"));
info.body = res.getStringField("body");
info.clientVersion = res.getStringField("client_version");
info.otherPlayer = res.getStringField("other_player_id");
info.selection = res.getStringField("selection");
info.characterId = static_cast<LWOOBJID>(res.getInt64Field("reporter_id"));
info.submitted = static_cast<uint64_t>(res.getInt64Field("submitted"));
info.resolved_time = 0;
info.resolved_by_id = 0;
info.resolution = "";
out.push_back(info);
res.nextRow();
}
return out;
}
std::vector<IBugReports::DetailedInfo> SQLiteDatabase::GetResolvedBugReports() {
std::vector<IBugReports::DetailedInfo> out;
auto [stmt, res] = ExecuteSelect(
"SELECT id, body, client_version, other_player_id, selection, reporter_id, "
"strftime('%s', submitted) as submitted, strftime('%s', resolved_time) as resolved_time, "
"resolved_by_id, resolution FROM bug_reports WHERE resolved_time IS NOT NULL ORDER BY id DESC;"
);
while (!res.eof()) {
IBugReports::DetailedInfo info;
info.id = static_cast<uint64_t>(res.getInt64Field("id"));
info.body = res.getStringField("body");
info.clientVersion = res.getStringField("client_version");
info.otherPlayer = res.getStringField("other_player_id");
info.selection = res.getStringField("selection");
info.characterId = static_cast<LWOOBJID>(res.getInt64Field("reporter_id"));
info.submitted = static_cast<uint64_t>(res.getInt64Field("submitted"));
info.resolved_time = static_cast<uint64_t>(res.getInt64Field("resolved_time"));
info.resolved_by_id = res.getIntField("resolved_by_id");
info.resolution = res.getStringField("resolution");
out.push_back(info);
res.nextRow();
}
return out;
}
std::optional<IBugReports::DetailedInfo> SQLiteDatabase::GetBugReportById(const uint64_t reportId) {
auto [_, result] = ExecuteSelect(
"SELECT id, body, client_version, other_player_id, selection, reporter_id, "
"strftime('%s', submitted) as submitted, strftime('%s', resolved_time) as resolved_time, "
"resolved_by_id, resolution FROM bug_reports WHERE id = ? LIMIT 1;",
reportId
);
if (result.eof()) {
return std::nullopt;
}
IBugReports::DetailedInfo info;
info.id = static_cast<uint64_t>(result.getInt64Field("id"));
info.body = result.getStringField("body");
info.clientVersion = result.getStringField("client_version");
info.otherPlayer = result.getStringField("other_player_id");
info.selection = result.getStringField("selection");
info.characterId = static_cast<LWOOBJID>(result.getInt64Field("reporter_id"));
info.submitted = static_cast<uint64_t>(result.getInt64Field("submitted"));
info.resolved_time = result.fieldIsNull("resolved_time") ? 0 : static_cast<uint64_t>(result.getInt64Field("resolved_time"));
info.resolved_by_id = result.fieldIsNull("resolved_by_id") ? 0 : result.getIntField("resolved_by_id");
info.resolution = result.getStringField("resolution");
return info;
}
void SQLiteDatabase::ResolveBugReport(const uint64_t reportId, const uint32_t resolvedById, const std::string_view resolution) {
ExecuteUpdate(
"UPDATE bug_reports SET resolved_time = datetime('now'), resolved_by_id = ?, resolution = ? WHERE id = ?;",
resolvedById, resolution, reportId
);
}
uint32_t SQLiteDatabase::GetBugReportCount() {
auto [_, res] = ExecuteSelect("SELECT COUNT(*) as count FROM bug_reports;");
return res.eof() ? 0 : res.getIntField("count");
}
uint32_t SQLiteDatabase::GetUnresolvedBugReportCount() {
auto [_, res] = ExecuteSelect("SELECT COUNT(*) as count FROM bug_reports WHERE resolved_time IS NULL;");
return res.eof() ? 0 : res.getIntField("count");
}

View File

@@ -7,6 +7,8 @@ set(DDATABASES_DATABASES_SQLITE_TABLES_SOURCES
"CharInfo.cpp"
"CharXml.cpp"
"CommandLog.cpp"
"DashboardAuditLog.cpp"
"DashboardConfig.cpp"
"Friends.cpp"
"IgnoreList.cpp"
"Leaderboard.cpp"

View File

@@ -83,3 +83,117 @@ bool SQLiteDatabase::IsNameInUse(const std::string_view name) {
return !result.eof();
}
uint32_t SQLiteDatabase::GetCharacterCount() {
auto [_, res] = ExecuteSelect("SELECT COUNT(*) as count FROM charinfo;");
if (res.eof()) return 0;
return res.getIntField("count");
}
std::vector<ICharInfo::Info> SQLiteDatabase::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 [stmt, res] = ExecuteSelect(query, limit, offset);
while (!res.eof()) {
ICharInfo::Info info;
info.id = res.getInt64Field("id");
info.name = res.getStringField("name");
info.pendingName = res.getStringField("pending_name");
info.needsRename = res.getIntField("needs_rename");
info.cloneId = res.getInt64Field("prop_clone_id");
info.accountId = res.getIntField("account_id");
info.permissionMap = static_cast<ePermissionMap>(res.getIntField("permission_map"));
out.push_back(info);
res.nextRow();
}
return out;
}
std::vector<ICharInfo::Info> SQLiteDatabase::GetCharactersWithPendingNames() {
std::vector<ICharInfo::Info> out;
auto [stmt, 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.eof()) {
ICharInfo::Info info;
info.id = res.getInt64Field("id");
info.name = res.getStringField("name");
info.pendingName = res.getStringField("pending_name");
info.needsRename = res.getIntField("needs_rename");
info.cloneId = res.getInt64Field("prop_clone_id");
info.accountId = res.getIntField("account_id");
info.permissionMap = static_cast<ePermissionMap>(res.getIntField("permission_map"));
out.push_back(info);
res.nextRow();
}
return out;
}
// SQLite-specific implementations added: UpdateCharacterPermissions and SetCharacterNeedsRename
void SQLiteDatabase::UpdateCharacterPermissions(const LWOOBJID characterId, const ePermissionMap permissions) {
ExecuteUpdate("UPDATE charinfo SET permission_map = ? WHERE id = ?;", static_cast<uint64_t>(permissions), characterId);
}
void SQLiteDatabase::SetCharacterNeedsRename(const LWOOBJID characterId, const bool needsRename) {
ExecuteUpdate("UPDATE charinfo SET needs_rename = ? WHERE id = ?;", needsRename ? 1 : 0, characterId);
}
std::optional<ICharInfo::Stats> SQLiteDatabase::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> SQLiteDatabase::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> SQLiteDatabase::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.eof()) {
ICharInfo::Activity activity;
activity.timestamp = res.getInt64Field("time");
activity.activity = static_cast<eActivityType>(res.getIntField("activity"));
activity.mapId = res.getIntField("map_id");
activities.push_back(activity);
res.nextRow();
}
return activities;
}
void SQLiteDatabase::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,20 @@
void SQLiteDatabase::InsertSlashCommandUsage(const LWOOBJID characterId, const std::string_view command) {
ExecuteInsert("INSERT INTO command_log (character_id, command) VALUES (?, ?);", characterId, command);
}
std::vector<ICommandLog::Entry> SQLiteDatabase::GetCommandLogs(uint32_t limit) {
auto [_, result] = ExecuteSelect("SELECT id, character_id, command FROM command_log ORDER BY id DESC LIMIT ?;", limit);
std::vector<ICommandLog::Entry> logs;
while (!result.eof()) {
ICommandLog::Entry entry;
entry.timestamp = 0; // Timestamp column doesn't exist in command_log table
entry.characterId = result.getInt64Field("character_id");
entry.command = result.getStringField("command");
entry.arguments = ""; // Arguments not currently stored in DB
logs.push_back(entry);
result.nextRow();
}
return logs;
}

View File

@@ -0,0 +1,67 @@
#include "SQLiteDatabase.h"
#include <ctime>
void SQLiteDatabase::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> SQLiteDatabase::GetRecentAuditLogs(uint32_t limit) {
auto [_, result] = ExecuteSelect("SELECT id, timestamp, ip_address, endpoint, method, user_agent, response_code FROM dashboard_audit_log ORDER BY timestamp DESC LIMIT ?;", limit);
std::vector<IDashboardAuditLog::AuditLogEntry> logs;
while (result.nextRow()) {
IDashboardAuditLog::AuditLogEntry entry;
entry.id = result.getInt64Field(0);
entry.timestamp = result.getInt64Field(1);
entry.ip_address = result.getStringField(2);
entry.endpoint = result.getStringField(3);
entry.method = result.getStringField(4);
entry.user_agent = result.getStringField(5);
entry.response_code = result.getIntField(6);
logs.push_back(entry);
}
return logs;
}
std::vector<IDashboardAuditLog::AuditLogEntry> SQLiteDatabase::GetAuditLogsByIP(const std::string_view ip, uint32_t limit) {
auto [_, result] = 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);
std::vector<IDashboardAuditLog::AuditLogEntry> logs;
while (result.nextRow()) {
IDashboardAuditLog::AuditLogEntry entry;
entry.id = result.getInt64Field(0);
entry.timestamp = result.getInt64Field(1);
entry.ip_address = result.getStringField(2);
entry.endpoint = result.getStringField(3);
entry.method = result.getStringField(4);
entry.user_agent = result.getStringField(5);
entry.response_code = result.getIntField(6);
logs.push_back(entry);
}
return logs;
}
void SQLiteDatabase::CleanupOldAuditLogs(uint32_t days_to_keep) {
uint64_t cutoff_time = static_cast<uint64_t>(std::time(nullptr)) - (days_to_keep * 86400);
ExecuteDelete("DELETE FROM dashboard_audit_log WHERE timestamp < ?;", cutoff_time);
}
void SQLiteDatabase::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> SQLiteDatabase::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,15 @@
#include "SQLiteDatabase.h"
std::optional<std::string> SQLiteDatabase::GetDashboardConfig(const std::string_view key) {
auto [_, result] = ExecuteSelect("SELECT config_value FROM dashboard_config WHERE config_key = ?;", key);
if (result.nextRow()) {
return result.getStringField(0);
}
return std::nullopt;
}
void SQLiteDatabase::SetDashboardConfig(const std::string_view key, const std::string_view value) {
ExecuteInsert("INSERT OR REPLACE INTO dashboard_config (config_key, config_value, updated_at) VALUES (?, ?, strftime('%s', 'now'));", key, value);
}

View File

@@ -24,3 +24,46 @@ std::optional<IPetNames::Info> SQLiteDatabase::GetPetNameInfo(const LWOOBJID& pe
return toReturn;
}
std::vector<IPetNames::DetailedInfo> SQLiteDatabase::GetAllPetNames() {
std::vector<IPetNames::DetailedInfo> out;
auto [stmt, res] = ExecuteSelect("SELECT id, pet_name, approved, owner_id FROM pet_names ORDER BY id DESC;");
while (!res.eof()) {
IPetNames::DetailedInfo info;
info.id = static_cast<LWOOBJID>(res.getInt64Field("id"));
info.petName = res.getStringField("pet_name");
info.approvalStatus = res.getIntField("approved");
info.ownerId = static_cast<LWOOBJID>(res.getInt64Field("owner_id"));
out.push_back(info);
res.nextRow();
}
return out;
}
std::vector<IPetNames::DetailedInfo> SQLiteDatabase::GetPetNamesByStatus(int32_t status) {
std::vector<IPetNames::DetailedInfo> out;
auto [stmt, res] = ExecuteSelect("SELECT id, pet_name, approved, owner_id FROM pet_names WHERE approved = ? ORDER BY id DESC;", status);
while (!res.eof()) {
IPetNames::DetailedInfo info;
info.id = static_cast<LWOOBJID>(res.getInt64Field("id"));
info.petName = res.getStringField("pet_name");
info.approvalStatus = res.getIntField("approved");
info.ownerId = static_cast<LWOOBJID>(res.getInt64Field("owner_id"));
out.push_back(info);
res.nextRow();
}
return out;
}
void SQLiteDatabase::SetPetApprovalStatus(const LWOOBJID& petId, int32_t status) {
ExecuteUpdate("UPDATE pet_names SET approved = ? WHERE id = ?;", status, petId);
}
uint32_t SQLiteDatabase::GetPendingPetNamesCount() {
auto [_, res] = ExecuteSelect("SELECT COUNT(*) as count FROM pet_names WHERE approved = 1;");
return res.eof() ? 0 : res.getIntField("count");
}

View File

@@ -9,3 +9,118 @@ std::optional<bool> SQLiteDatabase::IsPlaykeyActive(const int32_t playkeyId) {
return keyCheckRes.getIntField("active");
}
std::vector<IPlayKeys::Info> SQLiteDatabase::GetAllPlayKeys() {
std::vector<IPlayKeys::Info> out;
auto [stmt, res] = ExecuteSelect(
"SELECT id, key_string, key_uses, times_used, active, notes, strftime('%s', created_at) as created_at "
"FROM play_keys ORDER BY id DESC;"
);
while (!res.eof()) {
IPlayKeys::Info info;
info.id = res.getIntField("id");
info.key_string = res.getStringField("key_string");
info.key_uses = res.getIntField("key_uses");
info.times_used = res.getIntField("times_used");
info.active = res.getIntField("active") != 0;
info.notes = res.getStringField("notes");
info.created_at = static_cast<uint64_t>(res.getInt64Field("created_at"));
out.push_back(info);
res.nextRow();
}
return out;
}
std::optional<IPlayKeys::Info> SQLiteDatabase::GetPlayKeyById(const uint32_t playkeyId) {
auto [_, result] = ExecuteSelect(
"SELECT id, key_string, key_uses, times_used, active, notes, strftime('%s', created_at) as created_at "
"FROM play_keys WHERE id = ? LIMIT 1;",
playkeyId
);
if (result.eof()) {
return std::nullopt;
}
IPlayKeys::Info info;
info.id = result.getIntField("id");
info.key_string = result.getStringField("key_string");
info.key_uses = result.getIntField("key_uses");
info.times_used = result.getIntField("times_used");
info.active = result.getIntField("active") != 0;
info.notes = result.getStringField("notes");
info.created_at = static_cast<uint64_t>(result.getInt64Field("created_at"));
return info;
}
std::optional<IPlayKeys::Info> SQLiteDatabase::GetPlayKeyByString(const std::string_view key_string) {
auto [_, result] = ExecuteSelect(
"SELECT id, key_string, key_uses, times_used, active, notes, strftime('%s', created_at) as created_at "
"FROM play_keys WHERE key_string = ? LIMIT 1;",
key_string
);
if (result.eof()) {
return std::nullopt;
}
IPlayKeys::Info info;
info.id = result.getIntField("id");
info.key_string = result.getStringField("key_string");
info.key_uses = result.getIntField("key_uses");
info.times_used = result.getIntField("times_used");
info.active = result.getIntField("active") != 0;
info.notes = result.getStringField("notes");
info.created_at = static_cast<uint64_t>(result.getInt64Field("created_at"));
return info;
}
bool SQLiteDatabase::ConsumePlayKeyUsage(const uint32_t playkeyId) {
// Atomically check times_used < key_uses and increment times_used. If uses exhausted, deactivate.
auto [stmt, res] = ExecuteSelect("SELECT key_uses, times_used, active FROM play_keys WHERE id = ? LIMIT 1;", playkeyId);
if (res.eof()) return false;
int key_uses = res.getIntField("key_uses");
int times_used = res.getIntField("times_used");
int active = res.getIntField("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);
// If we've reached the limit, deactivate
if (times_used + 1 >= key_uses) {
ExecuteUpdate("UPDATE play_keys SET active = 0 WHERE id = ?;", playkeyId);
}
return true;
}
void SQLiteDatabase::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 SQLiteDatabase::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 ? 1 : 0, notes, playkeyId
);
}
void SQLiteDatabase::DeletePlayKey(const uint32_t playkeyId) {
ExecuteDelete("DELETE FROM play_keys WHERE id = ?;", playkeyId);
}
uint32_t SQLiteDatabase::GetPlayKeyCount() {
auto [_, res] = ExecuteSelect("SELECT COUNT(*) as count FROM play_keys;");
return res.eof() ? 0 : res.getIntField("count");
}

View File

@@ -193,10 +193,53 @@ std::optional<IProperty::Info> SQLiteDatabase::GetPropertyInfo(const LWOOBJID id
auto [_, propertyEntry] = 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 id = ?;", id);
if (propertyEntry.eof()) {
return std::nullopt;
}
return ReadPropertyInfo(propertyEntry);
}
std::vector<IProperty::Info> SQLiteDatabase::GetAllProperties() {
std::vector<IProperty::Info> out;
auto [stmt, 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.eof()) {
out.push_back(ReadPropertyInfo(res));
res.nextRow();
}
return out;
}
std::vector<IProperty::Info> SQLiteDatabase::GetPropertiesByApprovalStatus(uint32_t approved) {
std::vector<IProperty::Info> out;
auto [stmt, 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.eof()) {
out.push_back(ReadPropertyInfo(res));
res.nextRow();
}
return out;
}
uint32_t SQLiteDatabase::GetPropertyCount() {
auto [_, res] = ExecuteSelect("SELECT COUNT(*) as count FROM properties;");
return res.eof() ? 0 : res.getIntField("count");
}
uint32_t SQLiteDatabase::GetUnapprovedPropertyCount() {
auto [_, res] = ExecuteSelect("SELECT COUNT(*) as count FROM properties WHERE mod_approved = 0;");
return res.eof() ? 0 : res.getIntField("count");
}