Compare commits

...

35 Commits

Author SHA1 Message Date
Aaron Kimbrell
e3467465b4 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.
2026-04-22 11:01:41 -05:00
Aaron Kimbrell
d532a9b063 It works (kinda) now to actually implement things 2025-10-11 00:02:31 -05:00
Aaron Kimbrell
5453d163a3 WIP 2025-10-06 19:15:55 -05:00
David Markowitz
62ac65c520 feat: Mission Component debug (#1901)
* feat: Mission Component debug

* Add player argument to inspect command

* Add completion details

* Remove unlocalized server string

done on client instead
2025-10-05 22:13:27 -05:00
HailStorm32
5d5bce53d0 feat: Add configurable restrictions for muted accounts (#1887)
* Add configurable restrictions for muted accounts

* switched to and updated GetRandomElement

* Update config option check

* implement cached config values for mute settings and update handlers

* Address review

* Update dGame/dComponents/PetComponent.cpp

Co-authored-by: David Markowitz <39972741+EmosewaMC@users.noreply.github.com>

* Update dGame/dComponents/PetComponent.cpp

Co-authored-by: David Markowitz <39972741+EmosewaMC@users.noreply.github.com>

* reduce if argument chain

---------

Co-authored-by: David Markowitz <39972741+EmosewaMC@users.noreply.github.com>
2025-10-05 22:09:43 -05:00
David Markowitz
5791c55a9e fix: the exploding script is the most amazing piece of code i have ev… (#1900)
* fix: the exploding script is the most amazing piece of code i have ever had the pleasure of working with and has been amazing to work on and translate from lua

hahahahahahahahahahwwwwwwwwwwwwwwww草

* Enhance hit detection with proximity object checks

Refactor hit handling to include proximity checks for destroyable entities.
2025-10-05 00:19:46 -07:00
David Markowitz
17d0c45382 fix: why oh why is the aggro radius apart of the enemy (#1899) 2025-10-04 20:45:42 -07:00
David Markowitz
7dbbef81ac fix: regenerated proxy items dont need new ids and fix equip item ids (#1897)
* fix: changed item ids not reflected in equipped items

* dont do it for proxy items
2025-10-04 18:42:34 -07:00
David Markowitz
06958cb9cd feat: hardcore limit % coins dropped on death (#1898)
* feat: hardcore limit % coins dropped on death

Update EntityManager.cpp

* fix log msg
2025-10-04 17:25:23 -07:00
David Markowitz
69b1a694a6 fix: ignore foreign key checks more (#1895)
fixes an issue if you delete users in an earlier build of dlu.
2025-10-04 13:57:16 -05:00
David Markowitz
b2609ff6cb fix: live accurate player flag missions and flag debugging (#1894)
* feat: Add component ID to root component object

* fix: live accurate player flag missions and flag debugging

Tested that the client reflects the correct server progression after a test map and manually setting a flag off.
tested that session flags correctly pick up on progression updates

* banana
2025-10-04 01:07:52 -05:00
David Markowitz
e8c0b3e6da feat: Add component ID to root component object (#1893) 2025-10-03 20:57:42 -05:00
David Markowitz
25418fd8b2 fix: exploding asset bugs (#1890) 2025-10-01 20:48:08 -05:00
David Markowitz
502c965d97 feat: script debug info (#1891) 2025-10-01 14:21:25 -05:00
David Markowitz
205c190c61 fix: proxy items not equipping on login (#1892)
tested that items now equip on login
2025-10-01 07:55:51 -05:00
David Markowitz
670cb124c0 Initialize m_ActivityInfo with default constructor (#1889) 2025-09-30 23:33:04 -05:00
David Markowitz
76c2f380bf feat: re-write persistent object ID tracker (#1888)
* feat: re-write persistent object ID tracker

Features:
- Remove random objectIDs entirely
- Replace random objectIDs with persistentIDs
- Remove the need to contact the MASTER server for a persistent ID
- Add persistent ID logic to WorldServers that use transactions to guarantee unique IDs no matter when they are generated
- Default character xml version to be the most recent one

Fixes:
- Return optional from GetModel (and check for nullopt where it may exist)
- Regenerate inventory item ids on first login to be unique item IDs (fixes all those random IDs

Pet IDs and subkeys are left alone and are assumed to be reserved (checks are there to prevent this)
There is also duplicate check logic in place for properties and UGC/Models

* Update comment and log

* fix: sqlite transaction bug

* fix colliding temp item ids

temp items should not be saved. would cause issues between worlds as experienced before this commit
2025-09-29 08:54:37 -05:00
David Markowitz
b5a3cc9187 fix: Extend saved ugc id to 64 bits (#1885)
Tetsed that ugc models are saved and loaded with the correct bits
2025-09-24 06:01:46 -05:00
David Markowitz
74e1d36bb1 feat: Hardcore mode settings (#1884) 2025-09-23 07:02:29 -05:00
David Markowitz
64faac714c feat: Remove PERSISTENT ObjectID bit because it's not an ObjectID bit (#1881)
* feat: Remove PERSISTENT ObjectID bit because it's not an ObjectID bit

TODO: Need to add character save migration for the pet subkey in the inventory
Tested that the migrations work on mysql and sqlite and that properties have all their contents as before.
Need to test pets still

* fix: ugc, pet ids. remove persistent bit
2025-09-22 23:41:38 -05:00
David Markowitz
4a5dd68e87 fix: hardcore mode fixes (#1882)
fixes hardcore modes uscore drops
adds config option for excluded item drops
2025-09-21 18:36:32 -07:00
David Markowitz
4a577f233d fix: hardcore mode fixes (#1883)
fixes hardcore modes uscore drops
adds config option for excluded item drops

f

feat: Add logging for config options on load and reload

feat: Add logging for config options on load and reload
2025-09-21 20:12:50 -05:00
David Markowitz
bb05b3ac0d feat: Add logging for testing Johnny missions (#1880) 2025-09-20 17:22:16 -07:00
David Markowitz
06022e4b19 fix: use after free in TCPInterface (#1879)
lol
2025-09-19 01:12:34 -05:00
David Markowitz
6389876c6e feat: convert character ids to 64 bits (#1878)
* feat: convert character ids to 64 bits

remove all usages of the PERSISTENT bit with regards to storing of playerIDs on the server.  the bit does not exist and was a phantom in the first place.
Tested that a full playthrough of ag, ns and gf was still doable.  slash commands work, ugc works, friends works, ignore list works, properties work and have names, teaming works.
migrating an old mysql database works . need to test an old sqlite database

* fix sqlite migration

* remove nd specific column migration
2025-09-19 01:12:23 -05:00
David Markowitz
68f2e2dee2 Fix FetchContent_Declare speed (#1875) 2025-09-12 03:32:15 -05:00
HailStorm32
b798da8ef8 fix: Update mute expiry from database (#1871)
* Update mute expiry from database

* Address review comments

* Address review comment

Co-authored-by: David Markowitz <39972741+EmosewaMC@users.noreply.github.com>

---------

Co-authored-by: David Markowitz <39972741+EmosewaMC@users.noreply.github.com>
2025-09-08 23:07:08 -07:00
Copilot
154112050f feat: Implement Minecraft-style execute command with relative positioning (#1864)
* Initial plan

* Implement Minecraft-style execute command

Co-authored-by: aronwk-aaron <26027722+aronwk-aaron@users.noreply.github.com>

* Add relative positioning support to execute command using ~ syntax

Co-authored-by: aronwk-aaron <26027722+aronwk-aaron@users.noreply.github.com>

* update the parsing and fix chat response

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: aronwk-aaron <26027722+aronwk-aaron@users.noreply.github.com>
Co-authored-by: Aaron Kimbrell <aronwk.aaron@gmail.com>
2025-09-08 22:35:18 -07:00
David Markowitz
6d3bf2fdc3 fix: need to create account twice due to commit latency?? (#1873)
idk fixes the issue
2025-09-08 22:50:22 -05:00
David Markowitz
566a18df38 Show git download progress with FetchContent (#1869)
Some downloads are slower and showing progress is better than sitting there doing nothing
2025-09-07 20:03:00 -05:00
David Markowitz
f6c13d9ee6 Replace Quaternion with glm math (#1868) 2025-09-06 19:18:03 -07:00
David Markowitz
8198ad70f6 fix: zero out component in destructor (#1863) 2025-09-01 19:06:00 -05:00
Gie "Max" Vanommeslaeghe
4c3bace601 Merge pull request #1862 from DarkflameUniverse/fix-item-exploits
fix: item exploits
2025-09-01 22:33:07 +02:00
David Markowitz
6d2a21450b fix item exploits
Update VendorComponent.cpp

Update Mail.cpp
2025-09-01 13:17:44 -07:00
David Markowitz
f9e74e6994 Add more logging around rewarding items (#1861) 2025-08-31 20:33:16 -07:00
365 changed files with 14856 additions and 1829 deletions

View File

@@ -19,6 +19,7 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # Export the compile commands for debuggi
set(CMAKE_POLICY_DEFAULT_CMP0063 NEW) # Set CMAKE visibility policy to NEW on project and subprojects
set(CMAKE_VISIBILITY_INLINES_HIDDEN ON) # Set C and C++ symbol visibility to hide inlined functions
set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
set(FETCHCONTENT_QUIET FALSE) # GLM takes a long time to clone, this will at least show _something_ while its downloading
# Read variables from file
FILE(READ "${CMAKE_SOURCE_DIR}/CMakeVariables.txt" variables)
@@ -88,6 +89,7 @@ elseif(MSVC)
add_compile_options("/wd4267" "/utf-8" "/volatile:iso" "/Zc:inline")
elseif(WIN32)
add_compile_definitions(_CRT_SECURE_NO_WARNINGS)
add_compile_definitions(NOMINMAX)
endif()
# Our output dir
@@ -125,7 +127,7 @@ endif()
message(STATUS "Variable: DLU_CONFIG_DIR = ${DLU_CONFIG_DIR}")
# Copy resource files on first build
set(RESOURCE_FILES "sharedconfig.ini" "authconfig.ini" "chatconfig.ini" "worldconfig.ini" "masterconfig.ini" "blocklist.dcf")
set(RESOURCE_FILES "sharedconfig.ini" "authconfig.ini" "chatconfig.ini" "dashboardconfig.ini" "worldconfig.ini" "masterconfig.ini" "blocklist.dcf")
message(STATUS "Checking resource file integrity")
include(Utils)
@@ -252,6 +254,7 @@ include_directories(
"thirdparty/MD5"
"thirdparty/nlohmann"
"thirdparty/mongoose"
"thirdparty/inja"
)
# Add system specfic includes for Apple, Windows and Other Unix OS' (including Linux)
@@ -306,7 +309,7 @@ add_subdirectory(dServer)
add_subdirectory(dWeb)
# Create a list of common libraries shared between all binaries
set(COMMON_LIBRARIES "dCommon" "dDatabase" "dNet" "raknet" "magic_enum")
set(COMMON_LIBRARIES glm::glm "dCommon" "dDatabase" "dNet" "raknet" "magic_enum")
# Add platform specific common libraries
if(UNIX)
@@ -321,6 +324,7 @@ endif()
add_subdirectory(dWorldServer)
add_subdirectory(dAuthServer)
add_subdirectory(dChatServer)
add_subdirectory(dDashboardServer)
add_subdirectory(dMasterServer) # Add MasterServer last so it can rely on the other binaries
target_precompile_headers(

View File

@@ -6,6 +6,8 @@ FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG release-1.12.1
GIT_PROGRESS TRUE
GIT_SHALLOW 1
)
# For Windows: Prevent overriding the parent project's compiler/linker settings

View File

@@ -52,6 +52,7 @@ int main(int argc, char** argv) {
//Create all the objects we need to run our service:
Server::SetupLogger("AuthServer");
if (!Game::logger) return EXIT_FAILURE;
Game::config->LogSettings();
LOG("Starting Auth server...");
LOG("Version: %s", PROJECT_VERSION);

View File

@@ -1,4 +1,4 @@
set(DCHATFILTER_SOURCES "dChatFilter.cpp")
add_library(dChatFilter STATIC ${DCHATFILTER_SOURCES})
target_link_libraries(dChatFilter dDatabase)
target_link_libraries(dChatFilter dDatabase glm::glm)

View File

@@ -14,6 +14,6 @@ add_compile_definitions(ChatServer PRIVATE PROJECT_VERSION="\"${PROJECT_VERSION}
add_library(dChatServer ${DCHATSERVER_SOURCES})
target_include_directories(dChatServer PRIVATE "${PROJECT_SOURCE_DIR}/dServer" "${PROJECT_SOURCE_DIR}/dChatFilter")
target_link_libraries(dChatServer ${COMMON_LIBRARIES} dChatFilter)
target_link_libraries(dChatServer ${COMMON_LIBRARIES} dChatFilter glm::glm)
target_link_libraries(ChatServer ${COMMON_LIBRARIES} dChatFilter dChatServer dServer mongoose dWeb)

View File

@@ -34,7 +34,7 @@ void ChatIgnoreList::GetIgnoreList(Packet* packet) {
if (!receiver.ignoredPlayers.empty()) {
LOG_DEBUG("Player %llu already has an ignore list, but is requesting it again.", playerId);
} else {
auto ignoreList = Database::Get()->GetIgnoreList(static_cast<uint32_t>(playerId));
auto ignoreList = Database::Get()->GetIgnoreList(playerId);
if (ignoreList.empty()) {
LOG_DEBUG("Player %llu has no ignores", playerId);
return;
@@ -43,7 +43,6 @@ void ChatIgnoreList::GetIgnoreList(Packet* packet) {
for (auto& ignoredPlayer : ignoreList) {
receiver.ignoredPlayers.emplace_back(ignoredPlayer.name, ignoredPlayer.id);
GeneralUtils::SetBit(receiver.ignoredPlayers.back().playerId, eObjectBits::CHARACTER);
GeneralUtils::SetBit(receiver.ignoredPlayers.back().playerId, eObjectBits::PERSISTENT);
}
}
@@ -114,9 +113,8 @@ void ChatIgnoreList::AddIgnore(Packet* packet) {
}
if (ignoredPlayerId != LWOOBJID_EMPTY) {
Database::Get()->AddIgnore(static_cast<uint32_t>(playerId), static_cast<uint32_t>(ignoredPlayerId));
Database::Get()->AddIgnore(playerId, ignoredPlayerId);
GeneralUtils::SetBit(ignoredPlayerId, eObjectBits::CHARACTER);
GeneralUtils::SetBit(ignoredPlayerId, eObjectBits::PERSISTENT);
receiver.ignoredPlayers.emplace_back(toIgnoreStr, ignoredPlayerId);
LOG_DEBUG("Player %llu is ignoring %s", playerId, toIgnoreStr.c_str());
@@ -157,7 +155,7 @@ void ChatIgnoreList::RemoveIgnore(Packet* packet) {
return;
}
Database::Get()->RemoveIgnore(static_cast<uint32_t>(playerId), static_cast<uint32_t>(toRemove->playerId));
Database::Get()->RemoveIgnore(playerId, toRemove->playerId);
receiver.ignoredPlayers.erase(toRemove, receiver.ignoredPlayers.end());
CBITSTREAM;

View File

@@ -35,7 +35,6 @@ void ChatPacketHandler::HandleFriendlistRequest(Packet* packet) {
FriendData fd;
fd.isFTP = false; // not a thing in DLU
fd.friendID = friendData.friendID;
GeneralUtils::SetBit(fd.friendID, eObjectBits::PERSISTENT);
GeneralUtils::SetBit(fd.friendID, eObjectBits::CHARACTER);
fd.isBestFriend = friendData.isBestFriend; //0 = friends, 1 = left_requested, 2 = right_requested, 3 = both_accepted - are now bffs
@@ -161,9 +160,7 @@ void ChatPacketHandler::HandleFriendRequest(Packet* packet) {
// Set the bits
GeneralUtils::SetBit(queryPlayerID, eObjectBits::CHARACTER);
GeneralUtils::SetBit(queryPlayerID, eObjectBits::PERSISTENT);
GeneralUtils::SetBit(queryFriendID, eObjectBits::CHARACTER);
GeneralUtils::SetBit(queryFriendID, eObjectBits::PERSISTENT);
// Since this player can either be the friend of someone else or be friends with someone else
// their column in the database determines what bit gets set. When the value hits 3, they
@@ -318,7 +315,6 @@ void ChatPacketHandler::HandleRemoveFriend(Packet* packet) {
}
// Convert friendID to LWOOBJID
GeneralUtils::SetBit(friendID, eObjectBits::PERSISTENT);
GeneralUtils::SetBit(friendID, eObjectBits::CHARACTER);
Database::Get()->RemoveFriend(playerID, friendID);

View File

@@ -59,6 +59,7 @@ int main(int argc, char** argv) {
//Create all the objects we need to run our service:
Server::SetupLogger("ChatServer");
if (!Game::logger) return EXIT_FAILURE;
Game::config->LogSettings();
//Read our config:

View File

@@ -26,12 +26,14 @@ void HandleHTTPPlayersRequest(HTTPReply& reply, std::string body) {
const json data = Game::playerContainer;
reply.status = data.empty() ? eHTTPStatusCode::NO_CONTENT : eHTTPStatusCode::OK;
reply.message = data.empty() ? "{\"error\":\"No Players Online\"}" : data.dump();
reply.contentType = ContentType::JSON;
}
void HandleHTTPTeamsRequest(HTTPReply& reply, std::string body) {
const json data = TeamContainer::GetTeamContainer();
reply.status = data.empty() ? eHTTPStatusCode::NO_CONTENT : eHTTPStatusCode::OK;
reply.message = data.empty() ? "{\"error\":\"No Teams Online\"}" : data.dump();
reply.contentType = ContentType::JSON;
}
void HandleHTTPAnnounceRequest(HTTPReply& reply, std::string body) {
@@ -39,6 +41,7 @@ void HandleHTTPAnnounceRequest(HTTPReply& reply, std::string body) {
if (!data) {
reply.status = eHTTPStatusCode::BAD_REQUEST;
reply.message = "{\"error\":\"Invalid JSON\"}";
reply.contentType = ContentType::JSON;
return;
}
@@ -47,6 +50,7 @@ void HandleHTTPAnnounceRequest(HTTPReply& reply, std::string body) {
if (!check.empty()) {
reply.status = eHTTPStatusCode::BAD_REQUEST;
reply.message = check;
reply.contentType = ContentType::JSON;
} else {
ChatPackets::Announcement announcement;
@@ -56,6 +60,7 @@ void HandleHTTPAnnounceRequest(HTTPReply& reply, std::string body) {
reply.status = eHTTPStatusCode::OK;
reply.message = "{\"status\":\"Announcement Sent\"}";
reply.contentType = ContentType::JSON;
}
}

View File

@@ -54,6 +54,8 @@ elseif (WIN32)
zlib
URL https://github.com/madler/zlib/archive/refs/tags/v1.2.11.zip
URL_HASH MD5=9d6a627693163bbbf3f26403a3a0b0b1
GIT_PROGRESS TRUE
GIT_SHALLOW 1
)
# Disable warning about no project version.
@@ -74,5 +76,6 @@ else ()
endif ()
target_link_libraries(dCommon
PUBLIC glm::glm
PRIVATE ZLIB::ZLIB bcrypt tinyxml2
INTERFACE dDatabase)

View File

@@ -3,7 +3,7 @@
// C++
#include <charconv>
#include <cstdint>
#include <cmath>
#include <cmath>
#include <ctime>
#include <functional>
#include <optional>
@@ -19,6 +19,9 @@
#include "dPlatforms.h"
#include "Game.h"
#include "Logger.h"
#include "DluAssert.h"
#include <glm/ext/vector_float3.hpp>
enum eInventoryType : uint32_t;
enum class eObjectBits : size_t;
@@ -244,7 +247,7 @@ namespace GeneralUtils {
* @returns An std::optional containing the desired NiPoint3 if it can be constructed from the string parameters
*/
template <typename T>
[[nodiscard]] std::optional<NiPoint3> TryParse(const std::string_view strX, const std::string_view strY, const std::string_view strZ) {
[[nodiscard]] std::optional<T> TryParse(const std::string_view strX, const std::string_view strY, const std::string_view strZ) {
const auto x = TryParse<float>(strX);
if (!x) return std::nullopt;
@@ -252,7 +255,7 @@ namespace GeneralUtils {
if (!y) return std::nullopt;
const auto z = TryParse<float>(strZ);
return z ? std::make_optional<NiPoint3>(x.value(), y.value(), z.value()) : std::nullopt;
return z ? std::make_optional<T>(x.value(), y.value(), z.value()) : std::nullopt;
}
/**
@@ -261,8 +264,8 @@ namespace GeneralUtils {
* @returns An std::optional containing the desired NiPoint3 if it can be constructed from the string parameters
*/
template <typename T>
[[nodiscard]] std::optional<NiPoint3> TryParse(const std::span<const std::string> str) {
return (str.size() == 3) ? TryParse<NiPoint3>(str[0], str[1], str[2]) : std::nullopt;
[[nodiscard]] std::optional<T> TryParse(const std::span<const std::string> str) {
return (str.size() == 3) ? TryParse<T>(str[0], str[1], str[2]) : std::nullopt;
}
template <typename T>
@@ -303,7 +306,7 @@ namespace GeneralUtils {
template<typename Container>
inline Container::value_type GetRandomElement(const Container& container) {
DluAssert(!container.empty());
return container[GenerateRandomNumber<typename Container::value_type>(0, container.size() - 1)];
return container[GenerateRandomNumber<typename Container::size_type>(0, container.size() - 1)];
}
/**

View File

@@ -6,10 +6,14 @@
\brief Defines a point in space in XYZ coordinates
*/
class NiPoint3;
class NiQuaternion;
typedef NiPoint3 Vector3; //!< The Vector3 class is technically the NiPoint3 class, but typedef'd for clarity in some cases
#include <glm/ext/vector_float3.hpp>
#include "NiQuaternion.h"
//! A custom class the defines a point in space
class NiPoint3 {
public:
@@ -21,6 +25,12 @@ public:
//! Initializer
constexpr NiPoint3() = default;
constexpr NiPoint3(const glm::vec3& vec) noexcept
: x{ vec.x }
, y{ vec.y }
, z{ vec.z } {
}
//! Initializer
/*!
\param x The x coordinate

View File

@@ -4,6 +4,7 @@
#endif
#include "NiQuaternion.h"
#include <glm/ext/quaternion_float.hpp>
// MARK: Getters / Setters

View File

@@ -3,37 +3,18 @@
// C++
#include <cmath>
#include <glm/gtx/quaternion.hpp>
// MARK: Member Functions
Vector3 NiQuaternion::GetEulerAngles() const {
Vector3 angles;
// roll (x-axis rotation)
const float sinr_cosp = 2 * (w * x + y * z);
const float cosr_cosp = 1 - 2 * (x * x + y * y);
angles.x = std::atan2(sinr_cosp, cosr_cosp);
// pitch (y-axis rotation)
const float sinp = 2 * (w * y - z * x);
if (std::abs(sinp) >= 1) {
angles.y = std::copysign(3.14 / 2, sinp); // use 90 degrees if out of range
} else {
angles.y = std::asin(sinp);
}
// yaw (z-axis rotation)
const float siny_cosp = 2 * (w * z + x * y);
const float cosy_cosp = 1 - 2 * (y * y + z * z);
angles.z = std::atan2(siny_cosp, cosy_cosp);
return angles;
Vector3 QuatUtils::Euler(const NiQuaternion& quat) {
return glm::eulerAngles(quat);
}
// MARK: Helper Functions
//! Look from a specific point in space to another point in space (Y-locked)
NiQuaternion NiQuaternion::LookAt(const NiPoint3& sourcePoint, const NiPoint3& destPoint) {
NiQuaternion QuatUtils::LookAt(const NiPoint3& sourcePoint, const NiPoint3& destPoint) {
//To make sure we don't orient around the X/Z axis:
NiPoint3 source = sourcePoint;
NiPoint3 dest = destPoint;
@@ -51,11 +32,11 @@ NiQuaternion NiQuaternion::LookAt(const NiPoint3& sourcePoint, const NiPoint3& d
NiPoint3 vecB = vecA.CrossProduct(posZ);
if (vecB.DotProduct(forwardVector) < 0) rotAngle = -rotAngle;
return NiQuaternion::CreateFromAxisAngle(vecA, rotAngle);
return glm::angleAxis(rotAngle, glm::vec3{vecA.x, vecA.y, vecA.z});
}
//! Look from a specific point in space to another point in space
NiQuaternion NiQuaternion::LookAtUnlocked(const NiPoint3& sourcePoint, const NiPoint3& destPoint) {
NiQuaternion QuatUtils::LookAtUnlocked(const NiPoint3& sourcePoint, const NiPoint3& destPoint) {
NiPoint3 forwardVector = NiPoint3(destPoint - sourcePoint).Unitize();
NiPoint3 posZ = NiPoint3Constant::UNIT_Z;
@@ -67,37 +48,26 @@ NiQuaternion NiQuaternion::LookAtUnlocked(const NiPoint3& sourcePoint, const NiP
NiPoint3 vecB = vecA.CrossProduct(posZ);
if (vecB.DotProduct(forwardVector) < 0) rotAngle = -rotAngle;
return NiQuaternion::CreateFromAxisAngle(vecA, rotAngle);
return glm::angleAxis(rotAngle, glm::vec3{vecA.x, vecA.y, vecA.z});
}
//! Creates a Quaternion from a specific axis and angle relative to that axis
NiQuaternion NiQuaternion::CreateFromAxisAngle(const Vector3& axis, float angle) {
float halfAngle = angle * 0.5f;
float s = static_cast<float>(sin(halfAngle));
NiQuaternion q;
q.x = axis.GetX() * s;
q.y = axis.GetY() * s;
q.z = axis.GetZ() * s;
q.w = static_cast<float>(cos(halfAngle));
return q;
NiQuaternion QuatUtils::AxisAngle(const Vector3& axis, float angle) {
return glm::angleAxis(angle, glm::vec3(axis.x, axis.y, axis.z));
}
NiQuaternion NiQuaternion::FromEulerAngles(const NiPoint3& eulerAngles) {
// Abbreviations for the various angular functions
float cy = cos(eulerAngles.z * 0.5);
float sy = sin(eulerAngles.z * 0.5);
float cp = cos(eulerAngles.y * 0.5);
float sp = sin(eulerAngles.y * 0.5);
float cr = cos(eulerAngles.x * 0.5);
float sr = sin(eulerAngles.x * 0.5);
NiQuaternion q;
q.w = cr * cp * cy + sr * sp * sy;
q.x = sr * cp * cy - cr * sp * sy;
q.y = cr * sp * cy + sr * cp * sy;
q.z = cr * cp * sy - sr * sp * cy;
return q;
NiQuaternion QuatUtils::FromEuler(const NiPoint3& eulerAngles) {
return glm::quat(glm::vec3(eulerAngles.x, eulerAngles.y, eulerAngles.z));
}
Vector3 QuatUtils::Forward(const NiQuaternion& quat) {
return quat * glm::vec3(0, 0, 1);
}
Vector3 QuatUtils::Up(const NiQuaternion& quat) {
return quat * glm::vec3(0, 1, 0);
}
Vector3 QuatUtils::Right(const NiQuaternion& quat) {
return quat * glm::vec3(1, 0, 0);
}

View File

@@ -1,158 +1,27 @@
#ifndef __NIQUATERNION_H__
#define __NIQUATERNION_H__
#ifndef NIQUATERNION_H
#define NIQUATERNION_H
// Custom Classes
#include "NiPoint3.h"
/*!
\file NiQuaternion.hpp
\brief Defines a quaternion in space in WXYZ coordinates
*/
#define GLM_FORCE_QUAT_DATA_WXYZ
class NiQuaternion;
typedef NiQuaternion Quaternion; //!< A typedef for a shorthand version of NiQuaternion
#include <glm/ext/quaternion_float.hpp>
//! A class that defines a rotation in space
class NiQuaternion {
public:
float w{ 1 }; //!< The w coordinate
float x{ 0 }; //!< The x coordinate
float y{ 0 }; //!< The y coordinate
float z{ 0 }; //!< The z coordinate
using Quaternion = glm::quat;
using NiQuaternion = Quaternion;
//! The initializer
constexpr NiQuaternion() = default;
//! The initializer
/*!
\param w The w coordinate
\param x The x coordinate
\param y The y coordinate
\param z The z coordinate
*/
constexpr NiQuaternion(const float w, const float x, const float y, const float z) noexcept
: w{ w }
, x{ x }
, y{ y }
, z{ z } {
}
// MARK: Setters / Getters
//! Gets the W coordinate
/*!
\return The w coordinate
*/
[[nodiscard]] constexpr float GetW() const noexcept;
//! Sets the W coordinate
/*!
\param w The w coordinate
*/
constexpr void SetW(const float w) noexcept;
//! Gets the X coordinate
/*!
\return The x coordinate
*/
[[nodiscard]] constexpr float GetX() const noexcept;
//! Sets the X coordinate
/*!
\param x The x coordinate
*/
constexpr void SetX(const float x) noexcept;
//! Gets the Y coordinate
/*!
\return The y coordinate
*/
[[nodiscard]] constexpr float GetY() const noexcept;
//! Sets the Y coordinate
/*!
\param y The y coordinate
*/
constexpr void SetY(const float y) noexcept;
//! Gets the Z coordinate
/*!
\return The z coordinate
*/
[[nodiscard]] constexpr float GetZ() const noexcept;
//! Sets the Z coordinate
/*!
\param z The z coordinate
*/
constexpr void SetZ(const float z) noexcept;
// MARK: Member Functions
//! Returns the forward vector from the quaternion
/*!
\return The forward vector of the quaternion
*/
[[nodiscard]] constexpr Vector3 GetForwardVector() const noexcept;
//! Returns the up vector from the quaternion
/*!
\return The up vector fo the quaternion
*/
[[nodiscard]] constexpr Vector3 GetUpVector() const noexcept;
//! Returns the right vector from the quaternion
/*!
\return The right vector of the quaternion
*/
[[nodiscard]] constexpr Vector3 GetRightVector() const noexcept;
[[nodiscard]] Vector3 GetEulerAngles() const;
// MARK: Operators
//! Operator to check for equality
constexpr bool operator==(const NiQuaternion& rot) const noexcept;
//! Operator to check for inequality
constexpr bool operator!=(const NiQuaternion& rot) const noexcept;
// MARK: Helper Functions
//! Look from a specific point in space to another point in space (Y-locked)
/*!
\param sourcePoint The source location
\param destPoint The destination location
\return The Quaternion with the rotation towards the destination
*/
[[nodiscard]] static NiQuaternion LookAt(const NiPoint3& sourcePoint, const NiPoint3& destPoint);
//! Look from a specific point in space to another point in space
/*!
\param sourcePoint The source location
\param destPoint The destination location
\return The Quaternion with the rotation towards the destination
*/
[[nodiscard]] static NiQuaternion LookAtUnlocked(const NiPoint3& sourcePoint, const NiPoint3& destPoint);
//! Creates a Quaternion from a specific axis and angle relative to that axis
/*!
\param axis The axis that is used
\param angle The angle relative to this axis
\return A quaternion created from the axis and angle
*/
[[nodiscard]] static NiQuaternion CreateFromAxisAngle(const Vector3& axis, float angle);
[[nodiscard]] static NiQuaternion FromEulerAngles(const NiPoint3& eulerAngles);
namespace QuatUtils {
constexpr NiQuaternion IDENTITY = glm::identity<NiQuaternion>();
Vector3 Forward(const NiQuaternion& quat);
Vector3 Up(const NiQuaternion& quat);
Vector3 Right(const NiQuaternion& quat);
NiQuaternion LookAt(const NiPoint3& from, const NiPoint3& to);
NiQuaternion LookAtUnlocked(const NiPoint3& from, const NiPoint3& to);
Vector3 Euler(const NiQuaternion& quat);
NiQuaternion AxisAngle(const Vector3& axis, float angle);
NiQuaternion FromEuler(const NiPoint3& eulerAngles);
constexpr float PI_OVER_180 = glm::pi<float>() / 180.0f;
};
// Static Variables
namespace NiQuaternionConstant {
constexpr NiQuaternion IDENTITY(1, 0, 0, 0);
}
// Include constexpr and inline function definitions in a seperate file for readability
#include "NiQuaternion.inl"
#endif // !__NIQUATERNION_H__
#endif // !NIQUATERNION_H

View File

@@ -1,75 +0,0 @@
#pragma once
#ifndef __NIQUATERNION_H__
#error "This should only be included inline in NiQuaternion.h: Do not include directly!"
#endif
// MARK: Setters / Getters
//! Gets the W coordinate
constexpr float NiQuaternion::GetW() const noexcept {
return this->w;
}
//! Sets the W coordinate
constexpr void NiQuaternion::SetW(const float w) noexcept {
this->w = w;
}
//! Gets the X coordinate
constexpr float NiQuaternion::GetX() const noexcept {
return this->x;
}
//! Sets the X coordinate
constexpr void NiQuaternion::SetX(const float x) noexcept {
this->x = x;
}
//! Gets the Y coordinate
constexpr float NiQuaternion::GetY() const noexcept {
return this->y;
}
//! Sets the Y coordinate
constexpr void NiQuaternion::SetY(const float y) noexcept {
this->y = y;
}
//! Gets the Z coordinate
constexpr float NiQuaternion::GetZ() const noexcept {
return this->z;
}
//! Sets the Z coordinate
constexpr void NiQuaternion::SetZ(const float z) noexcept {
this->z = z;
}
// MARK: Member Functions
//! Returns the forward vector from the quaternion
constexpr Vector3 NiQuaternion::GetForwardVector() const noexcept {
return Vector3(2 * (x * z + w * y), 2 * (y * z - w * x), 1 - 2 * (x * x + y * y));
}
//! Returns the up vector from the quaternion
constexpr Vector3 NiQuaternion::GetUpVector() const noexcept {
return Vector3(2 * (x * y - w * z), 1 - 2 * (x * x + z * z), 2 * (y * z + w * x));
}
//! Returns the right vector from the quaternion
constexpr Vector3 NiQuaternion::GetRightVector() const noexcept {
return Vector3(1 - 2 * (y * y + z * z), 2 * (x * y + w * z), 2 * (x * z - w * y));
}
// MARK: Operators
//! Operator to check for equality
constexpr bool NiQuaternion::operator==(const NiQuaternion& rot) const noexcept {
return rot.x == this->x && rot.y == this->y && rot.z == this->z && rot.w == this->w;
}
//! Operator to check for inequality
constexpr bool NiQuaternion::operator!=(const NiQuaternion& rot) const noexcept {
return !(*this == rot);
}

View File

@@ -24,7 +24,7 @@ struct LocalSpaceInfo {
struct PositionUpdate {
NiPoint3 position = NiPoint3Constant::ZERO;
NiQuaternion rotation = NiQuaternionConstant::IDENTITY;
NiQuaternion rotation = QuatUtils::IDENTITY;
bool onGround = false;
bool onRail = false;
NiPoint3 velocity = NiPoint3Constant::ZERO;

View File

@@ -47,6 +47,8 @@ void dConfig::LoadConfig() {
void dConfig::ReloadConfig() {
this->m_ConfigValues.clear();
LoadConfig();
for (const auto& handler : m_ConfigHandlers) handler();
LogSettings();
}
const std::string& dConfig::GetValue(std::string key) {
@@ -58,6 +60,18 @@ const std::string& dConfig::GetValue(std::string key) {
return this->m_ConfigValues[key];
}
void dConfig::AddConfigHandler(std::function<void()> handler) {
m_ConfigHandlers.push_back(handler);
}
void dConfig::LogSettings() const {
LOG("Configuration settings:");
for (const auto& [key, value] : m_ConfigValues) {
const auto& valueLog = key.find("password") != std::string::npos ? "<HIDDEN>" : value;
LOG(" %s = %s", key.c_str(), valueLog.c_str());
}
}
void dConfig::ProcessLine(const std::string& line) {
auto splitLoc = line.find('=');
auto key = line.substr(0, splitLoc);

View File

@@ -1,5 +1,7 @@
#pragma once
#include <fstream>
#include <functional>
#include <map>
#include <string>
@@ -29,10 +31,15 @@ public:
* Reloads the config file to reset values
*/
void ReloadConfig();
// Adds a function to be called when the config is (re)loaded
void AddConfigHandler(std::function<void()> handler);
void LogSettings() const;
private:
void ProcessLine(const std::string& line);
private:
std::map<std::string, std::string> m_ConfigValues;
std::vector<std::function<void()>> m_ConfigHandlers;
std::string m_ConfigFilePath;
};

View File

@@ -3,9 +3,7 @@
namespace MessageType {
enum class Master : uint32_t {
REQUEST_PERSISTENT_ID = 1,
REQUEST_PERSISTENT_ID_RESPONSE,
REQUEST_ZONE_TRANSFER,
REQUEST_ZONE_TRANSFER = 1,
REQUEST_ZONE_TRANSFER_RESPONSE,
SERVER_INFO,
REQUEST_SESSION_KEY,

View File

@@ -18,7 +18,9 @@ enum class eCharacterVersion : uint32_t {
SPEED_BASE,
// Fixes nexus force explorer missions
NJ_JAYMISSIONS,
UP_TO_DATE, // will become NEXUS_FORCE_EXPLORER
NEXUS_FORCE_EXPLORER, // Fixes pet ids in player inventories
PET_IDS, // Fixes pet ids in player inventories
UP_TO_DATE, // will become INVENTORY_PERSISTENT_IDS
};
#endif //!__ECHARACTERVERSION__H__

View File

@@ -50,7 +50,10 @@ enum class eMissionState : int {
/**
* The mission has been completed before and has now been completed again. Used for daily missions.
*/
COMPLETE_READY_TO_COMPLETE = 12
COMPLETE_READY_TO_COMPLETE = 12,
// The mission is failed (don't know where this is used)
FAILED = 16,
};
#endif //!__MISSIONSTATE__H__

View File

@@ -1,13 +1,12 @@
#ifndef __EOBJECTBITS__H__
#define __EOBJECTBITS__H__
#ifndef EOBJECTBITS_H
#define EOBJECTBITS_H
#include <cstdint>
enum class eObjectBits : size_t {
PERSISTENT = 32,
CLIENT = 46,
SPAWNED = 58,
CHARACTER = 60
};
#endif //!__EOBJECTBITS__H__
#endif //!EOBJECTBITS_H

View File

@@ -0,0 +1,55 @@
add_subdirectory(blueprints)
set(DDASHBOARDSERVER_SOURCES
"DashboardWeb.cpp"
# Explicitly include blueprint sources to ensure they are compiled into the library
"blueprints/AuthBlueprint.cpp"
"blueprints/ApiBlueprint.cpp"
"blueprints/PageBlueprint.cpp"
"blueprints/PlayKeysBlueprint.cpp"
"blueprints/CharactersBlueprint.cpp"
"blueprints/MailBlueprint.cpp"
"blueprints/BugReportsBlueprint.cpp"
"blueprints/ModerationBlueprint.cpp"
)
# Create dDashboardServer library
add_library(dDashboardServer ${DDASHBOARDSERVER_SOURCES})
target_include_directories(dDashboardServer PRIVATE ${PROJECT_SOURCE_DIR}/dServer)
find_package(CURL)
if (CURL_FOUND)
target_link_libraries(dDashboardServer ${COMMON_LIBRARIES} dServer Crow::Crow bcrypt CURL::libcurl)
else()
message(WARNING "libcurl not found; building dDashboardServer without CURL::libcurl. Some features may be disabled.")
target_link_libraries(dDashboardServer ${COMMON_LIBRARIES} dServer Crow::Crow bcrypt)
endif()
add_executable(DashboardServer "DashboardServer.cpp")
if (CURL_FOUND)
target_link_libraries(DashboardServer ${COMMON_LIBRARIES} dServer Crow::Crow bcrypt CURL::libcurl dDashboardServer)
else()
target_link_libraries(DashboardServer ${COMMON_LIBRARIES} dServer Crow::Crow bcrypt dDashboardServer)
endif()
target_include_directories(DashboardServer PRIVATE ${PROJECT_SOURCE_DIR}/dServer)
add_compile_definitions(DashboardServer PRIVATE PROJECT_VERSION="\"${PROJECT_VERSION}\"")
# Define Windows version for ASIO/Crow compatibility (Windows 10)
if(WIN32)
target_compile_definitions(DashboardServer PRIVATE _WIN32_WINNT=0x0A00)
target_compile_definitions(dDashboardServer PRIVATE _WIN32_WINNT=0x0A00)
endif()
# Copy static files and templates to build directory
add_custom_command(TARGET DashboardServer POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_CURRENT_SOURCE_DIR}/static
$<TARGET_FILE_DIR:DashboardServer>/static
COMMENT "Copying static files to build directory"
)
add_custom_command(TARGET DashboardServer POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_CURRENT_SOURCE_DIR}/templates
$<TARGET_FILE_DIR:DashboardServer>/templates
COMMENT "Copying templates to build directory"
)

View File

@@ -0,0 +1,33 @@
#include "DashboardHelpers.h"
namespace DashboardHelpers {
DataTablesParams ParseDataTablesParams(const crow::request& req) {
DataTablesParams p;
try {
if (req.url_params.get("draw")) p.draw = std::stoi(req.url_params.get("draw"));
if (req.url_params.get("start")) p.start = std::stoi(req.url_params.get("start"));
if (req.url_params.get("length")) p.length = std::stoi(req.url_params.get("length"));
if (req.url_params.get("order[0][column]")) p.orderColumn = std::stoi(req.url_params.get("order[0][column]"));
if (req.url_params.get("order[0][dir]")) p.orderDir = req.url_params.get("order[0][dir]");
} catch (...) {
// ignore parse errors, return defaults
}
return p;
}
crow::json::wvalue CreateDataTablesResponse(int draw, uint32_t recordsTotal, uint32_t recordsFiltered, const crow::json::wvalue::list& data) {
crow::json::wvalue resp;
resp["draw"] = draw;
resp["recordsTotal"] = recordsTotal;
resp["recordsFiltered"] = recordsFiltered;
resp["data"] = data;
return resp;
}
bool RescueCharacter(const uint64_t characterId, const uint32_t zoneId) {
// Minimal stub: not implemented here. Return false to indicate no-op.
return false;
}
} // namespace DashboardHelpers

View File

@@ -0,0 +1,24 @@
#pragma once
#include <crow.h>
#include <string>
namespace DashboardHelpers {
struct DataTablesParams {
int draw{0};
int start{0};
int length{10};
int orderColumn{-1};
std::string orderDir{"asc"};
};
// Parse common DataTables GET params from the request
DataTablesParams ParseDataTablesParams(const crow::request& req);
// Create a DataTables response object
crow::json::wvalue CreateDataTablesResponse(int draw, uint32_t recordsTotal, uint32_t recordsFiltered, const crow::json::wvalue::list& data);
// Rescue character stub (real logic may be project-specific)
bool RescueCharacter(const uint64_t characterId, const uint32_t zoneId);
}

View File

@@ -0,0 +1,248 @@
#ifndef PROJECT_VERSION
#define PROJECT_VERSION "dev"
#endif
#include <iostream>
#include <string>
#include <chrono>
#include <thread>
//DLU Includes:
#include "dCommonVars.h"
#include "dServer.h"
#include "Logger.h"
#include "Database.h"
#include "dConfig.h"
#include "Diagnostics.h"
#include "AssetManager.h"
#include "BinaryPathFinder.h"
#include "ServiceType.h"
#include "StringifiedEnum.h"
#include "Game.h"
#include "Server.h"
//RakNet includes:
#include "RakNetDefines.h"
#include "MessageIdentifiers.h"
#include "MessageType/Server.h"
#include "DashboardWeb.h"
#include "DashboardShared.h"
namespace Game {
Logger* logger = nullptr;
dServer* server = nullptr;
dConfig* config = nullptr;
AssetManager* assetManager = nullptr;
Game::signal_t lastSignal = 0;
std::mt19937 randomEngine;
}
// Forward declaration
void HandlePacket(Packet* packet);
int main(int argc, char** argv) {
constexpr uint32_t dashboardFramerate = mediumFramerate;
constexpr uint32_t dashboardFrameDelta = mediumFrameDelta;
Diagnostics::SetProcessName("Dashboard");
Diagnostics::SetProcessFileName(argv[0]);
Diagnostics::Initialize();
std::signal(SIGINT, Game::OnSignal);
std::signal(SIGTERM, Game::OnSignal);
Game::config = new dConfig("dashboardconfig.ini");
//Create all the objects we need to run our service:
Server::SetupLogger("DashboardServer");
if (!Game::logger) return EXIT_FAILURE;
Game::config->LogSettings();
//Read our config:
LOG("Starting Dashboard server...");
LOG("Version: %s", PROJECT_VERSION);
LOG("Compiled on: %s", __TIMESTAMP__);
try {
std::string clientPathStr = Game::config->GetValue("client_location");
if (clientPathStr.empty()) clientPathStr = "./res";
std::filesystem::path clientPath = std::filesystem::path(clientPathStr);
if (clientPath.is_relative()) {
clientPath = BinaryPathFinder::GetBinaryDir() / clientPath;
}
Game::assetManager = new AssetManager(clientPath);
} catch (std::runtime_error& ex) {
LOG("Got an error while setting up assets: %s", ex.what());
delete Game::logger;
delete Game::config;
return EXIT_FAILURE;
}
//Connect to the Database
try {
Database::Connect();
} catch (std::exception& ex) {
LOG("Got an error while connecting to the database: %s", ex.what());
Database::Destroy("DashboardServer");
delete Game::logger;
delete Game::config;
return EXIT_FAILURE;
}
// Setup and start the Crow web server (runs in its own thread)
const uint32_t web_server_port = GeneralUtils::TryParse<uint32_t>(Game::config->GetValue("web_server_port")).value_or(8080);
DashboardWeb::Initialize(web_server_port);
//Find out the master's IP:
std::string masterIP;
uint32_t masterPort = 1000;
std::string masterPassword;
auto masterInfo = Database::Get()->GetMasterInfo();
if (masterInfo) {
masterIP = masterInfo->ip;
masterPort = masterInfo->port;
masterPassword = masterInfo->password;
}
//It's safe to pass 'localhost' here, as the IP is only used as the external IP.
std::string ourIP = "localhost";
const uint32_t maxClients = GeneralUtils::TryParse<uint32_t>(Game::config->GetValue("max_clients")).value_or(999);
const uint32_t ourPort = GeneralUtils::TryParse<uint32_t>(Game::config->GetValue("dashboard_server_port")).value_or(2006);
const auto externalIPString = Game::config->GetValue("external_ip");
if (!externalIPString.empty()) ourIP = externalIPString;
Game::server = new dServer(ourIP, ourPort, 0, maxClients, false, true, Game::logger, masterIP, masterPort, ServiceType::COMMON, Game::config, &Game::lastSignal, masterPassword);
// Update shared state with master server info
DashboardShared::g_Stats.SetMasterInfo(masterIP, masterPort);
Game::randomEngine = std::mt19937(time(0));
//Run it until server gets a kill message from Master:
auto t = std::chrono::high_resolution_clock::now();
Packet* packet = nullptr;
constexpr uint32_t logFlushTime = 30 * dashboardFramerate; // 30 seconds in frames
constexpr uint32_t sqlPingTime = 10 * 60 * dashboardFramerate; // 10 minutes in frames
uint32_t framesSinceLastFlush = 0;
uint32_t framesSinceMasterDisconnect = 0;
uint32_t framesSinceLastSQLPing = 0;
auto lastTime = std::chrono::high_resolution_clock::now();
auto startTime = lastTime; // Track server start time for uptime
Game::logger->Flush(); // once immediately before main loop
while (!Game::ShouldShutdown()) {
// Check if we're still connected to master:
if (!Game::server->GetIsConnectedToMaster()) {
framesSinceMasterDisconnect++;
if (framesSinceMasterDisconnect >= dashboardFramerate)
break; //Exit our loop, shut down.
DashboardShared::SetMasterConnected(false);
} else {
framesSinceMasterDisconnect = 0;
DashboardShared::SetMasterConnected(true);
}
const auto currentTime = std::chrono::high_resolution_clock::now();
const float deltaTime = std::chrono::duration<float>(currentTime - lastTime).count();
lastTime = currentTime;
// Check for packets from master:
Game::server->ReceiveFromMaster();
// Process queued packet sends from Crow threads
if (DashboardShared::g_PacketQueue.HasPending()) {
auto pendingPackets = DashboardShared::g_PacketQueue.DequeueAll();
for (const auto& request : pendingPackets) {
// Create BitStream from queued data
RakNet::BitStream bitStream(const_cast<unsigned char*>(request.data.data()), request.data.size(), false);
// Send via RakNet (safe - we're in the RakNet thread)
Game::server->Send(bitStream, request.target, request.broadcast);
DashboardShared::OnPacketSent();
LOG("Sent queued packet from web request (%zu bytes)", request.data.size());
}
}
// Check for RakNet packets:
packet = Game::server->Receive();
if (packet) {
HandlePacket(packet);
DashboardShared::OnPacketReceived(); // Update shared stats
Game::server->DeallocatePacket(packet);
packet = nullptr;
}
//Push our log every 30s:
if (framesSinceLastFlush >= logFlushTime) {
Game::logger->Flush();
framesSinceLastFlush = 0;
} else framesSinceLastFlush++;
//Every 10 min we ping our sql server to keep it alive hopefully:
if (framesSinceLastSQLPing >= sqlPingTime) {
//Find out the master's IP for absolutely no reason:
std::string masterIP;
uint32_t masterPort;
auto masterInfo = Database::Get()->GetMasterInfo();
if (masterInfo) {
masterIP = masterInfo->ip;
masterPort = masterInfo->port;
}
framesSinceLastSQLPing = 0;
} else framesSinceLastSQLPing++;
//Sleep our thread since dashboard can afford to.
t += std::chrono::milliseconds(dashboardFrameDelta);
std::this_thread::sleep_until(t);
}
// Stop the Crow web server
DashboardWeb::Stop();
//Delete our objects here:
Database::Destroy("DashboardServer");
delete Game::server;
delete Game::logger;
delete Game::config;
return EXIT_SUCCESS;
}
void HandlePacket(Packet* packet) {
if (packet->length < 4) return;
if (packet->data[0] == ID_DISCONNECTION_NOTIFICATION || packet->data[0] == ID_CONNECTION_LOST) {
LOG("A client has disconnected");
DashboardShared::OnClientDisconnected();
return;
}
if (packet->data[0] == ID_NEW_INCOMING_CONNECTION) {
LOG("New incoming connection from %s", packet->systemAddress.ToString());
DashboardShared::OnClientConnected();
return;
}
if (packet->data[0] != ID_USER_PACKET_ENUM) return;
// Handle server packets
if (static_cast<ServiceType>(packet->data[1]) == ServiceType::COMMON) {
if (static_cast<MessageType::Server>(packet->data[3]) == MessageType::Server::VERSION_CONFIRM) {
LOG("Version confirmation received from client");
DashboardShared::OnPacketReceived("VERSION_CONFIRM");
}
}
// Add more packet handling as needed
// This is where you would handle custom dashboard-specific packets
// All packet handling can safely update DashboardShared state
}

View File

@@ -0,0 +1,187 @@
#ifndef __DASHBOARDSHARED_H__
#define __DASHBOARDSHARED_H__
#include <atomic>
#include <mutex>
#include <string>
#include <vector>
#include <queue>
#include <functional>
#include <set>
#include <map>
#include <ctime>
#include <random>
#include <optional>
#include "dCommonVars.h"
#include "RakNetTypes.h"
#include "GameDatabase.h"
#include "crow.h"
// Forward declaration
class GameDatabase;
namespace RakNet {
class BitStream;
};
/**
* Shared state between the Crow web server (runs in background threads)
* and the RakNet game loop (runs in main thread).
*
* All members use thread-safe types (atomic, mutex-protected)
*
* IMPORTANT: RakNet is NOT thread-safe!
* - Crow threads can READ state and QUEUE packet send requests
* - Only the RakNet thread (main loop) can actually send packets
*/
namespace DashboardShared {
// ===== Atomic Counters (lock-free, safe for simple reads/writes) =====
inline std::atomic<uint32_t> g_ConnectedClients{0};
inline std::atomic<bool> g_ConnectedToMaster{false};
inline std::atomic<uint64_t> g_PacketsReceived{0};
inline std::atomic<uint64_t> g_PacketsSent{0};
// ===== Mutex-Protected Data (for complex structures) =====
struct ServerStats {
std::mutex mutex;
uint64_t uptime_seconds = 0;
std::string last_packet_type;
uint32_t raknet_port = 0;
std::string master_ip;
// Thread-safe getters
uint64_t GetUptime() {
std::lock_guard<std::mutex> lock(mutex);
return uptime_seconds;
}
std::string GetLastPacketType() {
std::lock_guard<std::mutex> lock(mutex);
return last_packet_type;
}
void SetLastPacketType(const std::string& type) {
std::lock_guard<std::mutex> lock(mutex);
last_packet_type = type;
}
void SetMasterInfo(const std::string& ip, uint32_t port) {
std::lock_guard<std::mutex> lock(mutex);
master_ip = ip;
raknet_port = port;
}
};
inline ServerStats g_Stats;
// ===== Packet Send Queue (for Crow -> RakNet communication) =====
/**
* Represents a packet send request from Crow to RakNet.
* Crow threads add to the queue, RakNet thread processes them.
*/
struct PacketSendRequest {
std::vector<uint8_t> data; // Packet data (owns the memory)
SystemAddress target; // Target address (or UNASSIGNED for broadcast)
bool broadcast; // Whether to broadcast
PacketSendRequest(const std::vector<uint8_t>& packetData,
const SystemAddress& addr,
bool isBroadcast)
: data(packetData), target(addr), broadcast(isBroadcast) {}
};
// Thread-safe queue of packet send requests
struct PacketQueue {
std::mutex mutex;
std::queue<PacketSendRequest> queue;
// Called from Crow threads to queue a packet for sending
void Enqueue(const std::vector<uint8_t>& data, const SystemAddress& addr, bool broadcast) {
std::lock_guard<std::mutex> lock(mutex);
queue.emplace(data, addr, broadcast);
}
// Called from RakNet thread to get all pending packets
std::vector<PacketSendRequest> DequeueAll() {
std::lock_guard<std::mutex> lock(mutex);
std::vector<PacketSendRequest> result;
while (!queue.empty()) {
result.push_back(std::move(queue.front()));
queue.pop();
}
return result;
}
// Check if queue has pending packets
bool HasPending() {
std::lock_guard<std::mutex> lock(mutex);
return !queue.empty();
}
};
inline PacketQueue g_PacketQueue;
// ===== Helper Functions =====
// Called from RakNet thread when a client connects
inline void OnClientConnected() {
g_ConnectedClients++;
}
// Called from RakNet thread when a client disconnects
inline void OnClientDisconnected() {
if (g_ConnectedClients > 0) {
g_ConnectedClients--;
}
}
// Called from RakNet thread when master connection status changes
inline void SetMasterConnected(bool connected) {
g_ConnectedToMaster = connected;
}
// Called from RakNet thread when a packet is processed
inline void OnPacketReceived(const std::string& packetType = "") {
g_PacketsReceived++;
if (!packetType.empty()) {
g_Stats.SetLastPacketType(packetType);
}
}
// Called from RakNet thread when a packet is sent
inline void OnPacketSent() {
g_PacketsSent++;
}
// ===== Crow -> RakNet Communication =====
/**
* Queue a RakNet packet to be sent (called from Crow threads).
* The packet will be sent on the next RakNet thread update.
*
* @param data Packet data to send
* @param target Target system address (use UNASSIGNED_SYSTEM_ADDRESS for broadcast)
* @param broadcast Whether to broadcast to all connected clients
*/
inline void QueuePacketSend(const std::vector<uint8_t>& data,
const SystemAddress& target = UNASSIGNED_SYSTEM_ADDRESS,
bool broadcast = false) {
g_PacketQueue.Enqueue(data, target, broadcast);
}
/**
* Helper to queue a BitStream for sending (called from Crow threads).
* Converts BitStream to raw data and queues it.
*/
inline void QueueBitStreamSend(RakNet::BitStream& bitStream,
const SystemAddress& target = UNASSIGNED_SYSTEM_ADDRESS,
bool broadcast = false) {
std::vector<uint8_t> data(bitStream.GetData(),
bitStream.GetData() + bitStream.GetNumberOfBytesUsed());
QueuePacketSend(data, target, broadcast);
}
}
#endif // __DASHBOARDSHARED_H__

View File

@@ -0,0 +1,153 @@
#include "DashboardWeb.h"
#include "DashboardShared.h"
// Blueprint includes
#include "blueprints/AuthBlueprint.h"
#include "blueprints/ApiBlueprint.h"
#include "blueprints/PageBlueprint.h"
#include "blueprints/PlayKeysBlueprint.h"
#include "blueprints/CharactersBlueprint.h"
#include "blueprints/MailBlueprint.h"
#include "blueprints/BugReportsBlueprint.h"
#include "blueprints/ModerationBlueprint.h"
// Crow headers - must come before ASIO to avoid conflicts
#include "crow.h"
#include "crow/middlewares/session.h"
// thanks bill gates
#ifdef _WIN32
#undef min
#undef max
#endif
#include <memory>
#include <thread>
#include <chrono>
#include <iostream>
namespace DashboardWeb {
using Session = crow::SessionMiddleware<crow::InMemoryStore>;
static crow::App<crow::CookieParser, Session> g_App {
Session{
// cookie config: use "session" cookie name, 24h max_age
crow::CookieParser::Cookie("session").max_age(24 * 60 * 60).path("/"),
// session id length
32,
// storage backend (InMemoryStore)
crow::InMemoryStore{}
}
};
static std::future<void> g_ServerFuture;
static bool g_Running = false;
static bool g_Initialized = false;
void SetupRoutes() {
static bool setupCalled = false;
if (setupCalled) {
std::cerr << "WARNING: SetupRoutes() called multiple times!" << std::endl;
return;
}
setupCalled = true;
std::cerr << "Setting up dashboard routes..." << std::endl;
// Set mustache template base directory
crow::mustache::set_base("./templates");
// Setup all blueprint routes
try {
std::cerr << " - Setting up AuthBlueprint..." << std::endl;
AuthBlueprint::Setup(g_App);
std::cerr << " - Setting up ApiBlueprint..." << std::endl;
ApiBlueprint::Setup(g_App);
std::cerr << " - Setting up PageBlueprint..." << std::endl;
PageBlueprint::Setup(g_App);
std::cerr << " - Setting up PlayKeysBlueprint..." << std::endl;
PlayKeysBlueprint::Setup(g_App);
std::cerr << " - Setting up CharactersBlueprint..." << std::endl;
CharactersBlueprint::Setup(g_App);
std::cerr << " - Setting up MailBlueprint..." << std::endl;
MailBlueprint::Setup(g_App);
std::cerr << " - Setting up BugReportsBlueprint..." << std::endl;
BugReportsBlueprint::Setup(g_App);
std::cerr << " - Setting up ModerationBlueprint..." << std::endl;
ModerationBlueprint::Setup(g_App);
std::cerr << "All routes set up successfully!" << std::endl;
} catch (const std::exception& e) {
// Print to stderr since LOG might not be available
std::cerr << "Error setting up routes: " << e.what() << std::endl;
throw;
}
}
void Initialize(uint32_t port) {
// Only allow initialization once per process lifetime
// Crow apps cannot be restarted once stopped
if (g_Initialized) {
std::cerr << "Dashboard web server already initialized. Cannot reinitialize." << std::endl;
return;
}
try {
// Setup routes (only happens once)
SetupRoutes();
// Configure Crow app
g_App.loglevel(crow::LogLevel::Info); // Changed to Info to see startup messages
// Start the server in a separate thread
g_ServerFuture = std::async(std::launch::async, [port]() {
try {
g_App.port(port).multithreaded().run();
} catch (const std::exception& e) {
std::cerr << "Error running Crow server: " << e.what() << std::endl;
}
});
g_Running = true;
g_Initialized = true;
// Give the server a moment to start
std::this_thread::sleep_for(std::chrono::milliseconds(500));
} catch (const std::exception& e) {
std::cerr << "Error initializing dashboard web server: " << e.what() << std::endl;
throw;
}
}
void Update() {
// Crow runs in its own thread, nothing to update here
}
void Stop() {
if (!g_Running) {
return;
}
g_App.stop();
// Wait for the server thread to finish (with timeout)
if (g_ServerFuture.valid()) {
auto status = g_ServerFuture.wait_for(std::chrono::seconds(5));
if (status == std::future_status::timeout) {
std::cerr << "Warning: Dashboard web server did not stop gracefully" << std::endl;
}
}
g_Running = false;
}
} // namespace DashboardWeb

View File

@@ -0,0 +1,19 @@
#ifndef __DASHBOARDWEB_H__
#define __DASHBOARDWEB_H__
#include <cstdint>
#include <string>
namespace DashboardWeb {
// Initialize the web server and configure routes using blueprints
void Initialize(uint32_t port);
// Process pending web requests (call each frame/tick)
void Update();
// Stop the web server
void Stop();
};
#endif // __DASHBOARDWEB_H__

View File

@@ -0,0 +1,143 @@
<!doctype html>
<html lang='en'>
<head>
<!-- Title -->
<title>{{#title}}{{title}}{{/title}}{{^title}}Dashboard{{/title}} - {{config.APP_NAME}}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
{{! CSS }}
<style>
.required:after {
content:" *";
color: red;
}
.error {
color: red;
}
</style>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Bootstrap Icons -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
<!-- DataTables CSS -->
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
<!-- Custom CSS consolidated -->
<link rel="stylesheet" href="/static/css/dashboard.css">
</head>
<body class="bg-dark text-white">
{{> header}}
<!-- Content -->
<div class="container py-0">
<!-- Text -->
<div class="text-center">
<span class="h3 mb-0"><br/>{{content_before}}<br/><br/></span>
</div>
<!-- Flashed messages: expect `messages` to be an array of {category, message} -->
{! TODO: make this dynamic toasts !!}
{{#messages}}
<div class="alert alert-{{category}}" role="alert">
{{message}}
</div>
{{/messages}}
</div>
<div class='container mt-4'>
{{content}}
</div>
<div class='container mt-4'>
{{content_after}}
</div>
<footer>
{{#footer}}
<hr class="my-5"/>
{{/footer}}
</footer>
{{! JS assets }}
<!-- Bootstrap JS Bundle -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<!-- jQuery (optional fallback for older scripts) -->
<script src="https://code.jquery.com/jquery-3.7.0.min.js"></script>
<!-- DataTables JS -->
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
<!-- Shared helper: wait for jQuery/DataTables (keeps pages resilient to CDN timing) -->
<script src="/static/js/wait-for-jq-dt.js"></script>
<!-- Chart.js -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<!-- Custom JS -->
<script src="/static/js/api.js"></script>
<script src="/static/js/dashboard.js"></script>
<script src="/static/js/login.js"></script>
<script>
// set the active nav-link item (use vanilla JS, fallback to jQuery)
(function(){
var endpoint = '{{request_endpoint}}' || '';
try{
var target_nav = '#' + endpoint.replace(/\./g, '-');
var el = document.querySelector(target_nav);
if(el) el.classList.add('active');
else if(window.jQuery) $(target_nav).addClass('active');
}catch(e){}
})();
// initialize Bootstrap 5 tooltips (no jQuery required)
(function(){
try{
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipTriggerList.forEach(function (tooltipTriggerEl) {
new bootstrap.Tooltip(tooltipTriggerEl);
});
}catch(e){
// fallback for legacy attribute name if still used
// legacy jQuery tooltip fallback (only runs if bootstrap init failed and jQuery tooltip is present)
if(window.jQuery && window.jQuery.fn && window.jQuery.fn.tooltip) $(function(){ $('[data-toggle="tooltip"]').tooltip(); });
}
})();
function setInnerHTML(elm, html) {
elm.innerHTML = html;
// re-init Bootstrap tooltips inside newly injected content
try{
var tooltipTriggerList = [].slice.call(elm.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipTriggerList.forEach(function (tooltipTriggerEl) {
new bootstrap.Tooltip(tooltipTriggerEl);
});
}catch(e){
if(window.jQuery && window.jQuery.fn && window.jQuery.fn.tooltip) $("body").tooltip({ selector: '[data-toggle=tooltip]' });
}
Array.from(elm.querySelectorAll("script")).forEach(function(oldScriptEl) {
var newScriptEl = document.createElement("script");
Array.from(oldScriptEl.attributes).forEach(function(attr) {
newScriptEl.setAttribute(attr.name, attr.value);
});
var scriptText = document.createTextNode(oldScriptEl.innerHTML || '');
newScriptEl.appendChild(scriptText);
oldScriptEl.parentNode.replaceChild(newScriptEl, oldScriptEl);
});
}
</script>
</body>
</html>

View File

@@ -0,0 +1,113 @@
{{! Navigation brand, nav toggle bar }}
<nav class='navbar navbar-expand-sm navbar-dark bg-primary flex-row pb-3'>
<div class='container md-0 flex-nowrap'>
{{! Logo and App Name }}
<nav class="navbar">
<a class="navbar-brand" href="{{url.main_index}}">
<img src="{{static.logo}}" width="30" height="30" class="d-inline-block align-top" alt="">
{{config.APP_NAME}}
</a>
</nav>
{{! Navigation brand, nav toggle bar }}
<nav class='navbar navbar-expand-sm navbar-dark bg-primary flex-row pb-3'>
<div class='container md-0 flex-nowrap'>
{{! Logo and App Name }}
<nav class="navbar">
<a class="navbar-brand" href="{{url.main_index}}">
<img src="{{static.logo}}" width="30" height="30" class="d-inline-block align-top" alt="">
{{config.APP_NAME}}
</a>
</nav>
{{! Visible only on large devices }}
<nav class='navbar-nav'>
<div class='collapse navbar-collapse'>
{{#current_user_authenticated}}
{{#USER_ENABLE_INVITE_USER}}
<a class='btn-nav-dashboard me-2' href='{{url.user_invite_user}}'>Invite</a>
{{/USER_ENABLE_INVITE_USER}}
<a class='btn-nav-dashboard' href='{{url.user_logout}}'><i class='fas fa-sign-out-alt me-1'></i>Logout</a>
{{/current_user_authenticated}}
</div>
</nav>
<button class='navbar-toggler' type='button' data-bs-toggle='collapse' data-bs-target='#navbarSupportedContent' aria-controls='navbarSupportedContent' aria-expanded='false' aria-label='Toggle navigation'>
<span class='navbar-toggler-icon'></span>
</button>
</div>
</nav>
{{! Navigation menu / links bar }}
<nav class='navbar navbar-expand-sm navbar-dark bg-primary p-sm-0 py-0 {{#navbar_shadow}}shadow-sm{{/navbar_shadow}}'>
<div class='container mt-0 pt-0'>
<div class='collapse navbar-collapse' id='navbarSupportedContent' style='margin-top: -16px;'>
<nav class='navbar-nav me-auto'>
<a id='main-index' class='nav-link' href='{{url.main_index}}'>Home</a>
{{#gm_ge_3}}
{{! General Moderation Links }}
<a id='accounts-index' class='nav-link' href='{{url.accounts_index}}'>Accounts</a>
<a id='character-index' class='nav-link' href='{{url.characters_index}}'>Characters</a>
<a id='property-index' class='nav-link' href='{{url.properties_index}}'>Properties</a>
{{/gm_ge_3}}
{{#gm_ge_5_require_play_key}}
{{! Play Keys }}
<a id='play_keys-index' class='nav-link' href='{{url.play_keys_index}}'>Play Keys</a>
{{/gm_ge_5_require_play_key}}
{{#gm_ge_2}}
<a id='report-index' class='nav-link' href='{{url.reports_index}}'>Reports</a>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false">Tools</a>
<div class="dropdown-menu">
<a class="dropdown-item text-center" href='{{url.mail_send}}'>Send Mail</a>
<hr/>
<h3 class="text-center">Moderation</h3>
<a class="dropdown-item text-center" href='{{url.moderation_unapproved}}'>Unapproved Items</a>
<a class="dropdown-item text-center" href='{{url.moderation_approved}}'>Approved Items</a>
<a class="dropdown-item text-center" href='{{url.moderation_all}}'>All Items</a>
<hr/>
<h3 class="text-center">Bug Reports</h3>
<a class="dropdown-item text-center" href='{{url.bug_reports_unresolved}}'>Unresolved Reports</a>
<a class="dropdown-item text-center" href='{{url.bug_reports_resolved}}'>Resolved Reports</a>
<a class="dropdown-item text-center" href='{{url.bug_reports_all}}'>All Reports</a>
{{#gm_ge_8}}
<hr/>
<h3 class="text-center">Logs</h3>
<a class="dropdown-item text-center" href='{{url.log_command}}'>Command Log</a>
<a class="dropdown-item text-center" href='{{url.log_activity}}'>Activity Log</a>
<a class="dropdown-item text-center" href='{{url.log_audit}}'>Audit Log</a>
<a class="dropdown-item text-center" href='{{url.log_system}}'>System Log</a>
{{/gm_ge_8}}
</div>
</li>
{{/gm_ge_2}}
{{#gm_eq_0}}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false">Bug Reports</a>
<div class="dropdown-menu">
<a class="dropdown-item text-center" href='{{url.bug_reports_unresolved}}'>Unresolved Reports</a>
<a class="dropdown-item text-center" href='{{url.bug_reports_resolved}}'>Resolved Reports</a>
<a class="dropdown-item text-center" href='{{url.bug_reports_all}}'>All Reports</a>
</div>
</li>
{{/gm_eq_0}}
{{#current_user_authenticated}}
<a id='main-about' class='nav-link' href='{{url.main_about}}'>About</a>
{{/current_user_authenticated}}
{{#current_user_authenticated}}
<a class='nav-link d-sm-none' href='{{url.user_logout}}'><i class='fas fa-sign-out-alt me-1'></i>Logout</a>
{{/current_user_authenticated}}
</nav>
</div>
</div>
</nav>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,17 @@
#pragma once
#include "crow.h"
#include "crow/middlewares/session.h"
namespace ApiBlueprint {
using Session = crow::SessionMiddleware<crow::InMemoryStore>;
using DashboardApp = crow::App<crow::CookieParser, Session>;
/**
* Setup API routes
* Registers all API endpoints for stats, accounts, and moderation
*/
void Setup(DashboardApp& app);
} // namespace ApiBlueprint

View File

@@ -0,0 +1,129 @@
#include "AuthBlueprint.h"
#include "Database.h"
#include <bcrypt/BCrypt.hpp>
namespace AuthBlueprint {
void Setup(DashboardApp& app) {
// Login route
CROW_ROUTE(app, "/api/login")
.methods("POST"_method)
([&](crow::request& req, crow::response& res) {
auto body = crow::json::load(req.body);
if (!body) {
res.code = 400;
res.set_header("Content-Type", "application/json");
res.write("{\"error\": \"Invalid JSON\"}");
res.end();
return;
}
std::string username = body["username"].s();
std::string password = body["password"].s();
if (username.empty() || password.empty()) {
res.code = 400;
res.set_header("Content-Type", "application/json");
res.write("{\"error\": \"Username and password required\"}");
res.end();
return;
}
// Get account info from database
auto accountInfo = Database::Get()->GetAccountInfo(username);
if (!accountInfo) {
res.code = 401;
res.set_header("Content-Type", "application/json");
res.write("{\"error\": \"Invalid credentials\"}");
res.end();
return;
}
// Verify password using bcrypt
if (!BCrypt::validatePassword(password, accountInfo->bcryptPassword)) {
res.code = 401;
res.set_header("Content-Type", "application/json");
res.write("{\"error\": \"Invalid credentials\"}");
res.end();
return;
}
// Check if account is banned or locked
if (accountInfo->banned) {
res.code = 403;
res.set_header("Content-Type", "application/json");
res.write("{\"error\": \"Account is banned\"}");
res.end();
return;
}
if (accountInfo->locked) {
res.code = 403;
res.set_header("Content-Type", "application/json");
res.write("{\"error\": \"Account is locked\"}");
res.end();
return;
}
// Create session
auto& session = app.get_context<Session>(req);
session.set("username", username);
session.set("account_id", static_cast<int>(accountInfo->id));
session.set("gm_level", static_cast<int>(accountInfo->maxGmLevel));
// Return success with user info
crow::json::wvalue response;
response["success"] = true;
response["username"] = username;
response["account_id"] = accountInfo->id;
response["gm_level"] = static_cast<uint8_t>(accountInfo->maxGmLevel);
res.set_header("Content-Type", "application/json");
res.write(response.dump());
res.end();
});
// Logout route
CROW_ROUTE(app, "/api/logout")
.methods("POST"_method)
([&](crow::request& req, crow::response& res) {
auto& session = app.get_context<Session>(req);
// Clear session
session.remove("username");
session.remove("account_id");
session.remove("gm_level");
crow::json::wvalue response;
response["success"] = true;
res.set_header("Content-Type", "application/json");
res.write(response.dump());
res.end();
});
// Auth status route
CROW_ROUTE(app, "/api/auth/status")
([&](const crow::request& req) {
auto& session = app.get_context<Session>(const_cast<crow::request&>(req));
std::string username = session.template get<std::string>("username");
crow::json::wvalue response;
if (!username.empty()) {
int account_id = session.template get<int>("account_id", -1);
int gm_level = session.template get<int>("gm_level", -1);
response["authenticated"] = true;
response["username"] = username;
response["account_id"] = account_id;
response["gm_level"] = gm_level;
} else {
response["authenticated"] = false;
}
return crow::response(response);
});
}
} // namespace AuthBlueprint

View File

@@ -0,0 +1,17 @@
#pragma once
#include "crow.h"
#include "crow/middlewares/session.h"
namespace AuthBlueprint {
using Session = crow::SessionMiddleware<crow::InMemoryStore>;
using DashboardApp = crow::App<crow::CookieParser, Session>;
/**
* Setup authentication routes
* Registers login, logout, and auth status endpoints
*/
void Setup(DashboardApp& app);
} // namespace AuthBlueprint

View File

@@ -0,0 +1,234 @@
#include "BugReportsBlueprint.h"
#include "Database.h"
#include "eGameMasterLevel.h"
#include "Logger.h"
#include <ctime>
namespace BugReportsBlueprint {
// Helper function to get current user's account info from session
std::optional<IAccounts::Info> GetCurrentUser(const crow::request& req, DashboardApp& app) {
auto& session = app.get_context<Session>(const_cast<crow::request&>(req));
std::string username = session.template get<std::string>("username");
if (username.empty()) {
return std::nullopt;
}
return Database::Get()->GetAccountInfo(username);
}
// Helper function to get user's GM level
eGameMasterLevel GetUserGMLevel(const crow::request& req, DashboardApp& app) {
auto user = GetCurrentUser(req, app);
if (!user) {
return eGameMasterLevel::CIVILIAN;
}
return user->maxGmLevel;
}
// Helper function to check if user has minimum GM level
bool HasMinimumGMLevel(const crow::request& req, DashboardApp& app, eGameMasterLevel required) {
auto level = GetUserGMLevel(req, app);
return static_cast<uint8_t>(level) >= static_cast<uint8_t>(required);
}
void Setup(DashboardApp& app) {
// Get all bug reports (filtered by status)
CROW_ROUTE(app, "/api/bugreports")
.methods("GET"_method)
([&](const crow::request& req) {
// Anyone authenticated can view their own bug reports
// GMs can view all
auto user = GetCurrentUser(req, app);
if (!user) {
return crow::response(401, "{\"error\": \"Not authenticated\"}");
}
crow::json::wvalue response;
crow::json::wvalue::list data;
try {
auto statusParam = req.url_params.get("status");
std::string status = statusParam ? statusParam : "all";
std::vector<IBugReports::DetailedInfo> reports;
if (status == "resolved") {
reports = Database::Get()->GetResolvedBugReports();
} else if (status == "unresolved") {
reports = Database::Get()->GetUnresolvedBugReports();
} else {
reports = Database::Get()->GetAllBugReports();
}
bool isGM = static_cast<uint8_t>(user->maxGmLevel) >= static_cast<uint8_t>(eGameMasterLevel::MODERATOR);
for (const auto& report : reports) {
// If not a GM, only show reports from user's own characters
if (!isGM) {
auto charInfo = Database::Get()->GetCharacterInfo(report.characterId);
if (!charInfo || charInfo->accountId != user->id) {
continue;
}
}
crow::json::wvalue item;
item["id"] = report.id;
item["body"] = report.body;
item["client_version"] = report.clientVersion;
item["other_player"] = report.otherPlayer;
item["selection"] = report.selection;
item["character_id"] = static_cast<uint64_t>(report.characterId);
item["submitted"] = report.submitted;
item["resolved_time"] = report.resolved_time;
item["resolved_by_id"] = report.resolved_by_id;
item["resolution"] = report.resolution;
// Get character name
auto charInfo = Database::Get()->GetCharacterInfo(report.characterId);
if (charInfo) {
item["character_name"] = charInfo->name;
} else {
item["character_name"] = "Unknown";
}
data.push_back(std::move(item));
}
response["data"] = std::move(data);
} catch (std::exception& ex) {
response["error"] = ex.what();
return crow::response(500, response);
}
return crow::response(response);
});
// Get a single bug report by ID
CROW_ROUTE(app, "/api/bugreports/<uint>")
.methods("GET"_method)
([&](const crow::request& req, uint64_t report_id) {
auto user = GetCurrentUser(req, app);
if (!user) {
return crow::response(401, "{\"error\": \"Not authenticated\"}");
}
crow::json::wvalue response;
try {
auto report = Database::Get()->GetBugReportById(report_id);
if (!report) {
response["success"] = false;
response["error"] = "Bug report not found";
return crow::response(404, response);
}
// Check access rights
bool canAccess = false;
if (static_cast<uint8_t>(user->maxGmLevel) >= static_cast<uint8_t>(eGameMasterLevel::MODERATOR)) {
canAccess = true;
} else {
auto charInfo = Database::Get()->GetCharacterInfo(report->characterId);
if (charInfo && charInfo->accountId == user->id) {
canAccess = true;
}
}
if (!canAccess) {
response["success"] = false;
response["error"] = "Access denied";
return crow::response(403, response);
}
response["success"] = true;
response["id"] = report->id;
response["body"] = report->body;
response["client_version"] = report->clientVersion;
response["other_player"] = report->otherPlayer;
response["selection"] = report->selection;
response["character_id"] = static_cast<uint64_t>(report->characterId);
response["submitted"] = report->submitted;
response["resolved_time"] = report->resolved_time;
response["resolved_by_id"] = report->resolved_by_id;
response["resolution"] = report->resolution;
// Get character name
auto charInfo = Database::Get()->GetCharacterInfo(report->characterId);
if (charInfo) {
response["character_name"] = charInfo->name;
}
} catch (std::exception& ex) {
response["success"] = false;
response["error"] = ex.what();
}
return crow::response(response);
});
// Resolve a bug report
CROW_ROUTE(app, "/api/bugreports/<uint>/resolve")
.methods("POST"_method)
([&](const crow::request& req, uint64_t report_id) {
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::MODERATOR)) {
return crow::response(403, "{\"error\": \"Forbidden\"}");
}
auto body = crow::json::load(req.body);
if (!body) {
return crow::response(400, "{\"error\": \"Invalid JSON\"}");
}
crow::json::wvalue response;
try {
auto user = GetCurrentUser(req, app);
if (!user) {
response["success"] = false;
response["error"] = "Not authenticated";
return crow::response(401, response);
}
std::string resolution;
if (body.has("resolution"))
resolution = std::string(body["resolution"].s());
else
resolution = "";
if (resolution.empty()) {
response["success"] = false;
response["error"] = "Resolution message is required";
return crow::response(response);
}
// Check if report exists and is not already resolved
auto report = Database::Get()->GetBugReportById(report_id);
if (!report) {
response["success"] = false;
response["error"] = "Bug report not found";
return crow::response(404, response);
}
if (report->resolved_time > 0) {
response["success"] = false;
response["error"] = "Bug report already resolved";
return crow::response(response);
}
Database::Get()->ResolveBugReport(report_id, user->id, resolution);
response["success"] = true;
response["message"] = "Bug report resolved successfully";
} catch (std::exception& ex) {
response["success"] = false;
response["error"] = ex.what();
}
return crow::response(response);
});
}
} // namespace BugReportsBlueprint

View File

@@ -0,0 +1,20 @@
#ifndef __BUGREPORTSBLUEPRINT_H__
#define __BUGREPORTSBLUEPRINT_H__
#include "crow.h"
#include "crow/middlewares/session.h"
namespace BugReportsBlueprint {
using Session = crow::SessionMiddleware<crow::InMemoryStore>;
using DashboardApp = crow::App<crow::CookieParser, Session>;
/**
* Setup bug reports management routes
* Registers routes for viewing and resolving bug reports
*/
void Setup(DashboardApp& app);
} // namespace BugReportsBlueprint
#endif // __BUGREPORTSBLUEPRINT_H__

View File

@@ -0,0 +1,14 @@
set(DDASHBOARDSERVER_BLUEPRINTS
"AuthBlueprint.cpp"
"ApiBlueprint.cpp"
"PageBlueprint.cpp"
"PlayKeysBlueprint.cpp"
"CharactersBlueprint.cpp"
"MailBlueprint.cpp"
"BugReportsBlueprint.cpp"
"ModerationBlueprint.cpp"
)
foreach(file ${DDASHBOARDSERVER_BLUEPRINTS})
set(DDASHBOARDSERVER_BLUEPRINTS_SOURCES ${DDASHBOARDSERVER_BLUEPRINTS_SOURCES} "blueprints/${file}" PARENT_SCOPE)
endforeach()

View File

@@ -0,0 +1,263 @@
#include "CharactersBlueprint.h"
#include "Database.h"
#include "eGameMasterLevel.h"
#include "ePermissionMap.h"
#include "Logger.h"
namespace CharactersBlueprint {
// Helper function to get current user's account info from session
std::optional<IAccounts::Info> GetCurrentUser(const crow::request& req, DashboardApp& app) {
auto& session = app.get_context<Session>(const_cast<crow::request&>(req));
std::string username = session.template get<std::string>("username");
if (username.empty()) {
return std::nullopt;
}
return Database::Get()->GetAccountInfo(username);
}
// Helper function to get user's GM level
eGameMasterLevel GetUserGMLevel(const crow::request& req, DashboardApp& app) {
auto user = GetCurrentUser(req, app);
if (!user) {
return eGameMasterLevel::CIVILIAN;
}
return user->maxGmLevel;
}
// Helper function to check if user has minimum GM level
bool HasMinimumGMLevel(const crow::request& req, DashboardApp& app, eGameMasterLevel required) {
auto level = GetUserGMLevel(req, app);
return static_cast<uint8_t>(level) >= static_cast<uint8_t>(required);
}
// Helper to check if user can access a character (owns it or is GM 3+)
bool CanAccessCharacter(const crow::request& req, DashboardApp& app, LWOOBJID characterId) {
auto user = GetCurrentUser(req, app);
if (!user) return false;
// GMs can access any character
if (static_cast<uint8_t>(user->maxGmLevel) >= static_cast<uint8_t>(eGameMasterLevel::MODERATOR)) {
return true;
}
// Check if user owns this character
auto charInfo = Database::Get()->GetCharacterInfo(characterId);
if (charInfo && charInfo->accountId == user->id) {
return true;
}
return false;
}
void Setup(DashboardApp& app) {
// Get character by ID
CROW_ROUTE(app, "/api/characters/<uint>")
.methods("GET"_method)
([&](const crow::request& req, uint64_t character_id) {
if (!CanAccessCharacter(req, app, character_id)) {
return crow::response(403, "{\"error\": \"Forbidden\"}");
}
crow::json::wvalue response;
try {
auto charInfo = Database::Get()->GetCharacterInfo(character_id);
if (!charInfo) {
response["success"] = false;
response["error"] = "Character not found";
return crow::response(404, response);
}
response["success"] = true;
response["id"] = static_cast<uint64_t>(charInfo->id);
response["name"] = charInfo->name;
response["pending_name"] = charInfo->pendingName;
response["account_id"] = charInfo->accountId;
response["needs_rename"] = charInfo->needsRename;
response["clone_id"] = static_cast<uint64_t>(charInfo->cloneId);
response["permission_map"] = static_cast<uint64_t>(charInfo->permissionMap);
} catch (std::exception& ex) {
response["success"] = false;
response["error"] = ex.what();
}
return crow::response(response);
});
// Get character XML
CROW_ROUTE(app, "/api/characters/<uint>/xml")
.methods("GET"_method)
([&](const crow::request& req, uint64_t character_id) {
if (!CanAccessCharacter(req, app, character_id)) {
return crow::response(403, "{\"error\": \"Forbidden\"}");
}
try {
auto xml = Database::Get()->GetCharacterXml(character_id);
auto res = crow::response(xml);
res.set_header("Content-Type", "application/xml");
res.set_header("Content-Disposition", "attachment; filename=\"character_" + std::to_string(character_id) + ".xml\"");
return res;
} catch (std::exception& ex) {
crow::json::wvalue response;
response["success"] = false;
response["error"] = ex.what();
return crow::response(500, response);
}
});
// Rescue character (teleport to safe zone)
CROW_ROUTE(app, "/api/characters/<uint>/rescue")
.methods("POST"_method)
([&](const crow::request& req, uint64_t character_id) {
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::MODERATOR)) {
return crow::response(403, "{\"error\": \"Forbidden\"}");
}
crow::json::wvalue response;
try {
auto body = crow::json::load(req.body);
if (!body) {
return crow::response(400, "{\"error\": \"Invalid JSON\"}");
}
uint32_t zoneId = 1200; // Default to Avant Gardens
if (body.has("zone_id")) {
zoneId = body["zone_id"].i();
}
// RescueCharacter logic removed; this server does not perform live rescues.
// Return not-implemented to indicate the operation must be performed via the chat server.
response["success"] = false;
response["error"] = "Rescue character not implemented on this server. Use chat server tools.";
} catch (std::exception& ex) {
response["success"] = false;
response["error"] = ex.what();
}
return crow::response(response);
});
// Toggle character restrictions (trade, mail, chat)
CROW_ROUTE(app, "/api/characters/<uint>/restrict/<int>")
.methods("POST"_method)
([&](const crow::request& req, uint64_t character_id, int restriction_bit) {
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::MODERATOR)) {
return crow::response(403, "{\"error\": \"Forbidden\"}");
}
crow::json::wvalue response;
try {
auto charInfo = Database::Get()->GetCharacterInfo(character_id);
if (!charInfo) {
response["success"] = false;
response["error"] = "Character not found";
return crow::response(404, response);
}
// Toggle the restriction bit
uint64_t currentPerms = static_cast<uint64_t>(charInfo->permissionMap);
uint64_t newPerms = currentPerms ^ (1ULL << restriction_bit);
Database::Get()->UpdateCharacterPermissions(character_id, static_cast<ePermissionMap>(newPerms));
response["success"] = true;
response["permission_map"] = newPerms;
response["message"] = "Character restrictions updated";
} catch (std::exception& ex) {
response["success"] = false;
response["error"] = ex.what();
}
return crow::response(response);
});
// Force character rename
CROW_ROUTE(app, "/api/characters/<uint>/force-rename")
.methods("POST"_method)
([&](const crow::request& req, uint64_t character_id) {
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::MODERATOR)) {
return crow::response(403, "{\"error\": \"Forbidden\"}");
}
crow::json::wvalue response;
try {
auto charInfo = Database::Get()->GetCharacterInfo(character_id);
if (!charInfo) {
response["success"] = false;
response["error"] = "Character not found";
return crow::response(404, response);
}
Database::Get()->SetCharacterNeedsRename(character_id, true);
response["success"] = true;
response["message"] = "Character will be forced to rename on next login";
} catch (std::exception& ex) {
response["success"] = false;
response["error"] = ex.what();
}
return crow::response(response);
});
// Set character name (admin override)
CROW_ROUTE(app, "/api/characters/<uint>/set-name")
.methods("POST"_method)
([&](const crow::request& req, uint64_t character_id) {
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::DEVELOPER)) {
return crow::response(403, "{\"error\": \"Forbidden\"}");
}
auto body = crow::json::load(req.body);
if (!body) {
return crow::response(400, "{\"error\": \"Invalid JSON\"}");
}
crow::json::wvalue response;
try {
std::string newName = body["name"].s();
if (newName.empty() || newName.length() > 33) {
response["success"] = false;
response["error"] = "Invalid name length (must be 1-33 characters)";
return crow::response(response);
}
// Check if name is already in use
if (Database::Get()->IsNameInUse(newName)) {
response["success"] = false;
response["error"] = "Name is already in use";
return crow::response(response);
}
Database::Get()->SetCharacterName(character_id, newName);
Database::Get()->SetPendingCharacterName(character_id, "");
Database::Get()->SetCharacterNeedsRename(character_id, false);
response["success"] = true;
response["message"] = "Character name updated successfully";
} catch (std::exception& ex) {
response["success"] = false;
response["error"] = ex.what();
}
return crow::response(response);
});
}
} // namespace CharactersBlueprint

View File

@@ -0,0 +1,20 @@
#ifndef __CHARACTERSBLUEPRINT_H__
#define __CHARACTERSBLUEPRINT_H__
#include "crow.h"
#include "crow/middlewares/session.h"
namespace CharactersBlueprint {
using Session = crow::SessionMiddleware<crow::InMemoryStore>;
using DashboardApp = crow::App<crow::CookieParser, Session>;
/**
* Setup character management routes
* Registers routes for viewing, editing, and managing characters
*/
void Setup(DashboardApp& app);
} // namespace CharactersBlueprint
#endif // __CHARACTERSBLUEPRINT_H__

View File

@@ -0,0 +1,207 @@
#include "MailBlueprint.h"
#include "Database.h"
#include "eGameMasterLevel.h"
#include "MailInfo.h"
#include "Logger.h"
#include <ctime>
namespace MailBlueprint {
// Helper function to get current user's account info from session
std::optional<IAccounts::Info> GetCurrentUser(const crow::request& req, DashboardApp& app) {
auto& session = app.get_context<Session>(const_cast<crow::request&>(req));
std::string username = session.template get<std::string>("username");
if (username.empty()) {
return std::nullopt;
}
return Database::Get()->GetAccountInfo(username);
}
// Helper function to get user's GM level
eGameMasterLevel GetUserGMLevel(const crow::request& req, DashboardApp& app) {
auto user = GetCurrentUser(req, app);
if (!user) {
return eGameMasterLevel::CIVILIAN;
}
return user->maxGmLevel;
}
// Helper function to check if user has minimum GM level
bool HasMinimumGMLevel(const crow::request& req, DashboardApp& app, eGameMasterLevel required) {
auto level = GetUserGMLevel(req, app);
return static_cast<uint8_t>(level) >= static_cast<uint8_t>(required);
}
void Setup(DashboardApp& app) {
// Send mail to a character or all characters
CROW_ROUTE(app, "/api/mail/send")
.methods("POST"_method)
([&](const crow::request& req) {
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::MODERATOR)) {
return crow::response(403, "{\"error\": \"Forbidden\"}");
}
auto body = crow::json::load(req.body);
if (!body) {
return crow::response(400, "{\"error\": \"Invalid JSON\"}");
}
crow::json::wvalue response;
try {
auto user = GetCurrentUser(req, app);
if (!user) {
response["success"] = false;
response["error"] = "Not authenticated";
return crow::response(401, response);
}
auto& session = app.get_context<Session>(const_cast<crow::request&>(req));
std::string username = session.template get<std::string>("username");
// Get mail parameters
std::string subject;
if (body.has("subject"))
subject = std::string(body["subject"].s());
else
subject = "";
std::string message;
if (body.has("body"))
message = std::string(body["body"].s());
else
message = "";
int64_t recipientId = body.has("recipient_id") ? body["recipient_id"].i() : 0;
bool sendToAll = body.has("send_to_all") ? body["send_to_all"].b() : false;
// Item attachment (optional)
int32_t itemLot = body.has("attachment_lot") ? body["attachment_lot"].i() : 0;
int32_t itemCount = body.has("attachment_count") ? body["attachment_count"].i() : 0;
if (subject.empty() || message.empty()) {
response["success"] = false;
response["error"] = "Subject and body are required";
return crow::response(response);
}
// Prefix sender name with [GM]
std::string senderName = "[GM] " + username;
std::vector<LWOOBJID> recipients;
if (sendToAll) {
// Get all accounts and their characters
auto allAccounts = Database::Get()->GetAllAccounts();
for (const auto& acct : allAccounts) {
auto chars = Database::Get()->GetAccountCharacterIds(acct.id);
for (const auto& charId : chars) {
recipients.push_back(charId);
}
}
} else if (recipientId > 0) {
recipients.push_back(recipientId);
} else {
response["success"] = false;
response["error"] = "No recipients specified";
return crow::response(response);
}
// Send mail to all recipients
uint64_t currentTime = static_cast<uint64_t>(std::time(nullptr));
int mailSent = 0;
for (const auto& recipId : recipients) {
// Get recipient character name
auto charInfo = Database::Get()->GetCharacterInfo(recipId);
if (!charInfo) continue;
MailInfo mail;
mail.senderUsername = senderName;
mail.recipient = charInfo->name;
mail.receiverId = recipId;
mail.subject = subject;
mail.body = message;
mail.itemID = itemLot > 0 ? 1 : 0; // If there's an item, set ID to 1
mail.itemLOT = itemLot;
mail.itemCount = itemCount > 0 ? itemCount : 1;
mail.timeSent = currentTime;
mail.wasRead = false;
Database::Get()->InsertNewMail(mail);
mailSent++;
}
response["success"] = true;
response["message"] = "Mail sent successfully";
response["recipients"] = mailSent;
} catch (std::exception& ex) {
response["success"] = false;
response["error"] = ex.what();
}
return crow::response(response);
});
// Get mail by ID (for viewing)
CROW_ROUTE(app, "/api/mail/<uint>")
.methods("GET"_method)
([&](const crow::request& req, uint64_t mail_id) {
// Any authenticated user can view mail
auto user = GetCurrentUser(req, app);
if (!user) {
return crow::response(401, "{\"error\": \"Not authenticated\"}");
}
crow::json::wvalue response;
try {
auto mail = Database::Get()->GetMail(mail_id);
if (!mail) {
response["success"] = false;
response["error"] = "Mail not found";
return crow::response(404, response);
}
// Check if user can access this mail (owns the character or is GM)
auto charInfo = Database::Get()->GetCharacterInfo(mail->receiverId);
bool canAccess = false;
if (charInfo && charInfo->accountId == user->id) {
canAccess = true;
}
if (static_cast<uint8_t>(user->maxGmLevel) >= static_cast<uint8_t>(eGameMasterLevel::MODERATOR)) {
canAccess = true;
}
if (!canAccess) {
response["success"] = false;
response["error"] = "Access denied";
return crow::response(403, response);
}
response["success"] = true;
response["id"] = mail->id;
response["sender_name"] = mail->senderUsername;
response["receiver_name"] = mail->recipient;
response["receiver_id"] = static_cast<uint64_t>(mail->receiverId);
response["subject"] = mail->subject;
response["body"] = mail->body;
response["attachment_lot"] = mail->itemLOT;
response["attachment_count"] = mail->itemCount;
response["time_sent"] = mail->timeSent;
response["was_read"] = mail->wasRead;
} catch (std::exception& ex) {
response["success"] = false;
response["error"] = ex.what();
}
return crow::response(response);
});
}
} // namespace MailBlueprint

View File

@@ -0,0 +1,20 @@
#ifndef __MAILBLUEPRINT_H__
#define __MAILBLUEPRINT_H__
#include "crow.h"
#include "crow/middlewares/session.h"
namespace MailBlueprint {
using Session = crow::SessionMiddleware<crow::InMemoryStore>;
using DashboardApp = crow::App<crow::CookieParser, Session>;
/**
* Setup mail management routes
* Registers routes for sending and viewing mail
*/
void Setup(DashboardApp& app);
} // namespace MailBlueprint
#endif // __MAILBLUEPRINT_H__

View File

@@ -0,0 +1,279 @@
#include "ModerationBlueprint.h"
#include "Database.h"
#include "eGameMasterLevel.h"
#include "Logger.h"
namespace ModerationBlueprint {
// Helper function to get current user's account info from session
std::optional<IAccounts::Info> GetCurrentUser(const crow::request& req, DashboardApp& app) {
auto& session = app.get_context<Session>(const_cast<crow::request&>(req));
std::string username = session.template get<std::string>("username");
if (username.empty()) {
return std::nullopt;
}
return Database::Get()->GetAccountInfo(username);
}
// Helper function to check if user has minimum GM level
bool HasMinimumGMLevel(const crow::request& req, DashboardApp& app, eGameMasterLevel required) {
auto user = GetCurrentUser(req, app);
if (!user) {
return false;
}
return static_cast<uint8_t>(user->maxGmLevel) >= static_cast<uint8_t>(required);
}
void Setup(DashboardApp& app) {
// Get pet names by status
CROW_ROUTE(app, "/api/moderation/pets")
.methods("GET"_method)
([&](const crow::request& req) {
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::MODERATOR)) {
return crow::response(403, "{\"error\": \"Forbidden\"}");
}
crow::json::wvalue response;
crow::json::wvalue::list data;
try {
auto statusParam = req.url_params.get("status");
std::string status = statusParam ? statusParam : "all";
std::vector<IPetNames::DetailedInfo> pets;
if (status == "approved") {
pets = Database::Get()->GetPetNamesByStatus(2);
} else if (status == "unapproved") {
pets = Database::Get()->GetPetNamesByStatus(1);
} else {
pets = Database::Get()->GetAllPetNames();
}
for (const auto& pet : pets) {
crow::json::wvalue item;
item["id"] = static_cast<uint64_t>(pet.id);
item["pet_name"] = pet.petName;
item["approval_status"] = pet.approvalStatus;
item["owner_id"] = static_cast<uint64_t>(pet.ownerId);
// Get owner character name
if (pet.ownerId > 0) {
auto charInfo = Database::Get()->GetCharacterInfo(pet.ownerId);
if (charInfo) {
item["owner_name"] = charInfo->name;
} else {
item["owner_name"] = "Unknown";
}
} else {
item["owner_name"] = "None";
}
data.push_back(std::move(item));
}
response["data"] = std::move(data);
} catch (std::exception& ex) {
response["error"] = ex.what();
return crow::response(500, response);
}
return crow::response(response);
});
// Approve a pet name
CROW_ROUTE(app, "/api/moderation/pets/<uint>/approve")
.methods("POST"_method)
([&](const crow::request& req, uint64_t pet_id) {
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::MODERATOR)) {
return crow::response(403, "{\"error\": \"Forbidden\"}");
}
crow::json::wvalue response;
try {
Database::Get()->SetPetApprovalStatus(pet_id, 2); // 2 = approved
response["success"] = true;
response["message"] = "Pet name approved";
} catch (std::exception& ex) {
response["success"] = false;
response["error"] = ex.what();
}
return crow::response(response);
});
// Reject a pet name
CROW_ROUTE(app, "/api/moderation/pets/<uint>/reject")
.methods("POST"_method)
([&](const crow::request& req, uint64_t pet_id) {
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::MODERATOR)) {
return crow::response(403, "{\"error\": \"Forbidden\"}");
}
crow::json::wvalue response;
try {
Database::Get()->SetPetApprovalStatus(pet_id, 0); // 0 = rejected
response["success"] = true;
response["message"] = "Pet name rejected";
} catch (std::exception& ex) {
response["success"] = false;
response["error"] = ex.what();
}
return crow::response(response);
});
// Get properties by approval status
CROW_ROUTE(app, "/api/moderation/properties")
.methods("GET"_method)
([&](const crow::request& req) {
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::MODERATOR)) {
return crow::response(403, "{\"error\": \"Forbidden\"}");
}
crow::json::wvalue response;
crow::json::wvalue::list data;
try {
auto statusParam = req.url_params.get("status");
std::string status = statusParam ? statusParam : "all";
std::vector<IProperty::Info> properties;
if (status == "approved") {
properties = Database::Get()->GetPropertiesByApprovalStatus(1);
} else if (status == "unapproved") {
properties = Database::Get()->GetPropertiesByApprovalStatus(0);
} else {
properties = Database::Get()->GetAllProperties();
}
for (const auto& prop : properties) {
crow::json::wvalue item;
item["id"] = static_cast<uint64_t>(prop.id);
item["name"] = prop.name;
item["description"] = prop.description;
item["owner_id"] = static_cast<uint64_t>(prop.ownerId);
item["clone_id"] = static_cast<uint64_t>(prop.cloneId);
item["privacy_option"] = prop.privacyOption;
item["mod_approved"] = prop.modApproved;
item["last_updated"] = prop.lastUpdatedTime;
item["claimed_time"] = prop.claimedTime;
item["reputation"] = prop.reputation;
item["performance_cost"] = prop.performanceCost;
item["rejection_reason"] = prop.rejectionReason;
// Get owner character name
auto charInfo = Database::Get()->GetCharacterInfo(prop.ownerId);
if (charInfo) {
item["owner_name"] = charInfo->name;
} else {
item["owner_name"] = "Unknown";
}
data.push_back(std::move(item));
}
response["data"] = std::move(data);
} catch (std::exception& ex) {
response["error"] = ex.what();
return crow::response(500, response);
}
return crow::response(response);
});
// Approve/unapprove a property
CROW_ROUTE(app, "/api/moderation/properties/<uint>/approve")
.methods("POST"_method)
([&](const crow::request& req, uint64_t property_id) {
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::MODERATOR)) {
return crow::response(403, "{\"error\": \"Forbidden\"}");
}
crow::json::wvalue response;
try {
auto prop = Database::Get()->GetPropertyInfo(property_id);
if (!prop) {
response["success"] = false;
response["error"] = "Property not found";
return crow::response(404, response);
}
// Toggle approval
IProperty::Info updatedInfo = *prop;
updatedInfo.modApproved = prop->modApproved ? 0 : 1;
updatedInfo.rejectionReason = "";
Database::Get()->UpdatePropertyModerationInfo(updatedInfo);
response["success"] = true;
response["approved"] = updatedInfo.modApproved;
} catch (std::exception& ex) {
response["success"] = false;
response["error"] = ex.what();
}
return crow::response(response);
});
// Reject a property with reason
CROW_ROUTE(app, "/api/moderation/properties/<uint>/reject")
.methods("POST"_method)
([&](const crow::request& req, uint64_t property_id) {
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::MODERATOR)) {
return crow::response(403, "{\"error\": \"Forbidden\"}");
}
auto body = crow::json::load(req.body);
if (!body) {
return crow::response(400, "{\"error\": \"Invalid JSON\"}");
}
crow::json::wvalue response;
try {
auto prop = Database::Get()->GetPropertyInfo(property_id);
if (!prop) {
response["success"] = false;
response["error"] = "Property not found";
return crow::response(404, response);
}
std::string reason;
if (body.has("reason"))
reason = std::string(body["reason"].s());
else
reason = "No reason provided";
IProperty::Info updatedInfo = *prop;
updatedInfo.modApproved = 0;
updatedInfo.rejectionReason = reason;
Database::Get()->UpdatePropertyModerationInfo(updatedInfo);
response["success"] = true;
response["message"] = "Property rejected";
} catch (std::exception& ex) {
response["success"] = false;
response["error"] = ex.what();
}
return crow::response(response);
});
}
} // namespace ModerationBlueprint

View File

@@ -0,0 +1,20 @@
#ifndef __MODERATIONBLUEPRINT_H__
#define __MODERATIONBLUEPRINT_H__
#include "crow.h"
#include "crow/middlewares/session.h"
namespace ModerationBlueprint {
using Session = crow::SessionMiddleware<crow::InMemoryStore>;
using DashboardApp = crow::App<crow::CookieParser, Session>;
/**
* Setup moderation routes
* Registers routes for pet name moderation and property approval
*/
void Setup(DashboardApp& app);
} // namespace ModerationBlueprint
#endif // __MODERATIONBLUEPRINT_H__

View File

@@ -0,0 +1,380 @@
#include "PageBlueprint.h"
#include "Logger.h"
#include "Database.h"
#include "eGameMasterLevel.h"
namespace PageBlueprint {
// Helper to get GM level name
std::string GetGMLevelName(eGameMasterLevel level) {
switch (level) {
case eGameMasterLevel::CIVILIAN: return "Civilian";
case eGameMasterLevel::FORUM_MODERATOR: return "Forum Moderator";
case eGameMasterLevel::JUNIOR_MODERATOR: return "Junior Moderator";
case eGameMasterLevel::MODERATOR: return "Moderator";
case eGameMasterLevel::SENIOR_MODERATOR: return "Senior Moderator";
case eGameMasterLevel::LEAD_MODERATOR: return "Lead Moderator";
case eGameMasterLevel::JUNIOR_DEVELOPER: return "Junior Developer";
case eGameMasterLevel::INACTIVE_DEVELOPER: return "Inactive Developer";
case eGameMasterLevel::DEVELOPER: return "Developer";
case eGameMasterLevel::OPERATOR: return "Operator";
default: return "Unknown";
}
}
// Helper to get current user's account info from session
std::optional<IAccounts::Info> GetCurrentUser(const crow::request& req, DashboardApp& app) {
auto& session = app.get_context<Session>(const_cast<crow::request&>(req));
std::string username = session.template get<std::string>("username");
if (username.empty()) {
return std::nullopt;
}
return Database::Get()->GetAccountInfo(username);
}
// Helper to get user's GM level
eGameMasterLevel GetUserGMLevel(const crow::request& req, DashboardApp& app) {
auto user = GetCurrentUser(req, app);
if (!user) {
return eGameMasterLevel::CIVILIAN;
}
return user->maxGmLevel;
}
// Helper to check if user has minimum GM level
bool HasMinimumGMLevel(const crow::request& req, DashboardApp& app, eGameMasterLevel required) {
auto level = GetUserGMLevel(req, app);
return static_cast<uint8_t>(level) >= static_cast<uint8_t>(required);
}
// Helper to create base context for all templates
crow::mustache::context GetBaseContext(const crow::request& req, DashboardApp& app) {
crow::mustache::context ctx;
auto& session = app.get_context<Session>(const_cast<crow::request&>(req));
std::string username = session.template get<std::string>("username");
int account_id = session.template get<int>("account_id", -1);
int gm_level = session.template get<int>("gm_level", -1);
if (!username.empty() && account_id != -1) {
LOG("User '%s' (Account ID: %d) is authenticated with GM level %d", username.c_str(), account_id, gm_level);
ctx["is_authenticated"] = true;
ctx["show_navbar"] = true;
ctx["username"] = username;
ctx["account_id"] = account_id;
ctx["gm_level"] = gm_level;
ctx["gm_level_name"] = GetGMLevelName(static_cast<eGameMasterLevel>(gm_level));
// Set permission flags
ctx["is_gm_3_plus"] = (gm_level >= 3);
ctx["is_gm_5_plus"] = (gm_level >= 5);
ctx["is_gm_8_plus"] = (gm_level >= 8);
ctx["is_gm_9_plus"] = (gm_level >= 9);
} else {
LOG("User is not authenticated");
ctx["is_authenticated"] = false;
ctx["show_navbar"] = false;
}
return ctx;
}
// Helper to render a page with layout
std::string RenderPage(const crow::request& req, DashboardApp& app, const std::string& template_name, const std::string& page_title, crow::mustache::context& page_ctx) {
auto base_ctx = GetBaseContext(req, app);
// Merge base context with page-specific context
for (const auto& key : page_ctx.keys()) {
base_ctx[key] = crow::json::wvalue(page_ctx[key]);
}
// Load the content template and render to string
auto content_page = crow::mustache::load(template_name);
std::string content_html = content_page.render_string(base_ctx);
// Set content and page title in base context
base_ctx["content"] = crow::json::wvalue(content_html);
base_ctx["page_title"] = crow::json::wvalue(page_title);
// Render with layout
auto layout = crow::mustache::load("layouts/base.html");
return layout.render_string(base_ctx);
}
void Setup(DashboardApp& app) {
// Home/Dashboard page
CROW_ROUTE(app, "/")
([&](const crow::request& req) {
crow::mustache::context ctx;
ctx["nav_home"] = true;
std::string html = RenderPage(req, app, "index.html", "Dashboard", ctx);
return crow::response(html);
});
// Login page
CROW_ROUTE(app, "/login")
([&](const crow::request& req) {
crow::mustache::context ctx;
std::string html = RenderPage(req, app, "login.html", "Login", ctx);
return crow::response(html);
});
// Accounts page
CROW_ROUTE(app, "/accounts")
([&](const crow::request& req) {
// Check GM level
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::MODERATOR)) {
return crow::response(403, "Forbidden - Insufficient GM level");
}
crow::mustache::context ctx;
ctx["nav_accounts"] = true;
std::string html = RenderPage(req, app, "accounts/index.html", "Accounts", ctx);
return crow::response(html);
});
// Activity Logs page
CROW_ROUTE(app, "/logs/activities")
([&](const crow::request& req) {
// Check GM level - Developers and above
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::DEVELOPER)) {
return crow::response(403, "Forbidden - Insufficient GM level");
}
crow::mustache::context ctx;
// Set nav active state if needed
std::string html = RenderPage(req, app, "logs/activities.html", "Activity Logs", ctx);
return crow::response(html);
});
// Characters page
CROW_ROUTE(app, "/characters")
([&](const crow::request& req) {
// Check GM level - Moderators and above
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::MODERATOR)) {
return crow::response(403, "Forbidden - Insufficient GM level");
}
crow::mustache::context ctx;
ctx["nav_characters"] = true;
std::string html = RenderPage(req, app, "characters/index.html", "Characters", ctx);
return crow::response(html);
});
// Play Keys page
CROW_ROUTE(app, "/playkeys")
([&](const crow::request& req) {
// Check GM level - Lead Moderators and above
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::LEAD_MODERATOR)) {
return crow::response(403, "Forbidden - Insufficient GM level");
}
crow::mustache::context ctx;
ctx["nav_playkeys"] = true;
std::string html = RenderPage(req, app, "playkeys/index.html", "Play Keys", ctx);
return crow::response(html);
});
// Registration page - public
CROW_ROUTE(app, "/register")
([&](const crow::request& req) {
crow::mustache::context ctx;
std::string html = RenderPage(req, app, "register.html", "Register", ctx);
return crow::response(html);
});
// Mail page
CROW_ROUTE(app, "/mail/send")
([&](const crow::request& req) {
// Check GM level - Moderators and above
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::MODERATOR)) {
return crow::response(403, "Forbidden - Insufficient GM level");
}
crow::mustache::context ctx;
ctx["nav_mail"] = true;
std::string html = RenderPage(req, app, "mail/send.html", "Send Mail", ctx);
return crow::response(html);
});
// Bug Reports page
CROW_ROUTE(app, "/bugreports")
([&](const crow::request& req) {
// Anyone authenticated can view their own bug reports
// GMs can view all
auto user = GetCurrentUser(req, app);
if (!user) {
return crow::response(403, "Forbidden - Login required");
}
crow::mustache::context ctx;
ctx["nav_bugreports"] = true;
std::string html = RenderPage(req, app, "bugreports/index.html", "Bug Reports", ctx);
return crow::response(html);
});
// Moderation page - Pet Names
CROW_ROUTE(app, "/moderation/pets")
([&](const crow::request& req) {
// Check GM level - Moderators and above
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::MODERATOR)) {
return crow::response(403, "Forbidden - Insufficient GM level");
}
crow::mustache::context ctx;
ctx["nav_moderation"] = true;
std::string html = RenderPage(req, app, "moderation/pets.html", "Pet Name Moderation", ctx);
return crow::response(html);
});
// Moderation page - Properties
CROW_ROUTE(app, "/moderation/properties")
([&](const crow::request& req) {
// Check GM level - Moderators and above
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::MODERATOR)) {
return crow::response(403, "Forbidden - Insufficient GM level");
}
crow::mustache::context ctx;
ctx["nav_moderation"] = true;
std::string html = RenderPage(req, app, "moderation/properties.html", "Property Moderation", ctx);
return crow::response(html);
});
// Account view page
CROW_ROUTE(app, "/accounts/view/<int>")
([&](const crow::request& req, int account_id) {
// Check GM level - Moderators and above
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::MODERATOR)) {
return crow::response(403, "Forbidden - Insufficient GM level");
}
crow::mustache::context ctx;
ctx["nav_accounts"] = true;
ctx["account_id"] = account_id;
std::string html = RenderPage(req, app, "accounts/view.html", "View Account", ctx);
return crow::response(html);
});
// Character view page
CROW_ROUTE(app, "/characters/view/<int>")
([&](const crow::request& req, int character_id) {
// Check GM level - Moderators and above
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::MODERATOR)) {
return crow::response(403, "Forbidden - Insufficient GM level");
}
crow::mustache::context ctx;
ctx["nav_characters"] = true;
ctx["character_id"] = character_id;
std::string html = RenderPage(req, app, "characters/view.html", "View Character", ctx);
return crow::response(html);
});
// Logs - Command Logs page
CROW_ROUTE(app, "/logs/commands")
([&](const crow::request& req) {
// Check GM level - Developers and above
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::DEVELOPER)) {
return crow::response(403, "Forbidden - Insufficient GM level");
}
crow::mustache::context ctx;
// Set nav active state if needed
std::string html = RenderPage(req, app, "logs/commands.html", "Command Logs", ctx);
return crow::response(html);
});
// Logs - Audit Logs page
CROW_ROUTE(app, "/logs/audits")
([&](const crow::request& req) {
// Check GM level - Developers and above
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::DEVELOPER)) {
return crow::response(403, "Forbidden - Insufficient GM level");
}
crow::mustache::context ctx;
// Set nav active state if needed
std::string html = RenderPage(req, app, "logs/audits.html", "Audit Logs", ctx);
return crow::response(html);
});
// About page
CROW_ROUTE(app, "/about")
([&](const crow::request& req) {
auto user = GetCurrentUser(req, app);
if (!user) {
return crow::response(403, "Forbidden - Login required");
}
crow::mustache::context ctx;
std::string html = RenderPage(req, app, "about.html", "About", ctx);
return crow::response(html);
});
// Bug Reports page (fix routing)
CROW_ROUTE(app, "/bugs")
([&](const crow::request& req) {
// Anyone authenticated can view their own bug reports
// GMs can view all
auto user = GetCurrentUser(req, app);
if (!user) {
return crow::response(403, "Forbidden - Login required");
}
crow::mustache::context ctx;
ctx["nav_bugs"] = true;
std::string html = RenderPage(req, app, "bugreports/index.html", "Bug Reports", ctx);
return crow::response(html);
});
// Moderation page - Pending Pets
CROW_ROUTE(app, "/moderation/pending")
([&](const crow::request& req) {
// Check GM level - Moderators and above
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::MODERATOR)) {
return crow::response(403, "Forbidden - Insufficient GM level");
}
crow::mustache::context ctx;
ctx["nav_moderation"] = true;
std::string html = RenderPage(req, app, "moderation/pets.html", "Pending Pet Names", ctx);
return crow::response(html);
});
// Properties page
CROW_ROUTE(app, "/properties")
([&](const crow::request& req) {
// Check GM level - Moderators and above
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::MODERATOR)) {
return crow::response(403, "Forbidden - Insufficient GM level");
}
crow::mustache::context ctx;
ctx["nav_moderation"] = true;
std::string html = RenderPage(req, app, "moderation/properties.html", "Property Moderation", ctx);
return crow::response(html);
});
}
} // namespace PageBlueprint

View File

@@ -0,0 +1,17 @@
#pragma once
#include "crow.h"
#include "crow/middlewares/session.h"
namespace PageBlueprint {
using Session = crow::SessionMiddleware<crow::InMemoryStore>;
using DashboardApp = crow::App<crow::CookieParser, Session>;
/**
* Setup page rendering routes
* Registers routes that render HTML pages (dashboard, login, accounts, etc.)
*/
void Setup(DashboardApp& app);
} // namespace PageBlueprint

View File

@@ -0,0 +1,288 @@
#include "PlayKeysBlueprint.h"
#include "Database.h"
#include "eGameMasterLevel.h"
#include "Logger.h"
#include <random>
#include <sstream>
#include <iomanip>
namespace PlayKeysBlueprint {
// Helper to generate a random play key string (format: XXXX-XXXX-XXXX-XXXX)
std::string GeneratePlayKeyString() {
static const char charset[] = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; // Excluding ambiguous chars
static std::random_device rd;
static std::mt19937 gen(rd());
static std::uniform_int_distribution<> dis(0, sizeof(charset) - 2);
std::stringstream ss;
for (int i = 0; i < 16; i++) {
if (i > 0 && i % 4 == 0) ss << '-';
ss << charset[dis(gen)];
}
return ss.str();
}
// Helper function to get current user's account info from session
std::optional<IAccounts::Info> GetCurrentUser(const crow::request& req, DashboardApp& app) {
auto& session = app.get_context<Session>(const_cast<crow::request&>(req));
std::string username = session.template get<std::string>("username");
if (username.empty()) {
return std::nullopt;
}
return Database::Get()->GetAccountInfo(username);
}
// Helper function to get user's GM level
eGameMasterLevel GetUserGMLevel(const crow::request& req, DashboardApp& app) {
auto user = GetCurrentUser(req, app);
if (!user) {
return eGameMasterLevel::CIVILIAN;
}
return user->maxGmLevel;
}
// Helper function to check if user has minimum GM level
bool HasMinimumGMLevel(const crow::request& req, DashboardApp& app, eGameMasterLevel required) {
auto level = GetUserGMLevel(req, app);
return static_cast<uint8_t>(level) >= static_cast<uint8_t>(required);
}
void Setup(DashboardApp& app) {
// Get all play keys (DataTables endpoint)
CROW_ROUTE(app, "/api/playkeys")
.methods("GET"_method)
([&](const crow::request& req) {
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::LEAD_MODERATOR)) {
return crow::response(403, "{\"error\": \"Forbidden\"}");
}
crow::json::wvalue response;
crow::json::wvalue::list data;
try {
auto keys = Database::Get()->GetAllPlayKeys();
for (const auto& key : keys) {
crow::json::wvalue item;
item["id"] = key.id;
item["key_string"] = key.key_string;
item["key_uses"] = key.key_uses;
item["times_used"] = key.times_used;
item["active"] = key.active;
item["notes"] = key.notes;
item["created_at"] = static_cast<uint64_t>(key.created_at);
data.push_back(std::move(item));
}
} catch (std::exception& ex) {
// return empty list on failure
}
response["data"] = std::move(data);
return crow::response(response);
});
// Create a new play key
CROW_ROUTE(app, "/api/playkeys/create")
.methods("POST"_method)
([&](const crow::request& req) {
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::LEAD_MODERATOR)) {
return crow::response(403, "{\"error\": \"Forbidden\"}");
}
auto body = crow::json::load(req.body);
if (!body) {
return crow::response(400, "{\"error\": \"Invalid JSON\"}");
}
crow::json::wvalue response;
try {
uint32_t count = body.has("count") ? body["count"].i() : 1;
uint32_t uses = body.has("uses") ? body["uses"].i() : 1;
std::string notes;
if (body.has("notes"))
notes = std::string(body["notes"].s());
else
notes = "";
// Limit to prevent abuse
if (count > 100) {
response["success"] = false;
response["error"] = "Cannot create more than 100 keys at once";
return crow::response(response);
}
crow::json::wvalue::list keys;
for (uint32_t i = 0; i < count; i++) {
std::string keyString = GeneratePlayKeyString();
Database::Get()->CreatePlayKey(keyString, uses, notes);
keys.push_back(keyString);
}
response["success"] = true;
response["keys"] = std::move(keys);
response["count"] = count;
} catch (std::exception& ex) {
response["success"] = false;
response["error"] = ex.what();
}
return crow::response(response);
});
// Get single play key by ID
CROW_ROUTE(app, "/api/playkeys/<int>")
.methods("GET"_method)
([&](const crow::request& req, int key_id) {
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::LEAD_MODERATOR)) {
return crow::response(403, "{\"error\": \"Forbidden\"}");
}
crow::json::wvalue response;
try {
auto key = Database::Get()->GetPlayKeyById(key_id);
if (!key) {
response["success"] = false;
response["error"] = "Play key not found";
return crow::response(404, response);
}
response["success"] = true;
response["id"] = key->id;
response["key_string"] = key->key_string;
response["key_uses"] = key->key_uses;
response["times_used"] = key->times_used;
response["active"] = key->active;
response["notes"] = key->notes;
response["created_at"] = static_cast<uint64_t>(key->created_at);
} catch (std::exception& ex) {
response["success"] = false;
response["error"] = ex.what();
}
return crow::response(response);
});
// Update a play key
CROW_ROUTE(app, "/api/playkeys/<int>")
.methods("PUT"_method, "POST"_method)
([&](const crow::request& req, int key_id) {
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::LEAD_MODERATOR)) {
return crow::response(403, "{\"error\": \"Forbidden\"}");
}
auto body = crow::json::load(req.body);
if (!body) {
return crow::response(400, "{\"error\": \"Invalid JSON\"}");
}
crow::json::wvalue response;
try {
// Get current key info
auto key = Database::Get()->GetPlayKeyById(key_id);
if (!key) {
response["success"] = false;
response["error"] = "Play key not found";
return crow::response(404, response);
}
uint32_t uses = body.has("uses") ? body["uses"].i() : key->key_uses;
bool active = body.has("active") ? body["active"].b() : key->active;
std::string notes;
if (body.has("notes"))
notes = std::string(body["notes"].s());
else
notes = key->notes;
Database::Get()->UpdatePlayKey(key_id, uses, active, notes);
response["success"] = true;
response["message"] = "Play key updated successfully";
} catch (std::exception& ex) {
response["success"] = false;
response["error"] = ex.what();
}
return crow::response(response);
});
// Delete a play key
CROW_ROUTE(app, "/api/playkeys/<int>")
.methods("DELETE"_method)
([&](const crow::request& req, int key_id) {
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::LEAD_MODERATOR)) {
return crow::response(403, "{\"error\": \"Forbidden\"}");
}
crow::json::wvalue response;
try {
// Check if key exists
auto key = Database::Get()->GetPlayKeyById(key_id);
if (!key) {
response["success"] = false;
response["error"] = "Play key not found";
return crow::response(404, response);
}
Database::Get()->DeletePlayKey(key_id);
response["success"] = true;
response["message"] = "Play key deleted successfully";
} catch (std::exception& ex) {
response["success"] = false;
response["error"] = ex.what();
}
return crow::response(response);
});
// Get accounts associated with a play key
CROW_ROUTE(app, "/api/playkeys/<int>/accounts")
.methods("GET"_method)
([&](const crow::request& req, int key_id) {
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::LEAD_MODERATOR)) {
return crow::response(403, "{\"error\": \"Forbidden\"}");
}
crow::json::wvalue response;
crow::json::wvalue::list accounts;
try {
// Get all accounts and filter by play_key_id
auto allAccounts = Database::Get()->GetAllAccounts();
for (const auto& acct : allAccounts) {
if (acct.play_key_id == static_cast<uint32_t>(key_id)) {
crow::json::wvalue item;
item["id"] = acct.id;
item["name"] = acct.name;
item["gm_level"] = static_cast<int>(acct.gm_level);
item["banned"] = acct.banned;
item["locked"] = acct.locked;
accounts.push_back(std::move(item));
}
}
response["data"] = std::move(accounts);
} catch (std::exception& ex) {
response["error"] = ex.what();
return crow::response(500, response);
}
return crow::response(response);
});
}
} // namespace PlayKeysBlueprint

View File

@@ -0,0 +1,20 @@
#ifndef __PLAYKEYSBLUEPRINT_H__
#define __PLAYKEYSBLUEPRINT_H__
#include "crow.h"
#include "crow/middlewares/session.h"
namespace PlayKeysBlueprint {
using Session = crow::SessionMiddleware<crow::InMemoryStore>;
using DashboardApp = crow::App<crow::CookieParser, Session>;
/**
* Setup play keys management routes
* Registers routes for creating, viewing, editing, and deleting play keys
*/
void Setup(DashboardApp& app);
} // namespace PlayKeysBlueprint
#endif // __PLAYKEYSBLUEPRINT_H__

View File

@@ -0,0 +1,144 @@
/*
* Consolidated NexusDashboard CSS
* Combined from nexus-theme.css and dashboard.css to provide a single
* consistent stylesheet for the DarkflameServer dashboard.
*/
/* ------------------------ Nexus theme (dark) variables ------------------------ */
:root {
--nexus-dark-bg: #212529;
--nexus-darker-bg: #1a1d20;
--nexus-card-bg: #2c3034;
--nexus-border: #404448;
--nexus-text: #f8f9fa;
--nexus-text-muted: #adb5bd;
--nexus-primary: #0d6efd;
--nexus-success: #198754;
--nexus-warning: #ffc107;
--nexus-danger: #dc3545;
--nexus-info: #0dcaf0;
/* legacy dashboard variables */
--primary-color: #0d6efd;
--success-color: #198754;
--warning-color: #ffc107;
--danger-color: #dc3545;
--dark-bg: #1a1a1a;
--light-bg: #f8f9fa;
}
/* ------------------------ Base layout, navbar, cards ------------------------ */
body {
background-color: var(--nexus-dark-bg);
color: var(--nexus-text);
min-height: 100vh;
display: flex;
flex-direction: column;
}
main { flex: 1; padding-bottom: 60px; }
.footer { margin-top: auto; border-top: 1px solid var(--nexus-border); background-color: var(--nexus-dark-bg); }
/* Ensure footer text is visible on dark background */
.footer, .footer .text-muted { color: var(--nexus-text-muted) !important; }
.navbar { box-shadow: 0 2px 4px rgba(0,0,0,.1); }
.navbar-brand { font-weight: bold; font-size: 1.25rem; }
.nav-link { transition: all 0.3s ease; }
.nav-link:hover { background-color: rgba(255,255,255,0.05); border-radius: 4px; }
.nav-link.active { background-color: rgba(255,255,255,0.08); border-radius: 4px; }
.card { background-color: var(--nexus-card-bg); border-color: var(--nexus-border); color: var(--nexus-text); border: none; box-shadow: 0 2px 8px rgba(0,0,0,0.1); margin-bottom: 1.5rem; transition: transform 0.2s ease, box-shadow 0.2s ease; }
.card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.15); }
.card-header { background-color: var(--nexus-darker-bg); border-bottom-color: var(--nexus-border); color: var(--nexus-text); font-weight: 600; }
/* ------------------------ Tables and DataTables ------------------------ */
.table { color: var(--nexus-text); background-color: #1e1e1e; }
.table thead th { background-color: #242526; color: var(--nexus-text); border-bottom: 1px solid var(--nexus-border); font-weight: 600; }
.table tbody td { color: var(--nexus-text); }
.table-striped > tbody > tr:nth-of-type(odd) > * { background-color: rgba(255,255,255,0.02); }
.table-hover > tbody > tr:hover > * { background-color: rgba(255,255,255,0.035); }
/* DataTables adds `odd`/`even` classes and sometimes doesn't use `.table-striped`.
Normalize striping across Bootstrap tables and DataTables instances so every
other row has a visible background in dark mode. Use slightly stronger contrast
and cover different DOM shapes that DataTables can produce (cells or `*`). */
.dataTable tbody tr.odd > *,
.dataTable tbody tr.odd td,
.table.table-striped tbody tr.odd > *,
.table.table-striped tbody tr.odd td {
background-color: rgba(255,255,255,0.03);
}
.dataTable tbody tr.even > *,
.dataTable tbody tr.even td,
.table.table-striped tbody tr.even > *,
.table.table-striped tbody tr.even td {
background-color: transparent;
}
/* Some DataTables setups use nested wrappers (.dataTables_scrollBody) so ensure
striping still applies inside scroll bodies. */
.dataTables_scrollBody table tbody tr.odd > *,
.dataTables_scrollBody table tbody tr.odd td {
background-color: rgba(255,255,255,0.03);
}
.dataTables_scrollBody table tbody tr.even > *,
.dataTables_scrollBody table tbody tr.even td {
background-color: transparent;
}
/* Keep hover state clear above striping */
.dataTable tbody tr:hover > *,
.table tbody tr:hover > * {
background-color: rgba(255,255,255,0.05);
}
.table > :not(caption) > * > * { border-bottom-color: var(--nexus-border); }
/* Light-theme overrides (explicit) */
@media (prefers-color-scheme: light) {
body { background-color: var(--light-bg); color: #212529; }
.card { background-color: #fff; color: #212529; }
.card-header { background-color: #fff; border-bottom: 2px solid var(--primary-color); color: #212529; }
.table { background-color: white; color: #212529; }
.table thead th { background-color: #f8f9fa; color: #212529; border-bottom: 2px solid #dee2e6; font-weight: 600; text-transform: uppercase; font-size: 0.85rem; letter-spacing: 0.5px; }
.dataTables_wrapper select, .dataTables_wrapper input { background-color: #fff; border-color: #ced4da; color: #212529; }
}
/* Dark mode explicit styling (prefers-color-scheme: dark) */
@media (prefers-color-scheme: dark) {
body { background-color: var(--nexus-dark-bg); color: var(--nexus-text); }
.card { background-color: var(--nexus-card-bg); color: var(--nexus-text); }
.card-header { background-color: var(--nexus-darker-bg); color: var(--nexus-text); }
.table { background-color: #1e1e1e; color: var(--nexus-text); }
.table thead th { background-color: #252525; border-bottom-color: #3a3a3a; color: var(--nexus-text); }
.dataTables_wrapper select, .dataTables_wrapper input { background-color: var(--nexus-darker-bg); border-color: var(--nexus-border); color: var(--nexus-text); }
}
/* DataTables specific visual rules */
.dataTables_wrapper { padding: 0; }
.dataTables_filter input { margin-left: 0.5rem; padding: 0.375rem 0.75rem; border: 1px solid #ced4da; border-radius: 0.25rem; }
.dataTables_length select { padding: 0.375rem 2rem 0.375rem 0.75rem; border: 1px solid #ced4da; border-radius: 0.25rem; margin: 0 0.5rem; }
.dataTables_wrapper .dataTables_paginate .paginate_button { color: var(--nexus-text) !important; }
.dataTables_wrapper .dataTables_paginate .paginate_button.current { background: var(--nexus-primary); border-color: var(--nexus-primary); color: white !important; }
/* Forms, badges, buttons, utilities */
.form-control, .form-select { background-color: var(--nexus-darker-bg); border-color: var(--nexus-border); color: var(--nexus-text); }
.form-control::placeholder { color: var(--nexus-text-muted); }
.form-label { color: var(--nexus-text); }
.badge { padding: 0.35em 0.65em; font-weight: 500; }
.btn { transition: all 0.2s ease; }
.btn:hover { transform: translateY(-1px); box-shadow: 0 2px 5px rgba(0,0,0,0.2); }
/* Utilities and accessibility */
.loading { position: relative; pointer-events: none; opacity: 0.6; }
.loading::after { content: ""; position: absolute; top: 50%; left: 50%; width: 2rem; height: 2rem; margin: -1rem 0 0 -1rem; border: 0.25rem solid currentColor; border-right-color: transparent; border-radius: 50%; animation: spinner 0.75s linear infinite; }
@keyframes spinner { to { transform: rotate(360deg); } }
/* Responsive tweaks */
@media (max-width: 768px) { .navbar-brand { font-size: 1rem; } .card { margin-bottom: 1rem; } .alerts-container { left: 10px; right: 10px; max-width: none; } .btn-group { flex-wrap: wrap; } }
/* Extra helpers */
.cursor-pointer { cursor: pointer; }
.text-truncate-2 { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.ws-nowrap { white-space: nowrap; }
/* End of consolidated stylesheet */

View File

@@ -0,0 +1,144 @@
/**
* API Client for DarkflameServer Dashboard
* Provides a simple interface for making API calls with error handling
*/
const API = {
/**
* Base URL for API endpoints
*/
baseURL: '',
/**
* Make a GET request
* @param {string} endpoint - The API endpoint
* @param {object} params - Query parameters
* @returns {Promise<any>} Response data
*/
async get(endpoint, params = {}) {
const url = new URL(this.baseURL + endpoint, window.location.origin);
Object.keys(params).forEach(key => url.searchParams.append(key, params[key]));
const response = await fetch(url, {
method: 'GET',
credentials: 'same-origin',
headers: {
'Accept': 'application/json'
}
});
return this.handleResponse(response);
},
/**
* Make a POST request
* @param {string} endpoint - The API endpoint
* @param {object} data - Request body data
* @returns {Promise<any>} Response data
*/
async post(endpoint, data = {}) {
const response = await fetch(this.baseURL + endpoint, {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify(data)
});
return this.handleResponse(response);
},
/**
* Make a PUT request
* @param {string} endpoint - The API endpoint
* @param {object} data - Request body data
* @returns {Promise<any>} Response data
*/
async put(endpoint, data = {}) {
const response = await fetch(this.baseURL + endpoint, {
method: 'PUT',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify(data)
});
return this.handleResponse(response);
},
/**
* Make a DELETE request
* @param {string} endpoint - The API endpoint
* @returns {Promise<any>} Response data
*/
async delete(endpoint) {
const response = await fetch(this.baseURL + endpoint, {
method: 'DELETE',
credentials: 'same-origin',
headers: {
'Accept': 'application/json'
}
});
return this.handleResponse(response);
},
/**
* Handle fetch response
* @param {Response} response - Fetch response object
* @returns {Promise<any>} Parsed response data
*/
async handleResponse(response) {
const contentType = response.headers.get('content-type');
// Try to parse as JSON first (even if content-type is missing)
try {
const text = await response.text();
// Try to parse as JSON
if (text) {
try {
const data = JSON.parse(text);
if (!response.ok) {
throw new Error(data.error || `HTTP error! status: ${response.status}`);
}
return data;
} catch (jsonError) {
// Not JSON, return as text
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return text;
}
}
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return text;
} catch (error) {
throw error;
}
}
};
/**
* Logout function
*/
async function logout() {
try {
await API.post('/api/logout');
window.location.href = '/login';
} catch (error) {
console.error('Logout error:', error);
// Force redirect even on error
window.location.href = '/login';
}
}

View File

@@ -0,0 +1,188 @@
/**
* Main Dashboard JavaScript
* Common utilities and functions for all pages
*/
/**
* Show an alert message
* @param {string} type - Alert type (success, danger, warning, info)
* @param {string} message - Alert message
* @param {number} duration - Auto-dismiss duration in ms (0 = no auto-dismiss)
*/
function showAlert(type, message, duration = 5000) {
const alertsContainer = document.getElementById('alerts-container') || createAlertsContainer();
const alertId = 'alert-' + Date.now();
const alertHTML = `
<div id="${alertId}" class="alert alert-${type} alert-dismissible fade show" role="alert">
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
`;
alertsContainer.insertAdjacentHTML('beforeend', alertHTML);
if (duration > 0) {
setTimeout(() => {
const alert = document.getElementById(alertId);
if (alert) {
const bsAlert = new bootstrap.Alert(alert);
bsAlert.close();
}
}, duration);
}
}
/**
* Create alerts container if it doesn't exist
*/
function createAlertsContainer() {
const main = document.querySelector('main');
const container = document.createElement('div');
container.id = 'alerts-container';
container.className = 'alerts-container';
main.insertBefore(container, main.firstChild);
return container;
}
/**
* Format timestamp to localized date/time
* @param {number} timestamp - Unix timestamp
* @returns {string} Formatted date/time
*/
function formatTimestamp(timestamp) {
if (!timestamp || timestamp === 0) return '-';
const date = new Date(timestamp * 1000);
return date.toLocaleString();
}
/**
* Format GM level to human-readable name
* @param {number} level - GM level number
* @returns {string} GM level name
*/
function formatGMLevel(level) {
const levels = {
0: 'Civilian',
1: 'Forum Moderator',
2: 'Junior Moderator',
3: 'Moderator',
4: 'Senior Moderator',
5: 'Lead Moderator',
6: 'Junior Developer',
7: 'Inactive Developer',
8: 'Developer',
9: 'Operator'
};
return levels[level] || 'Unknown';
}
/**
* Confirm action with modal
* @param {string} title - Modal title
* @param {string} message - Modal message
* @param {function} callback - Callback function if confirmed
*/
function confirmAction(title, message, callback) {
if (confirm(message)) {
callback();
}
}
/**
* Copy text to clipboard
* @param {string} text - Text to copy
*/
async function copyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
showAlert('success', 'Copied to clipboard!', 2000);
} catch (err) {
showAlert('danger', 'Failed to copy to clipboard');
}
}
/**
* Debounce function calls
* @param {function} func - Function to debounce
* @param {number} wait - Wait time in ms
* @returns {function} Debounced function
*/
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
/**
* Initialize DataTables default settings
*/
$.extend(true, $.fn.dataTable.defaults, {
responsive: true,
lengthMenu: [[10, 25, 50, 100, -1], [10, 25, 50, 100, "All"]],
pageLength: 25,
language: {
search: "_INPUT_",
searchPlaceholder: "Search...",
lengthMenu: "Show _MENU_ entries",
info: "Showing _START_ to _END_ of _TOTAL_ entries",
infoEmpty: "No entries found",
infoFiltered: "(filtered from _MAX_ total entries)",
zeroRecords: "No matching records found",
emptyTable: "No data available in table"
}
});
/**
* Handle form submission with API
* @param {string} formId - Form element ID
* @param {string} endpoint - API endpoint
* @param {function} onSuccess - Success callback
*/
function handleFormSubmit(formId, endpoint, onSuccess) {
const form = document.getElementById(formId);
if (!form) return;
form.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(form);
const data = Object.fromEntries(formData);
try {
const result = await API.post(endpoint, data);
if (result.success) {
showAlert('success', result.message || 'Operation successful');
if (onSuccess) onSuccess(result);
} else {
showAlert('danger', result.error || 'Operation failed');
}
} catch (error) {
showAlert('danger', error.message);
}
});
}
/**
* Initialize tooltips
*/
document.addEventListener('DOMContentLoaded', function() {
// Initialize Bootstrap tooltips
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipTriggerList.map(function(tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl);
});
// Initialize Bootstrap popovers
const popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'));
popoverTriggerList.map(function(popoverTriggerEl) {
return new bootstrap.Popover(popoverTriggerEl);
});
});

View File

@@ -0,0 +1,46 @@
/**
* Login page functionality
*/
// Function to initialize login form
function initLoginForm() {
const form = document.getElementById('login-form');
if (!form) return; // Not on login page
form.addEventListener('submit', async (e) => {
e.preventDefault();
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const messageDiv = document.getElementById('login-message');
try {
const response = await API.post('/api/login', { username, password });
if (response && response.success) {
messageDiv.className = 'alert alert-success';
messageDiv.textContent = 'Login successful! Redirecting...';
messageDiv.style.display = 'block';
setTimeout(() => {
window.location.href = '/';
}, 1000);
} else {
messageDiv.className = 'alert alert-danger';
messageDiv.textContent = response.error || 'Login failed';
messageDiv.style.display = 'block';
}
} catch (error) {
messageDiv.className = 'alert alert-danger';
messageDiv.textContent = error.message || 'An error occurred during login';
messageDiv.style.display = 'block';
}
});
}
// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initLoginForm);
} else {
initLoginForm();
}

View File

@@ -0,0 +1,43 @@
document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById('register-form');
const alertBox = document.getElementById('register-alert');
form.addEventListener('submit', async (e) => {
e.preventDefault();
alertBox.style.display = 'none';
const username = document.getElementById('username').value.trim();
const password = document.getElementById('password').value;
const play_key = document.getElementById('play_key').value.trim();
try {
const res = await fetch('/api/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password, play_key })
});
const data = await res.json();
if (!res.ok) {
alertBox.className = 'alert alert-danger';
alertBox.textContent = data.error || 'Registration failed';
alertBox.style.display = 'block';
return;
}
if (data.success) {
alertBox.className = 'alert alert-success';
alertBox.textContent = 'Account created successfully. You can now log in.';
alertBox.style.display = 'block';
form.reset();
} else {
alertBox.className = 'alert alert-danger';
alertBox.textContent = data.error || 'Registration failed';
alertBox.style.display = 'block';
}
} catch (err) {
alertBox.className = 'alert alert-danger';
alertBox.textContent = err.message || 'Registration failed';
alertBox.style.display = 'block';
}
});
});

View File

@@ -0,0 +1,75 @@
// Helper to wait for jQuery and DataTables (and optionally API) to be available
// Usage:
// safeInit(callback, { timeout: 5000, interval: 100, requireApi: false })
// The callback receives `window.jQuery` as its first argument.
(function(window) {
'use strict';
function waitFor(conditionFn, timeoutMs, intervalMs) {
return new Promise((resolve, reject) => {
const start = Date.now();
const iv = setInterval(() => {
try {
if (conditionFn()) {
clearInterval(iv);
resolve();
return;
}
} catch (e) {
// ignore
}
if (Date.now() - start > timeoutMs) {
clearInterval(iv);
reject(new Error('waitFor: timed out'));
}
}, intervalMs);
});
}
async function safeInit(cb, opts) {
opts = opts || {};
const timeout = typeof opts.timeout === 'number' ? opts.timeout : 5000;
const interval = typeof opts.interval === 'number' ? opts.interval : 100;
const requireApi = !!opts.requireApi;
// Wait for DOM ready first so scripts included at end of body have run
if (document.readyState === 'loading') {
await new Promise(r => document.addEventListener('DOMContentLoaded', r, { once: true }));
}
try {
await waitFor(() => window.jQuery && window.jQuery.fn && window.jQuery.fn.DataTable, timeout, interval);
if (requireApi) {
await waitFor(() => window.API, timeout, interval);
}
// call callback with jQuery
try { cb(window.jQuery); } catch (e) { console.error('safeInit callback error', e); }
} catch (err) {
console.error('safeInit: required libraries failed to load', err);
// If callback provided an onError handler, call it
if (opts.onError && typeof opts.onError === 'function') {
try { opts.onError(err); } catch (e) { console.error(e); }
} else {
// default fallback: show a banner if possible
const tableEls = document.querySelectorAll('table');
if (tableEls && tableEls.length) {
tableEls.forEach(el => {
const wrapper = document.createElement('div');
wrapper.className = 'alert alert-danger';
wrapper.textContent = 'Required JavaScript libraries failed to load (jQuery/DataTables). Please check your network or CDN allowlist.';
el.replaceWith(wrapper);
});
} else {
console.warn('safeInit: libraries missing');
}
}
}
}
// Expose globally
window.safeInit = safeInit;
window.waitForLibraries = function(timeoutMs, intervalMs) {
return waitFor(() => window.jQuery && window.jQuery.fn && window.jQuery.fn.DataTable, timeoutMs || 5000, intervalMs || 100);
};
})(window);

View File

@@ -0,0 +1,102 @@
<div class="row">
<div class="col-12">
<h1 class="mb-4">
<i class="bi bi-info-circle"></i>
About DarkflameServer Dashboard
</h1>
</div>
</div>
<div class="row">
<div class="col-md-8 offset-md-2">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Dashboard Information</h5>
</div>
<div class="card-body">
<h4 class="mb-3">DarkflameServer Web Dashboard</h4>
<p class="lead">
A modern C++ web interface for managing your Darkflame Universe server.
</p>
<hr class="my-4">
<h5>Features</h5>
<ul>
<li><strong>Account Management:</strong> Create, modify, ban, lock, and mute player accounts</li>
<li><strong>Character Management:</strong> View, rescue, and manage player characters</li>
<li><strong>Moderation Tools:</strong> Approve pet names, manage properties, and review bug reports</li>
<li><strong>Mail System:</strong> Send in-game mail to players with item attachments</li>
<li><strong>Play Keys:</strong> Manage registration keys for new accounts</li>
<li><strong>Activity Logs:</strong> Monitor player activity and track logins/logouts</li>
<li><strong>Audit Trail:</strong> Track all administrative actions for accountability</li>
</ul>
<hr class="my-4">
<h5>Technology Stack</h5>
<ul>
<li><strong>Backend:</strong> C++ with Crow web framework</li>
<li><strong>Frontend:</strong> Bootstrap 5, jQuery, DataTables</li>
<li><strong>Templates:</strong> Mustache templating engine</li>
<li><strong>Database:</strong> MySQL/MariaDB or SQLite</li>
</ul>
<hr class="my-4">
<h5>GM Levels</h5>
<dl class="row">
<dt class="col-sm-3">Level 0</dt>
<dd class="col-sm-9"><span class="badge bg-secondary">Civilian</span> - Regular player</dd>
<dt class="col-sm-3">Level 1</dt>
<dd class="col-sm-9"><span class="badge bg-info">Forum Moderator</span> - Forum moderation only</dd>
<dt class="col-sm-3">Level 2</dt>
<dd class="col-sm-9"><span class="badge bg-primary">Junior Moderator</span> - Basic moderation tools</dd>
<dt class="col-sm-3">Level 3</dt>
<dd class="col-sm-9"><span class="badge bg-success">Moderator</span> - Full moderation access</dd>
<dt class="col-sm-3">Level 4</dt>
<dd class="col-sm-9"><span class="badge bg-success">Senior Moderator</span> - Advanced moderation</dd>
<dt class="col-sm-3">Level 5</dt>
<dd class="col-sm-9"><span class="badge bg-warning">Lead Moderator</span> - Moderation leadership</dd>
<dt class="col-sm-3">Level 6</dt>
<dd class="col-sm-9"><span class="badge bg-warning">Junior Developer</span> - Development access</dd>
<dt class="col-sm-3">Level 7</dt>
<dd class="col-sm-9"><span class="badge bg-warning">Inactive Developer</span> - Limited dev access</dd>
<dt class="col-sm-3">Level 8</dt>
<dd class="col-sm-9"><span class="badge bg-danger">Developer</span> - Full development access</dd>
<dt class="col-sm-3">Level 9</dt>
<dd class="col-sm-9"><span class="badge bg-danger">Operator</span> - Full system access</dd>
</dl>
<hr class="my-4">
<h5>About Darkflame Universe</h5>
<p>
DarkflameServer is an open-source server emulator for LEGO Universe,
a massively multiplayer online game that was officially discontinued in 2012.
The Darkflame Universe project aims to preserve and revive this beloved game
for fans to continue enjoying.
</p>
<div class="d-grid gap-2 d-md-flex justify-content-md-start mt-4">
<a href="https://github.com/DarkflameUniverse/DarkflameServer" target="_blank" class="btn btn-primary">
<i class="bi bi-github"></i> GitHub Repository
</a>
<a href="https://github.com/DarkflameUniverse/DarkflameServer/tree/main/docs" target="_blank" class="btn btn-secondary">
<i class="bi bi-book"></i> Documentation
</a>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,162 @@
<div class="row">
<div class="col-12">
<h1 class="mb-4">
<i class="bi bi-people"></i>
Account Management
</h1>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0">All Accounts</h5>
</div>
<div class="card-body">
<table id="accounts-table" class="table table-striped table-hover">
<thead>
<tr>
<th>ID</th>
<th>Username</th>
<th>GM Level</th>
<th>Banned</th>
<th>Locked</th>
<th>Muted Until</th>
<th>Play Key ID</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<!-- Populated via DataTables Ajax -->
</tbody>
</table>
</div>
</div>
</div>
</div>
<script>
// Wait for jQuery + DataTables to be available without copying libraries locally.
// Poll for a limited time and show a helpful error if they fail to load.
function showLibraryError(message) {
const el = document.getElementById('accounts-table');
if (el) {
const wrapper = document.createElement('div');
wrapper.className = 'alert alert-danger';
wrapper.textContent = message;
el.replaceWith(wrapper);
} else {
alert(message);
}
}
// Use the same pattern as Recent Activity: wait for DOMContentLoaded, check auth, then fetch data
function loadAccounts() {
API.get('/api/auth/status').then(status => {
if (!status || !status.authenticated || status.gm_level < 3) {
showLibraryError('You do not have permission to view accounts. Please log in with sufficient GM level.');
return;
}
API.get('/api/accounts').then(res => {
const data = Array.isArray(res.data) ? res.data : (res || []);
if ($.fn.DataTable.isDataTable('#accounts-table')) {
const table = $('#accounts-table').DataTable();
table.clear();
table.rows.add(data);
table.draw(false);
} else {
const table = $('#accounts-table').DataTable({
data: data,
columns: [
{ data: 'id' },
{ data: 'name', render: function(d, t, row) { return `<a href="/accounts/view/${row.id}">${d}</a>`; } },
{ data: 'gm_level', render: function(d) { const badges={0:'secondary',1:'info',2:'primary',3:'success',4:'success',5:'warning',6:'warning',7:'warning',8:'danger',9:'danger'}; return `<span class="badge bg-${badges[d]||'secondary'}">${d}</span>`; } },
{ data: 'banned', render: d => d ? '<span class="badge bg-danger">Yes</span>' : '<span class="badge bg-success">No</span>' },
{ data: 'locked', render: d => d ? '<span class="badge bg-warning">Yes</span>' : '<span class="badge bg-success">No</span>' },
{ data: 'mute_expire', render: function(d) { if (!d || d === 0) return '-'; return new Date(d * 1000).toLocaleString(); } },
{ data: 'play_key_id' },
{ data: null, orderable: false, render: function(data, type, row) {
return `
<div class="btn-group btn-group-sm" role="group">
<a href="/accounts/view/${row.id}" class="btn btn-info" title="View"><i class="bi bi-eye"></i></a>
<button data-account-id="${row.id}" class="btn btn-warning js-toggle-lock" title="Lock/Unlock"><i class="bi bi-lock"></i></button>
<button data-account-id="${row.id}" class="btn btn-danger js-toggle-ban" title="Ban/Unban"><i class="bi bi-slash-circle"></i></button>
<button data-account-id="${row.id}" class="btn btn-secondary js-mute-account" title="Mute"><i class="bi bi-mic-mute"></i></button>
</div>`;
} }
],
pageLength: 25,
order: [[0, 'asc']],
processing: true
});
// Delegated event handlers
$('#accounts-table').on('click', '.js-toggle-lock', function() { const id = $(this).data('account-id'); toggleLock(id, table); });
$('#accounts-table').on('click', '.js-toggle-ban', function() { const id = $(this).data('account-id'); toggleBan(id, table); });
$('#accounts-table').on('click', '.js-mute-account', function() { const id = $(this).data('account-id'); muteAccount(id, table); });
}
}).catch(err => {
const msg = err && err.message ? err.message : 'Failed to load accounts';
showLibraryError(`Error loading accounts: ${msg}`);
});
}).catch(err => {
showLibraryError(`Error checking authentication: ${err && err.message ? err.message : err}`);
});
}
// Initialize when jQuery/DataTables and API are ready
safeInit(function($) {
loadAccounts();
}, { requireApi: true, timeout: 8000 });
async function toggleLock(accountId, table) {
if (!confirm('Are you sure you want to toggle the lock status for this account?')) return;
try {
const result = await API.post(`/api/accounts/${accountId}/lock`);
if (result.success) {
if (table && table.ajax) table.ajax.reload();
showAlert('success', 'Account lock status updated');
} else {
showAlert('danger', result.error || 'Failed to update account');
}
} catch (error) {
showAlert('danger', error.message || error);
}
}
async function toggleBan(accountId, table) {
if (!confirm('Are you sure you want to toggle the ban status for this account?')) return;
try {
const result = await API.post(`/api/accounts/${accountId}/ban`);
if (result.success) {
if (table && table.ajax) table.ajax.reload();
showAlert('success', 'Account ban status updated');
} else {
showAlert('danger', result.error || 'Failed to update account');
}
} catch (error) {
showAlert('danger', error.message || error);
}
}
async function muteAccount(accountId, table) {
const days = prompt('Enter number of days to mute (0 to unmute):');
if (days === null) return;
try {
const result = await API.post(`/api/accounts/${accountId}/mute`, { days: parseInt(days) });
if (result.success) {
if (table && table.ajax) table.ajax.reload();
showAlert('success', 'Account mute status updated');
} else {
showAlert('danger', result.error || 'Failed to update account');
}
} catch (error) {
showAlert('danger', error.message || error);
}
}
</script>

View File

@@ -0,0 +1,214 @@
<div class="row">
<div class="col-12">
<h1 class="mb-4"><i class="bi bi-person-circle"></i> Account Details</h1>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-header">Account Info</div>
<div class="card-body">
<dl class="row">
<dt class="col-sm-4">ID</dt><dd class="col-sm-8" id="acct-id">-</dd>
<dt class="col-sm-4">Username</dt><dd class="col-sm-8" id="acct-name">-</dd>
<dt class="col-sm-4">Email</dt><dd class="col-sm-8" id="acct-email">-</dd>
<dt class="col-sm-4">GM Level</dt><dd class="col-sm-8" id="acct-gm">-</dd>
<dt class="col-sm-4">Banned</dt><dd class="col-sm-8" id="acct-banned">-</dd>
<dt class="col-sm-4">Locked</dt><dd class="col-sm-8" id="acct-locked">-</dd>
<dt class="col-sm-4">Mute Expire</dt><dd class="col-sm-8" id="acct-mute">-</dd>
<dt class="col-sm-4">Play Key ID</dt><dd class="col-sm-8" id="acct-playkey">-</dd>
</dl>
</div>
</div>
<div class="card mt-3">
<div class="card-header">Administrative Actions</div>
<div class="card-body">
<button class="btn btn-danger mb-2" id="delete-account">Delete Account</button>
<hr>
<div class="mb-3">
<label class="form-label">Set GM Level</label>
<input type="number" id="gm-level-input" class="form-control" min="0" max="9">
<button class="btn btn-primary mt-2" id="set-gm">Update GM Level</button>
</div>
<div class="mb-3">
<label class="form-label">Update Email</label>
<input type="email" id="email-input" class="form-control">
<button class="btn btn-primary mt-2" id="set-email">Update Email</button>
</div>
<div class="mb-3">
<label class="form-label">Reset Password</label>
<input type="password" id="password-input" class="form-control">
<button class="btn btn-primary mt-2" id="reset-password">Reset Password</button>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">Characters</div>
<div class="card-body">
<table id="characters-table" class="table table-striped table-hover">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Level</th>
<th>Map</th>
<th>Last Login</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<div class="card mt-3">
<div class="card-header">Sessions</div>
<div class="card-body">
<table id="sessions-table" class="table table-striped table-hover">
<thead>
<tr>
<th>Session ID</th>
<th>IP Address</th>
<th>Login Time</th>
<th>Logout Time</th>
<th>Active</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
<script>
const accountId = (window.location.pathname.split('/').pop() || '').trim();
async function loadAccount() {
try {
const res = await API.get(`/api/accounts/${accountId}`);
if (res && res.success) {
$('#acct-id').text(res.id);
$('#acct-name').text(res.name);
$('#acct-email').text(res.email || '-');
$('#acct-gm').text(res.gm_level);
$('#acct-banned').html(res.banned ? '<span class="badge bg-danger">Yes</span>' : '<span class="badge bg-success">No</span>');
$('#acct-locked').html(res.locked ? '<span class="badge bg-warning">Yes</span>' : '<span class="badge bg-success">No</span>');
$('#acct-mute').text(res.mute_expire && res.mute_expire>0 ? new Date(res.mute_expire*1000).toLocaleString() : '-');
$('#acct-playkey').text(res.play_key_id || '-');
$('#gm-level-input').val(res.gm_level);
$('#email-input').val(res.email || '');
// Load related data
loadCharacters();
loadSessions();
} else {
alert(res.error || 'Failed to load account');
}
} catch (err) { alert(err.message); }
}
async function loadCharacters() {
try {
const res = await API.get(`/api/accounts/${accountId}/characters`);
const data = (res && Array.isArray(res.data)) ? res.data : (res || []);
if ($.fn.DataTable.isDataTable('#characters-table')) {
const table = $('#characters-table').DataTable();
table.clear();
table.rows.add(data);
table.draw(false);
} else {
$('#characters-table').DataTable({
data: data,
columns: [
{ data: 'id' },
{ data: 'name', render: function(d, t, row) { return `<a href="/characters/view/${row.id}">${d}</a>`; } },
{ data: 'level' },
{ data: 'map_id' },
{ data: 'last_login', render: d => d ? new Date(d * 1000).toLocaleString() : '-' }
],
order: [[0, 'desc']],
pageLength: 10
});
}
} catch (err) {
console.error('Failed to load characters', err);
}
}
async function loadSessions() {
try {
const res = await API.get(`/api/accounts/${accountId}/sessions`);
const data = (res && Array.isArray(res.data)) ? res.data : (res || []);
if ($.fn.DataTable.isDataTable('#sessions-table')) {
const table = $('#sessions-table').DataTable();
table.clear();
table.rows.add(data);
table.draw(false);
} else {
$('#sessions-table').DataTable({
data: data,
columns: [
{ data: 'session_id' },
{ data: 'ip_address' },
{ data: 'login_time', render: d => d ? new Date(d * 1000).toLocaleString() : '-' },
{ data: 'logout_time', render: d => d && d>0 ? new Date(d * 1000).toLocaleString() : '-' },
{ data: 'active', render: d => d ? '<span class="badge bg-success">Yes</span>' : '<span class="badge bg-secondary">No</span>' }
],
order: [[2, 'desc']],
pageLength: 10
});
}
} catch (err) {
console.error('Failed to load sessions', err);
}
}
// Initialize when libraries are ready (API used, jQuery optional)
safeInit(function($) {
loadAccount();
document.getElementById('delete-account').addEventListener('click', async function() {
if (!confirm('Delete this account? This action is irreversible.')) return;
try {
const res = await API.post(`/api/accounts/${accountId}/delete`, {});
if (res && res.success) {
alert('Account deleted');
window.location.href = '/accounts';
} else {
alert(res.error || 'Failed to delete');
}
} catch (err) { alert(err.message); }
});
document.getElementById('set-gm').addEventListener('click', async function() {
const lvl = parseInt(document.getElementById('gm-level-input').value);
try {
const res = await API.post(`/api/accounts/${accountId}/gm-level`, { gm_level: lvl });
if (res && res.success) { alert('GM level updated'); loadAccount(); } else { alert(res.error || 'Failed'); }
} catch (err) { alert(err.message); }
});
document.getElementById('set-email').addEventListener('click', async function() {
const email = document.getElementById('email-input').value.trim();
try {
const res = await API.post(`/api/accounts/${accountId}/email`, { email: email });
if (res && res.success) { alert('Email updated'); loadAccount(); } else { alert(res.error || 'Failed'); }
} catch (err) { alert(err.message); }
});
document.getElementById('reset-password').addEventListener('click', async function() {
const pw = document.getElementById('password-input').value;
if (!pw || pw.length < 8) { alert('Password must be at least 8 characters'); return; }
try {
const res = await API.post(`/api/accounts/${accountId}/password-reset`, { password: pw });
if (res && res.success) { alert('Password reset'); } else { alert(res.error || 'Failed'); }
} catch (err) { alert(err.message); }
});
}, { requireApi: true, timeout: 8000 });
</script>

View File

@@ -0,0 +1,151 @@
<div class="row">
<div class="col-12">
<h1 class="mb-4"><i class="bi bi-bug"></i> Bug Reports</h1>
</div>
</div>
<div class="row mb-3">
<div class="col-md-4">
<div class="list-group" id="report-filter">
<button class="list-group-item list-group-item-action active" data-status="all">All</button>
<button class="list-group-item list-group-item-action" data-status="unresolved">Unresolved</button>
<button class="list-group-item list-group-item-action" data-status="resolved">Resolved</button>
</div>
</div>
<div class="col-md-8">
<div class="card">
<div class="card-header">Reports</div>
<div class="card-body">
<table id="bugreports-table" class="table table-striped table-hover">
<thead>
<tr>
<th>ID</th>
<th>Character</th>
<th>Submitted</th>
<th>Resolved</th>
<th>Summary</th>
<th>Actions</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Modal for resolving -->
<div class="modal" tabindex="-1" id="resolveModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Resolve Bug Report</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Resolution Message</label>
<textarea id="resolution-text" class="form-control" rows="4"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="resolve-confirm">Resolve</button>
</div>
</div>
</div>
</div>
<script>
let currentStatus = 'all';
let currentResolveId = 0;
function loadTable() {
API.get('/api/bugreports', { status: currentStatus }).then(res => {
const data = Array.isArray(res.data) ? res.data : (res || []);
if ($.fn.DataTable.isDataTable('#bugreports-table')) {
const table = $('#bugreports-table').DataTable();
table.clear();
table.rows.add(data);
table.draw(false);
} else {
$('#bugreports-table').DataTable({
data: data,
destroy: true,
columns: [
{ data: 'id' },
{ data: 'character_name' },
{ data: 'submitted', render: d => d ? new Date(d * 1000).toLocaleString() : '-' },
{ data: 'resolved_time', render: d => d && d>0 ? new Date(d * 1000).toLocaleString() : '-' },
{ data: 'body', render: d => d ? d.substring(0,120) : '-' },
{ data: null, orderable: false, render: function(data, type, row) {
let actions = '';
if (!row.resolved_time || row.resolved_time == 0) {
actions += `<button class="btn btn-sm btn-success" onclick="openResolve(${row.id})">Resolve</button>`;
}
actions += ` <button class="btn btn-sm btn-info" onclick="viewReport(${row.id})">View</button>`;
return actions;
} }
],
order: [[0, 'desc']],
pageLength: 25
});
}
}).catch(err => {
alert(err && err.message ? err.message : 'Failed to load bug reports');
});
}
// Initialize when libraries are ready
safeInit(function($) {
loadTable();
// Filter clicks
$('#report-filter button').on('click', function() {
$('#report-filter button').removeClass('active');
$(this).addClass('active');
currentStatus = $(this).data('status');
loadTable();
});
// Resolve confirm
$('#resolve-confirm').on('click', async function() {
const resolution = $('#resolution-text').val().trim();
if (!resolution) { alert('Resolution message required'); return; }
try {
const res = await API.post(`/api/bugreports/${currentResolveId}/resolve`, { resolution: resolution });
if (res && res.success) {
$('#resolveModal').modal('hide');
loadTable();
alert('Bug report resolved');
} else {
alert(res.error || 'Failed to resolve');
}
} catch (err) {
alert(err.message);
}
});
}, { requireApi: true, timeout: 8000 });
function openResolve(id) {
currentResolveId = id;
$('#resolution-text').val('');
var modal = new bootstrap.Modal(document.getElementById('resolveModal'));
modal.show();
}
async function viewReport(id) {
try {
const res = await API.get(`/api/bugreports/${id}`);
if (res && res.success) {
const text = `ID: ${res.id}\nCharacter: ${res.character_name}\nSubmitted: ${res.submitted?new Date(res.submitted*1000).toLocaleString():''}\n\n${res.body}`;
alert(text);
} else {
alert(res.error || 'Failed to get report');
}
} catch (err) {
alert(err.message);
}
}
</script>

View File

@@ -0,0 +1,163 @@
<div class="row">
<div class="col-12">
<h1 class="mb-4">
<i class="bi bi-person-badge"></i>
Character Management
</h1>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0">All Characters</h5>
</div>
<div class="card-body">
<table id="characters-table" class="table table-striped table-hover">
<thead>
<tr>
<th>ID</th>
<th>Account</th>
<th>Name</th>
<th>Pending Name</th>
<th>Needs Rename</th>
<th>Last Login</th>
<th>Permission Map</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<!-- Populated via DataTables Ajax -->
</tbody>
</table>
</div>
</div>
</div>
</div>
<script>
// Wait for jQuery + DataTables to be available
function showLibraryError(message) {
const el = document.getElementById('characters-table');
if (el) {
const wrapper = document.createElement('div');
wrapper.className = 'alert alert-danger';
wrapper.textContent = message;
el.replaceWith(wrapper);
} else {
alert(message);
}
}
function loadCharacters() {
API.get('/api/auth/status').then(status => {
if (!status || !status.authenticated || status.gm_level < 3) {
showLibraryError('You do not have permission to view characters. Please log in with sufficient GM level.');
return;
}
API.get('/api/characters').then(res => {
const data = Array.isArray(res.data) ? res.data : (res || []);
if ($.fn.DataTable.isDataTable('#characters-table')) {
const table = $('#characters-table').DataTable();
table.clear();
table.rows.add(data);
table.draw(false);
} else {
const table = $('#characters-table').DataTable({
data: data,
columns: [
{ data: 'id' },
{ data: 'account_name', render: function(d, t, row) {
return row.account_id ? `<a href="/accounts/view/${row.account_id}">${d || row.account_id}</a>` : (d || '-');
}},
{ data: 'name', render: function(d, t, row) {
return `<a href="/characters/view/${row.id}">${d}</a>`;
}},
{ data: 'pending_name', render: d => d || '-' },
{ data: 'needs_rename', render: d => d ? '<span class="badge bg-warning">Yes</span>' : '<span class="badge bg-success">No</span>' },
{ data: 'last_login', render: function(d) {
if (!d || d === 0) return 'Never';
return new Date(d * 1000).toLocaleString();
}},
{ data: 'permission_map', render: d => d || '-' },
{ data: null, orderable: false, render: function(data, type, row) {
return `
<div class="btn-group btn-group-sm" role="group">
<a href="/characters/view/${row.id}" class="btn btn-info" title="View"><i class="bi bi-eye"></i></a>
<button data-char-id="${row.id}" class="btn btn-warning js-rescue-char" title="Rescue Character"><i class="bi bi-life-preserver"></i></button>
<button data-char-id="${row.id}" class="btn btn-danger js-delete-char" title="Delete Character"><i class="bi bi-trash"></i></button>
</div>`;
}}
],
pageLength: 25,
order: [[0, 'desc']],
processing: true
});
// Delegated event handlers
$('#characters-table').on('click', '.js-rescue-char', function() {
const id = $(this).data('char-id');
rescueCharacter(id, table);
});
$('#characters-table').on('click', '.js-delete-char', function() {
const id = $(this).data('char-id');
deleteCharacter(id, table);
});
}
}).catch(err => {
const msg = err && err.message ? err.message : 'Failed to load characters';
showLibraryError(`Error loading characters: ${msg}`);
});
}).catch(err => {
showLibraryError(`Error checking authentication: ${err && err.message ? err.message : err}`);
});
}
// Initialize when jQuery/DataTables and API are ready
safeInit(function($) {
loadCharacters();
}, { requireApi: true, timeout: 8000 });
async function rescueCharacter(charId, table) {
if (!confirm('Are you sure you want to rescue this character? This will move them to a safe location.')) return;
try {
const result = await API.post(`/api/characters/${charId}/rescue`);
if (result.success) {
showAlert('success', 'Character rescued successfully');
if (table && table.ajax) table.ajax.reload();
} else {
showAlert('danger', result.error || 'Failed to rescue character');
}
} catch (error) {
showAlert('danger', error.message || error);
}
}
async function deleteCharacter(charId, table) {
const confirmMsg = 'Are you sure you want to DELETE this character? This action is irreversible!';
if (!confirm(confirmMsg)) return;
const doubleConfirm = prompt('Type "DELETE" to confirm:');
if (doubleConfirm !== 'DELETE') {
showAlert('info', 'Deletion cancelled');
return;
}
try {
const result = await API.post(`/api/characters/${charId}/delete`);
if (result.success) {
showAlert('success', 'Character deleted');
if (table && table.ajax) table.ajax.reload();
} else {
showAlert('danger', result.error || 'Failed to delete character');
}
} catch (error) {
showAlert('danger', error.message || error);
}
}
</script>

View File

@@ -0,0 +1,314 @@
<div class="row">
<div class="col-12">
<h1 class="mb-4"><i class="bi bi-person-badge"></i> Character Details</h1>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-header">Character Info</div>
<div class="card-body">
<dl class="row">
<dt class="col-sm-5">ID</dt><dd class="col-sm-7" id="char-id">-</dd>
<dt class="col-sm-5">Name</dt><dd class="col-sm-7" id="char-name">-</dd>
<dt class="col-sm-5">Pending Name</dt><dd class="col-sm-7" id="char-pending-name">-</dd>
<dt class="col-sm-5">Account</dt><dd class="col-sm-7" id="char-account">-</dd>
<dt class="col-sm-5">Level</dt><dd class="col-sm-7" id="char-level">-</dd>
<dt class="col-sm-5">Universe Score</dt><dd class="col-sm-7" id="char-uscore">-</dd>
<dt class="col-sm-5">Current Zone</dt><dd class="col-sm-7" id="char-zone">-</dd>
<dt class="col-sm-5">Last Login</dt><dd class="col-sm-7" id="char-last-login">-</dd>
<dt class="col-sm-5">Created</dt><dd class="col-sm-7" id="char-created">-</dd>
</dl>
</div>
</div>
<div class="card mt-3">
<div class="card-header">Restrictions</div>
<div class="card-body">
<dl class="row">
<dt class="col-sm-5">Mail Restricted</dt><dd class="col-sm-7" id="char-mail-restricted">-</dd>
<dt class="col-sm-5">Trade Restricted</dt><dd class="col-sm-7" id="char-trade-restricted">-</dd>
<dt class="col-sm-5">Chat Restricted</dt><dd class="col-sm-7" id="char-chat-restricted">-</dd>
<dt class="col-sm-5">Needs Rename</dt><dd class="col-sm-7" id="char-needs-rename">-</dd>
</dl>
</div>
</div>
<div class="card mt-3">
<div class="card-header">Administrative Actions</div>
<div class="card-body">
<button class="btn btn-warning mb-2 w-100" id="rescue-char">
<i class="bi bi-life-preserver"></i> Rescue Character
</button>
<button class="btn btn-primary mb-2 w-100" id="approve-name">
<i class="bi bi-check-circle"></i> Approve Pending Name
</button>
<button class="btn btn-danger mb-2 w-100" id="delete-char">
<i class="bi bi-trash"></i> Delete Character
</button>
<hr>
<div class="mb-3">
<label class="form-label">Toggle Mail Restriction</label>
<button class="btn btn-secondary w-100" id="toggle-mail">Toggle Mail</button>
</div>
<div class="mb-3">
<label class="form-label">Toggle Trade Restriction</label>
<button class="btn btn-secondary w-100" id="toggle-trade">Toggle Trade</button>
</div>
<div class="mb-3">
<label class="form-label">Toggle Chat Restriction</label>
<button class="btn btn-secondary w-100" id="toggle-chat">Toggle Chat</button>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<ul class="nav nav-tabs card-header-tabs" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="stats-tab" data-bs-toggle="tab" href="#stats" role="tab">Stats</a>
</li>
<li class="nav-item">
<a class="nav-link" id="inventory-tab" data-bs-toggle="tab" href="#inventory" role="tab">Inventory</a>
</li>
<li class="nav-item">
<a class="nav-link" id="activity-tab" data-bs-toggle="tab" href="#activity" role="tab">Activity</a>
</li>
</ul>
</div>
<div class="card-body">
<div class="tab-content">
<div class="tab-pane fade show active" id="stats" role="tabpanel">
<h6>Character Statistics</h6>
<div id="char-stats-content">
<dl class="row">
<dt class="col-sm-6">Total Currency Collected</dt><dd class="col-sm-6" id="stat-currency">-</dd>
<dt class="col-sm-6">Total Bricks Collected</dt><dd class="col-sm-6" id="stat-bricks">-</dd>
<dt class="col-sm-6">Total Smashables</dt><dd class="col-sm-6" id="stat-smashables">-</dd>
<dt class="col-sm-6">Total Quick Builds</dt><dd class="col-sm-6" id="stat-quickbuilds">-</dd>
<dt class="col-sm-6">Total Enemies Smashed</dt><dd class="col-sm-6" id="stat-enemies">-</dd>
<dt class="col-sm-6">Total Rockets Used</dt><dd class="col-sm-6" id="stat-rockets">-</dd>
<dt class="col-sm-6">Total Missions Completed</dt><dd class="col-sm-6" id="stat-missions">-</dd>
<dt class="col-sm-6">Total Pets Tamed</dt><dd class="col-sm-6" id="stat-pets">-</dd>
</dl>
</div>
</div>
<div class="tab-pane fade" id="inventory" role="tabpanel">
<h6>Inventory Items</h6>
<div id="char-inventory-content">
<table class="table table-sm table-striped">
<thead>
<tr>
<th>Item ID</th>
<th>Count</th>
<th>Slot</th>
</tr>
</thead>
<tbody id="inventory-tbody">
<tr><td colspan="3" class="text-center">Loading...</td></tr>
</tbody>
</table>
</div>
</div>
<div class="tab-pane fade" id="activity" role="tabpanel">
<h6>Recent Activity</h6>
<div id="char-activity-content">
<table class="table table-sm table-striped">
<thead>
<tr>
<th>Timestamp</th>
<th>Activity</th>
<th>Map</th>
</tr>
</thead>
<tbody id="activity-tbody">
<tr><td colspan="3" class="text-center">Loading...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
const characterId = (window.location.pathname.split('/').pop() || '').trim();
async function loadCharacter() {
try {
const res = await API.get(`/api/characters/${characterId}`);
if (res && res.success) {
$('#char-id').text(res.id);
$('#char-name').text(res.name);
$('#char-pending-name').text(res.pending_name || '-');
if (res.account_id) {
$('#char-account').html(`<a href="/accounts/view/${res.account_id}">${res.account_name || res.account_id}</a>`);
} else {
$('#char-account').text('-');
}
$('#char-level').text(res.level || 0);
$('#char-uscore').text(res.uscore || 0);
$('#char-zone').text(res.zone_id || '-');
$('#char-last-login').text(res.last_login && res.last_login > 0 ? new Date(res.last_login * 1000).toLocaleString() : 'Never');
$('#char-created').text(res.created_on ? new Date(res.created_on * 1000).toLocaleString() : '-');
// Restrictions
$('#char-mail-restricted').html(res.mail_restricted ? '<span class="badge bg-danger">Yes</span>' : '<span class="badge bg-success">No</span>');
$('#char-trade-restricted').html(res.trade_restricted ? '<span class="badge bg-danger">Yes</span>' : '<span class="badge bg-success">No</span>');
$('#char-chat-restricted').html(res.chat_restricted ? '<span class="badge bg-danger">Yes</span>' : '<span class="badge bg-success">No</span>');
$('#char-needs-rename').html(res.needs_rename ? '<span class="badge bg-warning">Yes</span>' : '<span class="badge bg-success">No</span>');
// Load related data
loadCharacterStats();
loadCharacterActivity();
} else {
alert(res.error || 'Failed to load character');
}
} catch (err) {
alert(err.message || 'Error loading character');
}
}
async function loadCharacterStats() {
try {
const res = await API.get(`/api/characters/${characterId}/stats`);
if (res && res.success) {
$('#stat-currency').text(res.total_currency_collected || 0);
$('#stat-bricks').text(res.total_bricks_collected || 0);
$('#stat-smashables').text(res.total_smashables || 0);
$('#stat-quickbuilds').text(res.total_quickbuilds_completed || 0);
$('#stat-enemies').text(res.total_enemies_smashed || 0);
$('#stat-rockets').text(res.total_rockets_used || 0);
$('#stat-missions').text(res.total_missions_completed || 0);
$('#stat-pets').text(res.total_pets_tamed || 0);
}
} catch (err) {
console.error('Failed to load character stats', err);
}
}
async function loadCharacterActivity() {
try {
const res = await API.get(`/api/characters/${characterId}/activity`);
const data = (res && Array.isArray(res.data)) ? res.data : [];
const tbody = $('#activity-tbody');
tbody.empty();
if (data.length === 0) {
tbody.append('<tr><td colspan="3" class="text-center">No activity found</td></tr>');
} else {
data.forEach(activity => {
const row = $('<tr>');
row.append($('<td>').text(new Date(activity.timestamp * 1000).toLocaleString()));
row.append($('<td>').text(activity.activity));
row.append($('<td>').text(activity.map_id));
tbody.append(row);
});
}
} catch (err) {
console.error('Failed to load character activity', err);
$('#activity-tbody').html('<tr><td colspan="3" class="text-center text-danger">Failed to load activity</td></tr>');
}
}
// Load inventory when the tab is clicked
$('#inventory-tab').on('shown.bs.tab', async function() {
try {
const res = await API.get(`/api/characters/${characterId}/inventory`);
const data = (res && Array.isArray(res.data)) ? res.data : [];
const tbody = $('#inventory-tbody');
tbody.empty();
if (data.length === 0) {
tbody.append('<tr><td colspan="3" class="text-center">No items found</td></tr>');
} else {
data.forEach(item => {
const row = $('<tr>');
row.append($('<td>').text(item.item_id));
row.append($('<td>').text(item.count || 1));
row.append($('<td>').text(item.slot || '-'));
tbody.append(row);
});
}
} catch (err) {
console.error('Failed to load inventory', err);
$('#inventory-tbody').html('<tr><td colspan="3" class="text-center text-danger">Failed to load inventory</td></tr>');
}
});
// Initialize when libraries are ready
safeInit(function($) {
loadCharacter();
document.getElementById('rescue-char').addEventListener('click', async function() {
if (!confirm('Rescue this character to a safe location?')) return;
try {
const res = await API.post(`/api/characters/${characterId}/rescue`, {});
if (res && res.success) {
alert('Character rescued');
loadCharacter();
} else {
alert(res.error || 'Failed to rescue character');
}
} catch (err) { alert(err.message); }
});
document.getElementById('approve-name').addEventListener('click', async function() {
try {
const res = await API.post(`/api/characters/${characterId}/approve-name`, {});
if (res && res.success) {
alert('Name approved');
loadCharacter();
} else {
alert(res.error || 'Failed to approve name');
}
} catch (err) { alert(err.message); }
});
document.getElementById('delete-char').addEventListener('click', async function() {
const confirmMsg = 'DELETE this character? This action is irreversible!';
if (!confirm(confirmMsg)) return;
const doubleConfirm = prompt('Type "DELETE" to confirm:');
if (doubleConfirm !== 'DELETE') return;
try {
const res = await API.post(`/api/characters/${characterId}/delete`, {});
if (res && res.success) {
alert('Character deleted');
window.location.href = '/characters';
} else {
alert(res.error || 'Failed to delete');
}
} catch (err) { alert(err.message); }
});
document.getElementById('toggle-mail').addEventListener('click', async function() {
try {
const res = await API.post(`/api/characters/${characterId}/toggle-mail`, {});
if (res && res.success) { alert('Mail restriction toggled'); loadCharacter(); } else { alert(res.error || 'Failed'); }
} catch (err) { alert(err.message); }
});
document.getElementById('toggle-trade').addEventListener('click', async function() {
try {
const res = await API.post(`/api/characters/${characterId}/toggle-trade`, {});
if (res && res.success) { alert('Trade restriction toggled'); loadCharacter(); } else { alert(res.error || 'Failed'); }
} catch (err) { alert(err.message); }
});
document.getElementById('toggle-chat').addEventListener('click', async function() {
try {
const res = await API.post(`/api/characters/${characterId}/toggle-chat`, {});
if (res && res.success) { alert('Chat restriction toggled'); loadCharacter(); } else { alert(res.error || 'Failed'); }
} catch (err) { alert(err.message); }
});
}, { requireApi: true, timeout: 8000 });
</script>

View File

@@ -0,0 +1,293 @@
<div class="row">
<div class="col-12">
<h1 class="mb-4">Dashboard</h1>
</div>
</div>
{{#is_authenticated}}
<div class="row">
<!-- Account Info Card -->
<div class="col-md-6 col-lg-3 mb-4">
<div class="card stats-card">
<div class="card-body">
<h5 class="card-title">
<i class="bi bi-person-circle text-primary"></i>
Your Account
</h5>
<p class="card-text">
<strong>Username:</strong> {{username}}<br>
<strong>Account ID:</strong> {{account_id}}<br>
<strong>GM Level:</strong> {{gm_level}} ({{gm_level_name}})
</p>
</div>
</div>
</div>
{{#is_gm_3_plus}}
<!-- Server Stats Card -->
<div class="col-md-6 col-lg-3 mb-4">
<div class="card stats-card">
<div class="card-body">
<h5 class="card-title">
<i class="bi bi-server text-success"></i>
Server Status
</h5>
<div id="server-stats">
<p class="card-text">
<strong>Master:</strong> <span id="master-status" class="badge bg-secondary">Loading...</span><br>
<strong>Connected Clients:</strong> <span id="client-count">-</span><br>
<strong>Packets Sent:</strong> <span id="packets-sent">-</span><br>
<strong>Packets Received:</strong> <span id="packets-received">-</span>
</p>
</div>
</div>
</div>
</div>
<!-- Accounts Card -->
<div class="col-md-6 col-lg-3 mb-4">
<div class="card stats-card">
<div class="card-body">
<h5 class="card-title">
<i class="bi bi-people text-info"></i>
Accounts
</h5>
<p class="card-text">
<strong>Total Accounts:</strong> <span id="total-accounts">-</span><br>
<strong>Banned:</strong> <span id="banned-accounts">-</span><br>
<strong>Locked:</strong> <span id="locked-accounts">-</span>
</p>
<a href="/accounts" class="btn btn-sm btn-primary">Manage Accounts</a>
</div>
</div>
</div>
<!-- Characters Card -->
<div class="col-md-6 col-lg-3 mb-4">
<div class="card stats-card">
<div class="card-body">
<h5 class="card-title">
<i class="bi bi-person-badge text-warning"></i>
Characters
</h5>
<p class="card-text">
<strong>Total Characters:</strong> <span id="total-characters">-</span><br>
<strong>Pending Names:</strong> <span id="pending-names">-</span>
</p>
<a href="/characters" class="btn btn-sm btn-primary">Manage Characters</a>
</div>
</div>
</div>
{{/is_gm_3_plus}}
</div>
{{#is_gm_3_plus}}
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0">
<i class="bi bi-activity"></i>
Recent Activity
</h5>
</div>
<div class="card-body">
<table id="recent-activity-table" class="table table-striped table-hover">
<thead>
<tr>
<th>Time</th>
<th>Character</th>
<th>Activity</th>
<th>Map</th>
</tr>
</thead>
<tbody>
<!-- Populated via API -->
</tbody>
</table>
</div>
</div>
</div>
</div>
{{/is_gm_3_plus}}
<!-- Character Cards for All Authenticated Users -->
<div class="row mt-4">
<div class="col-12">
<h3 class="mb-3">
<i class="bi bi-person-badge"></i>
Your Characters
</h3>
<hr>
</div>
</div>
<div class="row" id="character-cards-container">
<!-- Character cards will be populated via JavaScript -->
<div class="col-12 text-center">
<p class="text-muted">Loading characters...</p>
</div>
</div>
{{/is_authenticated}}
{{^is_authenticated}}
<div class="row">
<div class="col-md-6 offset-md-3">
<div class="card">
<div class="card-body text-center">
<h3>Welcome to DarkflameServer Dashboard</h3>
<p class="lead">Please log in to access the dashboard.</p>
<a href="/login" class="btn btn-primary btn-lg">Login</a>
</div>
</div>
</div>
</div>
{{/is_authenticated}}
<script>
{{#is_gm_3_plus}}
// Load dashboard stats
async function loadDashboardStats() {
try {
// Server stats
const serverStats = await API.get('/api/stats/server');
if (serverStats) {
updateServerStats(serverStats);
}
// Account stats
const accountStats = await API.get('/api/stats/accounts');
if (accountStats) {
updateAccountStats(accountStats);
}
// Character stats
const characterStats = await API.get('/api/stats/characters');
if (characterStats) {
updateCharacterStats(characterStats);
}
// Recent activity
const activities = await API.get('/api/stats/recent-activity');
if (activities && activities.data) {
updateRecentActivity(activities.data);
}
} catch (error) {
console.error('Error loading dashboard stats:', error);
}
}
// Update server stats on UI
function updateServerStats(data) {
document.getElementById('master-status').textContent = data.master_connected ? 'Connected' : 'Disconnected';
document.getElementById('master-status').className = data.master_connected ? 'badge bg-success' : 'badge bg-danger';
document.getElementById('client-count').textContent = data.connected_clients || 0;
document.getElementById('packets-sent').textContent = data.packets_sent || 0;
document.getElementById('packets-received').textContent = data.packets_received || 0;
}
// Update account stats on UI
function updateAccountStats(data) {
document.getElementById('total-accounts').textContent = data.total || 0;
document.getElementById('banned-accounts').textContent = data.banned || 0;
document.getElementById('locked-accounts').textContent = data.locked || 0;
}
// Update character stats on UI
function updateCharacterStats(data) {
document.getElementById('total-characters').textContent = data.total || 0;
document.getElementById('pending-names').textContent = data.pending_names || 0;
}
// Update recent activity table
function updateRecentActivity(data) {
// Avoid reinitialising the DataTable on repeated calls (e.g. interval refreshs).
// If the table already exists, update its data and redraw. Otherwise initialize it.
if ($.fn.DataTable.isDataTable('#recent-activity-table')) {
const table = $('#recent-activity-table').DataTable();
table.clear();
table.rows.add(data);
table.draw(false);
} else {
const table = $('#recent-activity-table').DataTable({
data: data,
columns: [
{ data: 'timestamp' },
{ data: 'character_name' },
{ data: 'activity' },
{ data: 'map_id' }
],
pageLength: 10,
order: [[0, 'desc']]
});
}
}
// Initial load
document.addEventListener('DOMContentLoaded', loadDashboardStats);
// Auto-refresh stats every 30 seconds
setInterval(loadDashboardStats, 30000);
{{/is_gm_3_plus}}
{{#is_authenticated}}
// Load user's characters for character cards
async function loadUserCharacters() {
try {
const res = await API.get('/api/user/characters');
const characters = (res && Array.isArray(res.data)) ? res.data : (res || []);
const container = document.getElementById('character-cards-container');
container.innerHTML = '';
if (characters.length === 0) {
container.innerHTML = `
<div class="col-12 text-center">
<p class="text-muted">You don't have any characters yet. Log in to the game to create one!</p>
</div>
`;
return;
}
characters.forEach(char => {
const card = document.createElement('div');
card.className = 'col-md-6 col-lg-4 mb-4';
card.innerHTML = `
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">
<i class="bi bi-person-circle"></i>
${char.name}
</h5>
<p class="card-text">
<strong>Level:</strong> ${char.level || 0}<br>
<strong>Universe Score:</strong> ${char.uscore || 0}<br>
<strong>Current Zone:</strong> ${char.zone_id || 'Unknown'}<br>
<strong>Last Login:</strong> ${char.last_login && char.last_login > 0 ? new Date(char.last_login * 1000).toLocaleString() : 'Never'}
</p>
${char.pending_name ? `<span class="badge bg-warning mb-2">Pending Name: ${char.pending_name}</span><br>` : ''}
${char.needs_rename ? '<span class="badge bg-danger mb-2">Needs Rename</span><br>' : ''}
<a href="/characters/view/${char.id}" class="btn btn-sm btn-primary mt-2">View Details</a>
</div>
</div>
`;
container.appendChild(card);
});
} catch (error) {
console.error('Error loading user characters:', error);
const container = document.getElementById('character-cards-container');
container.innerHTML = `
<div class="col-12">
<div class="alert alert-warning">
Failed to load your characters. Please try refreshing the page.
</div>
</div>
`;
}
}
// Load character cards on page load
document.addEventListener('DOMContentLoaded', loadUserCharacters);
{{/is_authenticated}}
</script>

View File

@@ -0,0 +1,157 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{page_title}} - DarkflameServer Dashboard</title>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Bootstrap Icons -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
<!-- DataTables CSS -->
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
<!-- Custom CSS consolidated -->
<link rel="stylesheet" href="/static/css/dashboard.css">
{{#extra_head}}
{{{extra_head}}}
{{/extra_head}}
</head>
<body>
{{#show_navbar}}
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="/">
<i class="bi bi-grid-3x3-gap-fill"></i>
DarkflameServer Dashboard
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link{{#nav_home}} active{{/nav_home}}" href="/">
<i class="bi bi-house-door"></i> Home
</a>
</li>
{{#is_gm_3_plus}}
<li class="nav-item">
<a class="nav-link{{#nav_accounts}} active{{/nav_accounts}}" href="/accounts">
<i class="bi bi-people"></i> Accounts
</a>
</li>
<li class="nav-item">
<a class="nav-link{{#nav_characters}} active{{/nav_characters}}" href="/characters">
<i class="bi bi-person-badge"></i> Characters
</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
<i class="bi bi-shield-check"></i> Moderation
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="/moderation/pending">Pending Pets</a></li>
<li><a class="dropdown-item" href="/properties">Properties</a></li>
</ul>
</li>
<li class="nav-item">
<a class="nav-link{{#nav_mail}} active{{/nav_mail}}" href="/mail/send">
<i class="bi bi-envelope"></i> Mail
</a>
</li>
{{/is_gm_3_plus}}
{{#is_gm_5_plus}}
<li class="nav-item">
<a class="nav-link{{#nav_playkeys}} active{{/nav_playkeys}}" href="/playkeys">
<i class="bi bi-key"></i> Play Keys
</a>
</li>
{{/is_gm_5_plus}}
{{#is_gm_8_plus}}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
<i class="bi bi-journal-text"></i> Logs
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="/logs/activities">Activity Logs</a></li>
<li><a class="dropdown-item" href="/logs/commands">Command Logs</a></li>
<li><a class="dropdown-item" href="/logs/audits">Audit Logs</a></li>
</ul>
</li>
{{/is_gm_8_plus}}
<li class="nav-item">
<a class="nav-link{{#nav_bugs}} active{{/nav_bugs}}" href="/bugs">
<i class="bi bi-bug"></i> Bug Reports
</a>
</li>
</ul>
<ul class="navbar-nav">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
<i class="bi bi-person-circle"></i> {{username}}
{{#gm_level_name}}
<span class="badge bg-primary">{{gm_level_name}}</span>
{{/gm_level_name}}
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="/about">About</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#" onclick="logout(); return false;">
<i class="bi bi-box-arrow-right"></i> Logout
</a></li>
</ul>
</li>
</ul>
</div>
</div>
</nav>
{{/show_navbar}}
<main class="{{#show_navbar}}container-fluid mt-4{{/show_navbar}}">
{{#flash_messages}}
<div class="alert alert-{{type}} alert-dismissible fade show" role="alert">
{{message}}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{{/flash_messages}}
{{{content}}}
</main>
<footer class="footer mt-auto py-3 bg-dark border-top">
<div class="container text-center">
<span class="text-muted">DarkflameServer Dashboard &copy; 2025 | Powered by Crow C++</span>
</div>
</footer>
<!-- Bootstrap JS Bundle -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<!-- jQuery -->
<script src="https://code.jquery.com/jquery-3.7.0.min.js"></script>
<!-- DataTables JS -->
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
<!-- Shared helper: wait for jQuery/DataTables (keeps pages resilient to CDN timing) -->
<script src="/static/js/wait-for-jq-dt.js"></script>
<!-- Chart.js -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<!-- Custom JS -->
<script src="/static/js/api.js"></script>
<script src="/static/js/dashboard.js"></script>
<script src="/static/js/login.js"></script>
{{#extra_scripts}}
{{{extra_scripts}}}
{{/extra_scripts}}
</body>
</html>

View File

@@ -0,0 +1,31 @@
<div class="row justify-content-center">
<div class="col-md-6 col-lg-4">
<div class="card shadow-lg mt-5">
<div class="card-header bg-primary text-white text-center">
<h4 class="mb-0">
<i class="bi bi-shield-lock"></i>
DarkflameServer Dashboard
</h4>
</div>
<div class="card-body">
<form id="login-form">
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control" id="username" name="username" required autofocus>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">
<i class="bi bi-box-arrow-in-right"></i>
Login
</button>
</div>
</form>
<div id="login-message" class="mt-3" style="display: none;"></div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,73 @@
<div class="row">
<div class="col-12">
<h1 class="mb-4">
<i class="bi bi-activity"></i>
Activity Logs
</h1>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Player Activity</h5>
</div>
<div class="card-body">
<table id="activity-log-table" class="table table-striped table-hover">
<thead>
<tr>
<th>Time</th>
<th>Character</th>
<th>Activity</th>
<th>Map ID</th>
</tr>
</thead>
<tbody>
<!-- Populated via DataTables Ajax -->
</tbody>
</table>
</div>
</div>
</div>
</div>
<script>
// Initialize when libraries are ready
safeInit(function($) {
$('#activity-log-table').DataTable({
processing: true,
serverSide: true,
ajax: {
url: '/api/activity-log',
type: 'GET'
},
columns: [
{
data: 'timestamp',
render: function(data, type, row) {
if (type === 'display' || type === 'filter') {
const date = new Date(data * 1000);
return date.toLocaleString();
}
return data;
}
},
{
data: 'character_name',
render: function(data, type, row) {
return `<a href="/characters/view/${row.character_id}">${data}</a>`;
}
},
{
data: 'activity_name'
},
{
data: 'map_id'
}
],
order: [[0, 'desc']],
pageLength: 25
});
}, { requireApi: false, timeout: 8000 });
</script>

View File

@@ -0,0 +1,139 @@
<div class="row">
<div class="col-12">
<h1 class="mb-4">
<i class="bi bi-journal-check"></i>
Audit Logs
</h1>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Dashboard Audit Trail</h5>
</div>
<div class="card-body">
<table id="audits-table" class="table table-striped table-hover">
<thead>
<tr>
<th>Timestamp</th>
<th>Admin</th>
<th>Action</th>
<th>Target</th>
<th>Details</th>
</tr>
</thead>
<tbody>
<!-- Populated via DataTables Ajax -->
</tbody>
</table>
</div>
</div>
</div>
</div>
<script>
function showLibraryError(message) {
const el = document.getElementById('audits-table');
if (el) {
const wrapper = document.createElement('div');
wrapper.className = 'alert alert-danger';
wrapper.textContent = message;
el.replaceWith(wrapper);
} else {
alert(message);
}
}
function loadAuditLogs() {
API.get('/api/auth/status').then(status => {
if (!status || !status.authenticated || status.gm_level < 8) {
showLibraryError('You do not have permission to view audit logs. GM Level 8+ required.');
return;
}
API.get('/api/logs/audits').then(res => {
const data = Array.isArray(res.data) ? res.data : (res || []);
if ($.fn.DataTable.isDataTable('#audits-table')) {
const table = $('#audits-table').DataTable();
table.clear();
table.rows.add(data);
table.draw(false);
} else {
$('#audits-table').DataTable({
data: data,
columns: [
{
data: 'timestamp',
render: function(d) {
if (!d || d === 0) return '-';
return new Date(d * 1000).toLocaleString();
}
},
{
data: 'admin_username',
render: function(d, t, row) {
if (row.admin_account_id) {
return `<a href="/accounts/view/${row.admin_account_id}">${d || row.admin_account_id}</a>`;
}
return d || '-';
}
},
{
data: 'action',
render: function(d) {
// Color-code actions
const badges = {
'ban': 'danger',
'unban': 'success',
'lock': 'warning',
'unlock': 'success',
'mute': 'warning',
'unmute': 'success',
'delete': 'danger',
'create': 'success',
'update': 'info',
'gm_level_change': 'primary'
};
const action = d.toLowerCase();
const badgeClass = badges[action] || 'secondary';
return `<span class="badge bg-${badgeClass}">${d}</span>`;
}
},
{
data: 'target_type',
render: function(d, t, row) {
if (!d) return '-';
if (d === 'account' && row.target_id) {
return `<a href="/accounts/view/${row.target_id}">Account ${row.target_id}</a>`;
} else if (d === 'character' && row.target_id) {
return `<a href="/characters/view/${row.target_id}">Character ${row.target_id}</a>`;
}
return `${d} ${row.target_id || ''}`;
}
},
{ data: 'details', render: d => d || '-' }
],
pageLength: 25,
order: [[0, 'desc']],
processing: true
});
}
}).catch(err => {
const msg = err && err.message ? err.message : 'Failed to load audit logs';
showLibraryError(`Error loading audit logs: ${msg}`);
});
}).catch(err => {
showLibraryError(`Error checking authentication: ${err && err.message ? err.message : err}`);
});
}
// Initialize when jQuery/DataTables and API are ready
safeInit(function($) {
loadAuditLogs();
}, { requireApi: true, timeout: 8000 });
</script>

View File

@@ -0,0 +1,106 @@
<div class="row">
<div class="col-12">
<h1 class="mb-4">
<i class="bi bi-terminal"></i>
Command Logs
</h1>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Recent Commands</h5>
</div>
<div class="card-body">
<table id="commands-table" class="table table-striped table-hover">
<thead>
<tr>
<th>Timestamp</th>
<th>Character</th>
<th>Command</th>
<th>Arguments</th>
</tr>
</thead>
<tbody>
<!-- Populated via DataTables Ajax -->
</tbody>
</table>
</div>
</div>
</div>
</div>
<script>
function showLibraryError(message) {
const el = document.getElementById('commands-table');
if (el) {
const wrapper = document.createElement('div');
wrapper.className = 'alert alert-danger';
wrapper.textContent = message;
el.replaceWith(wrapper);
} else {
alert(message);
}
}
function loadCommandLogs() {
API.get('/api/auth/status').then(status => {
if (!status || !status.authenticated || status.gm_level < 8) {
showLibraryError('You do not have permission to view command logs. GM Level 8+ required.');
return;
}
API.get('/api/logs/commands').then(res => {
const data = Array.isArray(res.data) ? res.data : (res || []);
if ($.fn.DataTable.isDataTable('#commands-table')) {
const table = $('#commands-table').DataTable();
table.clear();
table.rows.add(data);
table.draw(false);
} else {
$('#commands-table').DataTable({
data: data,
columns: [
{
data: 'timestamp',
render: function(d) {
if (!d || d === 0) return '-';
return new Date(d * 1000).toLocaleString();
}
},
{
data: 'character_name',
render: function(d, t, row) {
if (row.character_id) {
return `<a href="/characters/view/${row.character_id}">${d || row.character_id}</a>`;
}
return d || '-';
}
},
{ data: 'command' },
{ data: 'arguments', render: d => d || '-' }
],
pageLength: 25,
order: [[0, 'desc']],
processing: true
});
}
}).catch(err => {
const msg = err && err.message ? err.message : 'Failed to load command logs';
showLibraryError(`Error loading command logs: ${msg}`);
});
}).catch(err => {
showLibraryError(`Error checking authentication: ${err && err.message ? err.message : err}`);
});
}
// Initialize when jQuery/DataTables and API are ready
safeInit(function($) {
loadCommandLogs();
}, { requireApi: true, timeout: 8000 });
</script>

View File

@@ -0,0 +1,80 @@
<div class="row">
<div class="col-12">
<h1 class="mb-4"><i class="bi bi-envelope"></i> Send Mail</h1>
</div>
</div>
<div class="row">
<div class="col-md-8">
<div class="card">
<div class="card-header">Compose Mail</div>
<div class="card-body">
<form id="send-mail-form">
<div class="mb-3 form-check">
<input class="form-check-input" type="checkbox" id="send-to-all">
<label class="form-check-label" for="send-to-all">Send to all characters</label>
</div>
<div class="mb-3">
<label class="form-label">Recipient Character ID (leave blank if sending to all)</label>
<input type="number" id="recipient-id" class="form-control">
</div>
<div class="mb-3">
<label class="form-label">Subject</label>
<input type="text" id="subject" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">Message</label>
<textarea id="body" class="form-control" rows="6" required></textarea>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Attachment LOT (optional)</label>
<input type="number" id="attachment-lot" class="form-control">
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Attachment Count</label>
<input type="number" id="attachment-count" class="form-control" value="1" min="1">
</div>
</div>
<button type="submit" class="btn btn-primary">Send Mail</button>
</form>
<div id="mail-result" class="mt-3"></div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('send-mail-form');
form.addEventListener('submit', async function(e) {
e.preventDefault();
const sendToAll = document.getElementById('send-to-all').checked;
const recipientId = parseInt(document.getElementById('recipient-id').value) || 0;
const subject = document.getElementById('subject').value.trim();
const body = document.getElementById('body').value.trim();
const lot = parseInt(document.getElementById('attachment-lot').value) || 0;
const count = parseInt(document.getElementById('attachment-count').value) || 1;
const payload = { subject: subject, body: body };
if (sendToAll) payload.send_to_all = true;
else payload.recipient_id = recipientId;
if (lot > 0) {
payload.attachment_lot = lot;
payload.attachment_count = count;
}
try {
const res = await API.post('/api/mail/send', payload);
if (res && res.success) {
document.getElementById('mail-result').innerHTML = `<div class="alert alert-success">Sent to ${res.recipients} recipient(s)</div>`;
form.reset();
} else {
document.getElementById('mail-result').innerHTML = `<div class="alert alert-danger">${res.error || 'Failed to send mail'}</div>`;
}
} catch (err) {
document.getElementById('mail-result').innerHTML = `<div class="alert alert-danger">${err.message}</div>`;
}
});
});
</script>

View File

@@ -0,0 +1,85 @@
<div class="row">
<div class="col-12">
<h1 class="mb-4"><i class="bi bi-paw"></i> Pet Name Moderation</h1>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">Pending Pet Names</div>
<div class="card-body">
<table id="pets-table" class="table table-striped table-hover">
<thead>
<tr>
<th>ID</th>
<th>Character</th>
<th>Pet Name</th>
<th>Submitted</th>
<th>Actions</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
<script>
let petsTable = null;
function loadPets() {
API.get('/api/moderation/pets').then(res => {
const data = Array.isArray(res.data) ? res.data : (res || []);
if ($.fn.DataTable.isDataTable('#pets-table')) {
const table = $('#pets-table').DataTable();
table.clear();
table.rows.add(data);
table.draw(false);
petsTable = table;
} else {
petsTable = $('#pets-table').DataTable({
data: data,
columns: [
{ data: 'id' },
{ data: 'character_name' },
{ data: 'pet_name' },
{ data: 'submitted', render: d => d ? new Date(d * 1000).toLocaleString() : '-' },
{ data: null, orderable: false, render: function(data, type, row) {
return `
<button class="btn btn-sm btn-success" onclick="approvePet(${row.id})">Approve</button>
<button class="btn btn-sm btn-danger" onclick="rejectPet(${row.id})">Reject</button>
`;
} }
],
order: [[0, 'desc']],
pageLength: 25
});
}
}).catch(err => { alert(err && err.message ? err.message : 'Failed to load pets'); });
}
// Initialize when libraries are ready
safeInit(function($) {
loadPets();
}, { requireApi: true, timeout: 8000 });
window.approvePet = async function(id) {
if (!confirm('Approve this pet name?')) return;
try {
const res = await API.post(`/api/moderation/pets/${id}/approve`);
if (res && res.success) { loadPets(); alert('Approved'); } else { alert(res.error || 'Failed'); }
} catch (err) { alert(err.message); }
};
window.rejectPet = async function(id) {
if (!confirm('Reject this pet name?')) return;
try {
const res = await API.post(`/api/moderation/pets/${id}/reject`);
if (res && res.success) { loadPets(); alert('Rejected'); } else { alert(res.error || 'Failed'); }
} catch (err) { alert(err.message); }
};
</script>

View File

@@ -0,0 +1,82 @@
<div class="row">
<div class="col-12">
<h1 class="mb-4"><i class="bi bi-house"></i> Property Moderation</h1>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">Pending Properties</div>
<div class="card-body">
<table id="properties-table" class="table table-striped table-hover">
<thead>
<tr>
<th>ID</th>
<th>Owner (Character)</th>
<th>Property Name</th>
<th>Submitted</th>
<th>Actions</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
<script>
let propertiesTable = null;
function loadProperties() {
API.get('/api/moderation/properties').then(res => {
const data = Array.isArray(res.data) ? res.data : (res || []);
if ($.fn.DataTable.isDataTable('#properties-table')) {
const table = $('#properties-table').DataTable();
table.clear();
table.rows.add(data);
table.draw(false);
propertiesTable = table;
} else {
propertiesTable = $('#properties-table').DataTable({
data: data,
columns: [
{ data: 'id' },
{ data: 'character_name' },
{ data: 'property_name' },
{ data: 'submitted', render: d => d ? new Date(d * 1000).toLocaleString() : '-' },
{ data: null, orderable: false, render: function(data, type, row) {
return `
<button class="btn btn-sm btn-success" onclick="approveProperty(${row.id})">Approve</button>
<button class="btn btn-sm btn-danger" onclick="rejectProperty(${row.id})">Reject</button>
`;
} }
],
order: [[0, 'desc']],
pageLength: 25
});
}
}).catch(err => { alert(err && err.message ? err.message : 'Failed to load properties'); });
}
// Initialize when libraries are ready
safeInit(function($) { loadProperties(); }, { requireApi: true, timeout: 8000 });
window.approveProperty = async function(id) {
if (!confirm('Approve this property?')) return;
try {
const res = await API.post(`/api/moderation/properties/${id}/approve`);
if (res && res.success) { loadProperties(); alert('Approved'); } else { alert(res.error || 'Failed'); }
} catch (err) { alert(err.message); }
};
window.rejectProperty = async function(id) {
if (!confirm('Reject this property?')) return;
try {
const res = await API.post(`/api/moderation/properties/${id}/reject`);
if (res && res.success) { loadProperties(); alert('Rejected'); } else { alert(res.error || 'Failed'); }
} catch (err) { alert(err.message); }
};
</script>

View File

@@ -0,0 +1,155 @@
<div class="row">
<div class="col-12">
<h1 class="mb-4"><i class="bi bi-key"></i> Play Keys</h1>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<div class="card">
<div class="card-header">Create Play Keys</div>
<div class="card-body">
<form id="create-keys-form" class="row g-2">
<div class="col-auto">
<label class="form-label">Count</label>
<input type="number" id="key-count" class="form-control" value="1" min="1" max="100">
</div>
<div class="col-auto">
<label class="form-label">Uses</label>
<input type="number" id="key-uses" class="form-control" value="1" min="1">
</div>
<div class="col-6">
<label class="form-label">Notes</label>
<input type="text" id="key-notes" class="form-control" placeholder="Optional notes">
</div>
<div class="col-12">
<button type="submit" class="btn btn-primary">Create</button>
</div>
</form>
<div id="created-keys" class="mt-3"></div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">Existing Play Keys</div>
<div class="card-body">
<table id="playkeys-table" class="table table-striped table-hover">
<thead>
<tr>
<th>ID</th>
<th>Key</th>
<th>Uses</th>
<th>Times Used</th>
<th>Active</th>
<th>Notes</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
<script>
let playkeysTable = null;
function loadPlaykeys() {
API.get('/api/playkeys').then(res => {
const data = Array.isArray(res.data) ? res.data : (res || []);
if ($.fn.DataTable.isDataTable('#playkeys-table')) {
const table = $('#playkeys-table').DataTable();
table.clear();
table.rows.add(data);
table.draw(false);
playkeysTable = table;
} else {
playkeysTable = $('#playkeys-table').DataTable({
data: data,
columns: [
{ data: 'id' },
{ data: 'key_string' },
{ data: 'key_uses' },
{ data: 'times_used' },
{ data: 'active', render: d => d ? '<span class="badge bg-success">Yes</span>' : '<span class="badge bg-secondary">No</span>' },
{ data: 'notes' },
{ data: 'created_at', render: d => d ? new Date(d * 1000).toLocaleString() : '-' },
{ data: null, orderable: false, render: function(data, type, row) {
return `
<button class="btn btn-sm btn-danger" onclick="deleteKey(${row.id})">Delete</button>
<button class="btn btn-sm btn-info" onclick="viewKey(${row.id})">View</button>
`;
} }
],
order: [[0, 'desc']],
pageLength: 25
});
// Create keys form handler
$('#create-keys-form').on('submit', async function(e) {
e.preventDefault();
const count = parseInt($('#key-count').val()) || 1;
const uses = parseInt($('#key-uses').val()) || 1;
const notes = $('#key-notes').val() || '';
try {
const res = await API.post('/api/playkeys/create', { count: count, uses: uses, notes: notes });
if (res && res.success) {
$('#created-keys').html(`<div class="alert alert-success">Created ${res.count} key(s): <pre>${JSON.stringify(res.keys)}</pre></div>`);
loadPlaykeys();
} else {
$('#created-keys').html(`<div class="alert alert-danger">${res.error || 'Failed to create keys'}</div>`);
}
} catch (err) {
$('#created-keys').html(`<div class="alert alert-danger">${err.message}</div>`);
}
});
}
}).catch(err => {
const msg = err && err.message ? err.message : 'Failed to load play keys';
document.getElementById('created-keys').innerHTML = `<div class="alert alert-danger">${msg}</div>`;
});
}
// Use safeInit to ensure jQuery/DataTables and API are present
safeInit(function($) {
loadPlaykeys();
}, { requireApi: true, timeout: 8000 });
async function deleteKey(id) {
if (!confirm('Delete this play key?')) return;
try {
const res = await API.delete(`/api/playkeys/${id}`);
if (res && res.success) {
loadPlaykeys();
alert('Play key deleted');
} else {
alert(res.error || 'Failed to delete key');
}
} catch (err) {
alert(err.message);
}
}
async function viewKey(id) {
try {
const res = await API.get(`/api/playkeys/${id}`);
if (res && res.success) {
const info = `ID: ${res.id}\nKey: ${res.key_string}\nUses: ${res.key_uses}\nTimes used: ${res.times_used}\nActive: ${res.active}\nNotes: ${res.notes}`;
alert(info);
} else {
alert(res.error || 'Failed to get key');
}
} catch (err) {
alert(err.message);
}
}
</script>

View File

@@ -0,0 +1,31 @@
<div class="container py-4">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">Register</div>
<div class="card-body">
<form id="register-form">
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control" id="username" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" required>
</div>
<div class="mb-3">
<label for="play_key" class="form-label">Play Key</label>
<input type="text" class="form-control" id="play_key" placeholder="XXXX-XXXX-XXXX-XXXX" required>
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary">Create Account</button>
</div>
</form>
<div id="register-alert" class="mt-3" style="display:none;"></div>
</div>
</div>
</div>
</div>
</div>
<script src="/static/js/register.js"></script>

View File

@@ -15,7 +15,7 @@ target_include_directories(dDatabaseCDClient PUBLIC "."
"${PROJECT_SOURCE_DIR}/dCommon"
"${PROJECT_SOURCE_DIR}/dCommon/dEnums"
)
target_link_libraries(dDatabaseCDClient PRIVATE sqlite3)
target_link_libraries(dDatabaseCDClient PRIVATE sqlite3 glm::glm)
if (${CDCLIENT_CACHE_ALL})
add_compile_definitions(dDatabaseCDClient PRIVATE CDCLIENT_CACHE_ALL=${CDCLIENT_CACHE_ALL})

View File

@@ -10,4 +10,5 @@ add_dependencies(dDatabase conncpp_dylib)
target_include_directories(dDatabase PUBLIC ".")
target_link_libraries(dDatabase
PUBLIC dDatabaseCDClient dDatabaseGame)
PUBLIC dDatabaseCDClient dDatabaseGame
PRIVATE glm::glm)

View File

@@ -29,7 +29,7 @@ target_include_directories(dDatabaseGame PUBLIC "."
target_link_libraries(dDatabaseGame
INTERFACE dCommon
PRIVATE sqlite3 MariaDB::ConnCpp)
PRIVATE sqlite3 MariaDB::ConnCpp glm::glm)
# Glob together all headers that need to be precompiled
file(

View File

@@ -25,6 +25,8 @@
#include "IAccountsRewardCodes.h"
#include "IBehaviors.h"
#include "IUgcModularBuild.h"
#include "IDashboardAuditLog.h"
#include "IDashboardConfig.h"
#ifdef _DEBUG
# define DLU_SQL_TRY_CATCH_RETHROW(x) do { try { x; } catch (std::exception& ex) { LOG("SQL Error: %s", ex.what()); throw; } } while(0)
@@ -38,7 +40,8 @@ class GameDatabase :
public IPropertyContents, public IProperty, public IPetNames, public ICharXml,
public IMigrationHistory, public IUgc, public IFriends, public ICharInfo,
public IAccounts, public IActivityLog, public IAccountsRewardCodes, public IIgnoreList,
public IBehaviors, public IUgcModularBuild {
public IBehaviors, public IUgcModularBuild,
public IDashboardAuditLog, public IDashboardConfig {
public:
virtual ~GameDatabase() = default;
// TODO: These should be made private.
@@ -48,7 +51,7 @@ public:
virtual void Commit() = 0;
virtual bool GetAutoCommit() = 0;
virtual void SetAutoCommit(bool value) = 0;
virtual void DeleteCharacter(const uint32_t characterId) = 0;
virtual void DeleteCharacter(const LWOOBJID characterId) = 0;
};
#endif //!__GAMEDATABASE__H__

View File

@@ -5,6 +5,7 @@
#include <optional>
#include <string>
#include <string_view>
#include <vector>
enum class eGameMasterLevel : uint8_t;
@@ -14,6 +15,7 @@ public:
std::string bcryptPassword;
uint32_t id{};
uint32_t playKeyId{};
uint64_t muteExpire{};
bool banned{};
bool locked{};
eGameMasterLevel maxGmLevel{};
@@ -37,7 +39,62 @@ public:
// Update the GameMaster level of an account.
virtual void UpdateAccountGmLevel(const uint32_t accountId, const eGameMasterLevel gmLevel) = 0;
// Set the play_key_id for an account (used during registration)
virtual void UpdateAccountPlayKey(const uint32_t accountId, const uint32_t playKeyId) = 0;
// Get counts for dashboard/stats
virtual uint32_t GetBannedAccountCount() = 0;
virtual uint32_t GetLockedAccountCount() = 0;
virtual uint32_t GetAccountCount() = 0;
struct ListInfo {
uint32_t id{};
std::string name;
eGameMasterLevel gm_level{};
bool banned{};
bool locked{};
uint64_t mute_expire{};
uint32_t play_key_id{};
};
struct DetailedInfo {
uint32_t id{};
std::string name;
std::string email;
eGameMasterLevel gm_level{};
bool banned{};
bool locked{};
uint64_t mute_expire{};
uint32_t play_key_id{};
uint64_t created_at{};
};
struct SessionInfo {
uint64_t sessionId{};
std::string ipAddress;
uint64_t loginTime{};
uint64_t logoutTime{};
bool active{};
};
// Return all accounts for dashboard listing
virtual std::vector<ListInfo> GetAllAccounts() = 0;
// Update an account's locked status
virtual void UpdateAccountLock(const uint32_t accountId, const bool locked) = 0;
// Get detailed account info by ID (for dashboard viewing)
virtual std::optional<DetailedInfo> GetAccountById(const uint32_t accountId) = 0;
// Update account email (for dashboard)
virtual void UpdateAccountEmail(const uint32_t accountId, const std::string_view email) = 0;
// Delete account and all associated data
virtual void DeleteAccount(const uint32_t accountId) = 0;
// Get account session history
virtual std::vector<SessionInfo> GetAccountSessions(const uint32_t accountId, uint32_t limit = 50) = 0;
};
#endif //!__IACCOUNTS__H__

View File

@@ -14,7 +14,28 @@ enum class eActivityType : uint32_t {
class IActivityLog {
public:
// Update the activity log for the given account.
virtual void UpdateActivityLog(const uint32_t characterId, const eActivityType activityType, const LWOMAPID mapId) = 0;
virtual void UpdateActivityLog(const LWOOBJID characterId, const eActivityType activityType, const LWOMAPID mapId) = 0;
struct Entry {
LWOOBJID characterId{};
eActivityType activity{};
uint32_t timestamp{};
LWOMAPID mapId{};
};
// Retrieve recent activity entries ordered by time desc.
virtual std::vector<Entry> GetRecentActivity(const uint32_t limit) = 0;
// Get total count of activity log entries
virtual uint32_t GetActivityLogCount() = 0;
// Get paginated activity log entries with ordering
virtual std::vector<Entry> GetActivityLogPaginated(
uint32_t offset,
uint32_t limit,
const std::string& orderColumn = "time",
const std::string& orderDir = "DESC"
) = 0;
};
#endif //!__IACTIVITYLOG__H__

View File

@@ -9,7 +9,7 @@ class IBehaviors {
public:
struct Info {
LWOOBJID behaviorId{};
uint32_t characterId{};
LWOOBJID characterId{};
std::string behaviorInfo;
};

View File

@@ -2,7 +2,10 @@
#define __IBUGREPORTS__H__
#include <cstdint>
#include <string>
#include <string_view>
#include <vector>
#include <optional>
class IBugReports {
public:
@@ -11,10 +14,32 @@ public:
std::string clientVersion;
std::string otherPlayer;
std::string selection;
uint32_t characterId{};
LWOOBJID characterId{};
};
struct DetailedInfo {
uint64_t id{};
std::string body;
std::string clientVersion;
std::string otherPlayer;
std::string selection;
LWOOBJID characterId{};
uint64_t submitted{};
uint64_t resolved_time{};
uint32_t resolved_by_id{};
std::string resolution;
};
// Add a new bug report to the database.
virtual void InsertNewBugReport(const Info& info) = 0;
// Dashboard methods
virtual std::vector<DetailedInfo> GetAllBugReports() = 0;
virtual std::vector<DetailedInfo> GetUnresolvedBugReports() = 0;
virtual std::vector<DetailedInfo> GetResolvedBugReports() = 0;
virtual std::optional<DetailedInfo> GetBugReportById(const uint64_t reportId) = 0;
virtual void ResolveBugReport(const uint64_t reportId, const uint32_t resolvedById, const std::string_view resolution) = 0;
virtual uint32_t GetBugReportCount() = 0;
virtual uint32_t GetUnresolvedBugReportCount() = 0;
};
#endif //!__IBUGREPORTS__H__

View File

@@ -9,43 +9,110 @@
#include "ePermissionMap.h"
// Forward declare eActivityType for Activity struct
enum class eActivityType : uint32_t;
class ICharInfo {
public:
struct Info {
std::string name;
std::string pendingName;
uint32_t id{};
LWOOBJID id{};
uint32_t accountId{};
bool needsRename{};
LWOCLONEID cloneId{};
ePermissionMap permissionMap{};
// Extended fields for dashboard
uint32_t level{};
uint64_t uscore{};
uint32_t zoneId{};
uint64_t lastLogin{};
uint64_t createdOn{};
};
struct Stats {
uint64_t totalCurrencyCollected{};
uint64_t totalBricksCollected{};
uint64_t totalSmashables{};
uint64_t totalQuickbuildsCompleted{};
uint64_t totalEnemiesSmashed{};
uint64_t totalRocketsUsed{};
uint64_t totalMissionsCompleted{};
uint64_t totalPetsTamed{};
};
struct InventoryItem {
LWOOBJID itemId{};
uint32_t count{};
int32_t slot{};
};
struct Activity {
uint64_t timestamp{};
eActivityType activity{};
uint32_t mapId{};
};
// Get the approved names of all characters.
virtual std::vector<std::string> GetApprovedCharacterNames() = 0;
// Get the character info for the given character id.
virtual std::optional<ICharInfo::Info> GetCharacterInfo(const uint32_t charId) = 0;
virtual std::optional<ICharInfo::Info> GetCharacterInfo(const LWOOBJID charId) = 0;
// Get the character info for the given character name.
virtual std::optional<ICharInfo::Info> GetCharacterInfo(const std::string_view name) = 0;
// Get the character ids for the given account.
virtual std::vector<uint32_t> GetAccountCharacterIds(const uint32_t accountId) = 0;
virtual std::vector<LWOOBJID> GetAccountCharacterIds(const LWOOBJID accountId) = 0;
// Insert a new character into the database.
virtual void InsertNewCharacter(const ICharInfo::Info info) = 0;
// Set the name of the given character.
virtual void SetCharacterName(const uint32_t characterId, const std::string_view name) = 0;
virtual void SetCharacterName(const LWOOBJID characterId, const std::string_view name) = 0;
// Set the pending name of the given character.
virtual void SetPendingCharacterName(const uint32_t characterId, const std::string_view name) = 0;
virtual void SetPendingCharacterName(const LWOOBJID characterId, const std::string_view name) = 0;
// Updates the given character ids last login to be right now.
virtual void UpdateLastLoggedInCharacter(const uint32_t characterId) = 0;
virtual void UpdateLastLoggedInCharacter(const LWOOBJID characterId) = 0;
virtual bool IsNameInUse(const std::string_view name) = 0;
// Get total count of characters
virtual uint32_t GetCharacterCount() = 0;
// Get paginated list of all characters
virtual std::vector<Info> GetAllCharactersPaginated(
uint32_t offset,
uint32_t limit,
const std::string& orderColumn = "id",
const std::string& orderDir = "DESC"
) = 0;
// Get characters with pending names (for moderation)
virtual std::vector<Info> GetCharactersWithPendingNames() = 0;
// Update character permission map (for restrictions)
virtual void UpdateCharacterPermissions(const LWOOBJID characterId, ePermissionMap permissions) = 0;
// Set needs rename flag
virtual void SetCharacterNeedsRename(const LWOOBJID characterId, bool needsRename) = 0;
// Get character statistics
virtual std::optional<Stats> GetCharacterStats(const LWOOBJID characterId) = 0;
// Get character inventory
virtual std::vector<InventoryItem> GetCharacterInventory(const LWOOBJID characterId) = 0;
// Get character activity history
virtual std::vector<Activity> GetCharacterActivity(const LWOOBJID characterId, uint32_t limit = 50) = 0;
// Rescue character to a safe zone
virtual void RescueCharacter(const LWOOBJID characterId, uint32_t zoneId) = 0;
// Delete character and all associated data
virtual void DeleteCharacter(const LWOOBJID characterId) = 0;
};
#endif //!__ICHARINFO__H__

View File

@@ -8,13 +8,13 @@
class ICharXml {
public:
// Get the character xml for the given character id.
virtual std::string GetCharacterXml(const uint32_t charId) = 0;
virtual std::string GetCharacterXml(const LWOOBJID charId) = 0;
// Update the character xml for the given character id.
virtual void UpdateCharacterXml(const uint32_t charId, const std::string_view lxfml) = 0;
virtual void UpdateCharacterXml(const LWOOBJID charId, const std::string_view lxfml) = 0;
// Insert the character xml for the given character id.
virtual void InsertCharacterXml(const uint32_t characterId, const std::string_view lxfml) = 0;
virtual void InsertCharacterXml(const LWOOBJID characterId, const std::string_view lxfml) = 0;
};
#endif //!__ICHARXML__H__

View File

@@ -2,13 +2,24 @@
#define __ICOMMANDLOG__H__
#include <cstdint>
#include <string>
#include <string_view>
#include <vector>
class ICommandLog {
public:
public:
struct Entry {
uint64_t timestamp{};
LWOOBJID characterId{};
std::string command;
std::string arguments;
};
// Insert a new slash command log entry.
virtual void InsertSlashCommandUsage(const uint32_t characterId, const std::string_view command) = 0;
virtual void InsertSlashCommandUsage(const LWOOBJID characterId, const std::string_view command) = 0;
// Get recent command log entries
virtual std::vector<Entry> GetCommandLogs(uint32_t limit = 100) = 0;
};
#endif //!__ICOMMANDLOG__H__

View File

@@ -0,0 +1,58 @@
#ifndef __IDASHBOARDAUDITLOG__H__
#define __IDASHBOARDAUDITLOG__H__
#include <cstdint>
#include <string>
#include <string_view>
#include <vector>
/**
* Interface for Dashboard audit log table.
* Records all API requests, security events, and administrative actions.
*/
class IDashboardAuditLog {
public:
struct AuditLogEntry {
uint64_t id;
uint64_t timestamp;
std::string ip_address;
std::string endpoint;
std::string method;
std::string user_agent;
int32_t response_code;
};
struct AdminActionLog {
uint64_t timestamp;
uint32_t adminAccountId;
std::string action;
std::string targetType;
uint64_t targetId;
std::string details;
};
// Insert a new audit log entry for API requests
virtual void 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) = 0;
// Insert a new admin action log entry
virtual void InsertAdminActionLog(uint32_t adminAccountId, const std::string_view action,
const std::string_view targetType, uint64_t targetId,
const std::string_view details) = 0;
// Get recent audit log entries (limit = number of entries)
virtual std::vector<AuditLogEntry> GetRecentAuditLogs(uint32_t limit = 100) = 0;
// Get recent admin action logs
virtual std::vector<AdminActionLog> GetAuditLogs(uint32_t limit = 100) = 0;
// Get audit logs for a specific IP address
virtual std::vector<AuditLogEntry> GetAuditLogsByIP(const std::string_view ip, uint32_t limit = 100) = 0;
// Clear old audit logs (older than days_to_keep)
virtual void CleanupOldAuditLogs(uint32_t days_to_keep = 30) = 0;
};
#endif //!__IDASHBOARDAUDITLOG__H__

View File

@@ -0,0 +1,27 @@
#ifndef __IDASHBOARDCONFIG__H__
#define __IDASHBOARDCONFIG__H__
#include <optional>
#include <string>
#include <string_view>
/**
* Interface for Dashboard configuration table.
* Stores key-value configuration settings for the Dashboard server.
*/
class IDashboardConfig {
public:
struct DashboardConfig {
std::string key;
std::string value;
};
// Get a configuration value
virtual std::optional<std::string> GetDashboardConfig(const std::string_view key) = 0;
// Set a configuration value
virtual void SetDashboardConfig(const std::string_view key, const std::string_view value) = 0;
};
#endif //!__IDASHBOARDCONFIG__H__

View File

@@ -8,25 +8,25 @@
class IFriends {
public:
struct BestFriendStatus {
uint32_t playerCharacterId{};
uint32_t friendCharacterId{};
LWOOBJID playerCharacterId{};
LWOOBJID friendCharacterId{};
uint32_t bestFriendStatus{};
};
// Get the friends list for the given character id.
virtual std::vector<FriendData> GetFriendsList(const uint32_t charId) = 0;
virtual std::vector<FriendData> GetFriendsList(const LWOOBJID charId) = 0;
// Get the best friend status for the given player and friend character ids.
virtual std::optional<IFriends::BestFriendStatus> GetBestFriendStatus(const uint32_t playerCharacterId, const uint32_t friendCharacterId) = 0;
virtual std::optional<IFriends::BestFriendStatus> GetBestFriendStatus(const LWOOBJID playerCharacterId, const LWOOBJID friendCharacterId) = 0;
// Set the best friend status for the given player and friend character ids.
virtual void SetBestFriendStatus(const uint32_t playerCharacterId, const uint32_t friendCharacterId, const uint32_t bestFriendStatus) = 0;
virtual void SetBestFriendStatus(const LWOOBJID playerCharacterId, const LWOOBJID friendCharacterId, const uint32_t bestFriendStatus) = 0;
// Add a friend to the given character id.
virtual void AddFriend(const uint32_t playerCharacterId, const uint32_t friendCharacterId) = 0;
virtual void AddFriend(const LWOOBJID playerCharacterId, const LWOOBJID friendCharacterId) = 0;
// Remove a friend from the given character id.
virtual void RemoveFriend(const uint32_t playerCharacterId, const uint32_t friendCharacterId) = 0;
virtual void RemoveFriend(const LWOOBJID playerCharacterId, const LWOOBJID friendCharacterId) = 0;
};
#endif //!__IFRIENDS__H__

View File

@@ -9,12 +9,12 @@ class IIgnoreList {
public:
struct Info {
std::string name;
uint32_t id;
LWOOBJID id;
};
virtual std::vector<Info> GetIgnoreList(const uint32_t playerId) = 0;
virtual void AddIgnore(const uint32_t playerId, const uint32_t ignoredPlayerId) = 0;
virtual void RemoveIgnore(const uint32_t playerId, const uint32_t ignoredPlayerId) = 0;
virtual std::vector<Info> GetIgnoreList(const LWOOBJID playerId) = 0;
virtual void AddIgnore(const LWOOBJID playerId, const LWOOBJID ignoredPlayerId) = 0;
virtual void RemoveIgnore(const LWOOBJID playerId, const LWOOBJID ignoredPlayerId) = 0;
};
#endif //!__IIGNORELIST__H__

View File

@@ -5,12 +5,13 @@
#include <optional>
#include <string>
#include <vector>
#include "dCommonVars.h"
class ILeaderboard {
public:
struct Entry {
uint32_t charId{};
LWOOBJID charId{};
uint32_t lastPlayedTimestamp{};
float primaryScore{};
float secondaryScore{};
@@ -36,12 +37,12 @@ public:
virtual std::vector<ILeaderboard::Entry> GetAscendingLeaderboard(const uint32_t activityId) = 0;
virtual std::vector<ILeaderboard::Entry> GetNsLeaderboard(const uint32_t activityId) = 0;
virtual std::vector<ILeaderboard::Entry> GetAgsLeaderboard(const uint32_t activityId) = 0;
virtual std::optional<Score> GetPlayerScore(const uint32_t playerId, const uint32_t gameId) = 0;
virtual std::optional<Score> GetPlayerScore(const LWOOBJID playerId, const uint32_t gameId) = 0;
virtual void SaveScore(const uint32_t playerId, const uint32_t gameId, const Score& score) = 0;
virtual void UpdateScore(const uint32_t playerId, const uint32_t gameId, const Score& score) = 0;
virtual void IncrementNumWins(const uint32_t playerId, const uint32_t gameId) = 0;
virtual void IncrementTimesPlayed(const uint32_t playerId, const uint32_t gameId) = 0;
virtual void SaveScore(const LWOOBJID playerId, const uint32_t gameId, const Score& score) = 0;
virtual void UpdateScore(const LWOOBJID playerId, const uint32_t gameId, const Score& score) = 0;
virtual void IncrementNumWins(const LWOOBJID playerId, const uint32_t gameId) = 0;
virtual void IncrementTimesPlayed(const LWOOBJID playerId, const uint32_t gameId) = 0;
};
#endif //!__ILEADERBOARD__H__

View File

@@ -16,13 +16,13 @@ public:
virtual void InsertNewMail(const MailInfo& mail) = 0;
// Get the mail for the given character id.
virtual std::vector<MailInfo> GetMailForPlayer(const uint32_t characterId, const uint32_t numberOfMail) = 0;
virtual std::vector<MailInfo> GetMailForPlayer(const LWOOBJID characterId, const uint32_t numberOfMail) = 0;
// Get the mail for the given mail id.
virtual std::optional<MailInfo> GetMail(const uint64_t mailId) = 0;
// Get the number of unread mail for the given character id.
virtual uint32_t GetUnreadMailCount(const uint32_t characterId) = 0;
virtual uint32_t GetUnreadMailCount(const LWOOBJID characterId) = 0;
// Mark the given mail as read.
virtual void MarkMailRead(const uint64_t mailId) = 0;

View File

@@ -6,14 +6,19 @@
class IObjectIdTracker {
public:
// Only the first 48 bits of the ids are the id, the last 16 bits are reserved for flags.
struct Range {
uint64_t minID{}; // Only the first 48 bits are the id, the last 16 bits are reserved for flags.
uint64_t maxID{}; // Only the first 48 bits are the id, the last 16 bits are reserved for flags.
};
// Get the current persistent id.
virtual std::optional<uint32_t> GetCurrentPersistentId() = 0;
virtual std::optional<uint64_t> GetCurrentPersistentId() = 0;
// Insert the default persistent id.
virtual void InsertDefaultPersistentId() = 0;
// Update the persistent id.
virtual void UpdatePersistentId(const uint32_t newId) = 0;
virtual Range GetPersistentIdRange() = 0;
};
#endif //!__IOBJECTIDTRACKER__H__

View File

@@ -3,6 +3,8 @@
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
class IPetNames {
public:
@@ -11,11 +13,24 @@ public:
int32_t approvalStatus{};
};
struct DetailedInfo {
LWOOBJID id{};
std::string petName;
int32_t approvalStatus{};
LWOOBJID ownerId{};
};
// Set the pet name moderation status for the given pet id.
virtual void SetPetNameModerationStatus(const LWOOBJID& petId, const IPetNames::Info& info) = 0;
// Get pet info for the given pet id.
virtual std::optional<IPetNames::Info> GetPetNameInfo(const LWOOBJID& petId) = 0;
// Dashboard methods
virtual std::vector<DetailedInfo> GetAllPetNames() = 0;
virtual std::vector<DetailedInfo> GetPetNamesByStatus(int32_t status) = 0;
virtual void SetPetApprovalStatus(const LWOOBJID& petId, int32_t status) = 0;
virtual uint32_t GetPendingPetNamesCount() = 0;
};
#endif //!__IPETNAMES__H__

View File

@@ -3,13 +3,38 @@
#include <cstdint>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
class IPlayKeys {
public:
struct Info {
uint32_t id{};
std::string key_string;
uint32_t key_uses{};
uint32_t times_used{};
bool active{};
std::string notes;
uint64_t created_at{};
};
// Get the playkey id for the given playkey.
// Optional of bool may seem pointless, however the optional indicates if the playkey exists
// and the bool indicates if the playkey is active.
virtual std::optional<bool> IsPlaykeyActive(const int32_t playkeyId) = 0;
// Dashboard methods
virtual std::vector<Info> GetAllPlayKeys() = 0;
virtual std::optional<Info> GetPlayKeyById(const uint32_t playkeyId) = 0;
// Find a playkey by its string value (e.g. "ABCD-EFGH-...."). Returns Info if found.
virtual std::optional<Info> GetPlayKeyByString(const std::string_view key_string) = 0;
// Consume one usage of the given playkey id. Returns true if consumed, false if no uses left or not active.
virtual bool ConsumePlayKeyUsage(const uint32_t playkeyId) = 0;
virtual void CreatePlayKey(const std::string_view key_string, uint32_t uses, const std::string_view notes) = 0;
virtual void UpdatePlayKey(const uint32_t playkeyId, uint32_t uses, bool active, const std::string_view notes) = 0;
virtual void DeletePlayKey(const uint32_t playkeyId) = 0;
virtual uint32_t GetPlayKeyCount() = 0;
};
#endif //!__IPLAYKEYS__H__

View File

@@ -13,7 +13,7 @@ public:
std::string description;
std::string rejectionReason;
LWOOBJID id{};
uint32_t ownerId{};
LWOOBJID ownerId{};
LWOCLONEID cloneId{};
int32_t privacyOption{};
uint32_t modApproved{};
@@ -27,7 +27,7 @@ public:
uint32_t mapId{};
std::string searchString;
ePropertySortType sortChoice{};
uint32_t playerId{};
LWOOBJID playerId{};
uint32_t numResults{};
uint32_t startIndex{};
uint32_t playerSort{};
@@ -39,6 +39,9 @@ public:
std::vector<IProperty::Info> entries;
};
// Get the property info for the given property id.
virtual std::optional<IProperty::Info> GetPropertyInfo(const LWOOBJID id) = 0;
// Get the property info for the given property id.
virtual std::optional<IProperty::Info> GetPropertyInfo(const LWOMAPID mapId, const LWOCLONEID cloneId) = 0;
@@ -58,8 +61,14 @@ public:
// Update the property performance cost for the given property id.
virtual void UpdatePerformanceCost(const LWOZONEID& zoneId, const float performanceCost) = 0;
// Insert a new property into the database.
virtual void InsertNewProperty(const IProperty::Info& info, const uint32_t templateId, const LWOZONEID& zoneId) = 0;
// Dashboard methods
virtual std::vector<Info> GetAllProperties() = 0;
virtual std::vector<Info> GetPropertiesByApprovalStatus(uint32_t approved) = 0;
virtual uint32_t GetPropertyCount() = 0;
virtual uint32_t GetUnapprovedPropertyCount() = 0;
};
#endif //!__IPROPERTY__H__

View File

@@ -13,19 +13,19 @@ public:
}
NiPoint3 position;
NiQuaternion rotation;
NiQuaternion rotation = QuatUtils::IDENTITY;
LWOOBJID id{};
LOT lot{};
uint32_t ugcId{};
LWOOBJID ugcId{};
std::array<LWOOBJID, 5> behaviors{};
};
// Inserts a new UGC model into the database.
virtual void InsertNewUgcModel(
std::stringstream& sd0Data,
const uint32_t blueprintId,
const uint64_t blueprintId,
const uint32_t accountId,
const uint32_t characterId) = 0;
const LWOOBJID characterId) = 0;
// Get the property models for the given property id.
virtual std::vector<IPropertyContents::Model> GetPropertyModels(const LWOOBJID& propertyId) = 0;
@@ -45,6 +45,6 @@ public:
virtual void RemoveModel(const LWOOBJID& modelId) = 0;
// Gets a model by ID
virtual Model GetModel(const LWOOBJID modelID) = 0;
virtual std::optional<Model> GetModel(const LWOOBJID modelID) = 0;
};
#endif //!__IPROPERTIESCONTENTS__H__

View File

@@ -29,5 +29,7 @@ public:
// Inserts a new UGC model into the database.
virtual void UpdateUgcModelData(const LWOOBJID& modelId, std::stringstream& lxfml) = 0;
virtual std::optional<IUgc::Model> GetUgcModel(const LWOOBJID ugcId) = 0;
};
#endif //!__IUGC__H__

View File

@@ -7,7 +7,7 @@
class IUgcModularBuild {
public:
virtual void InsertUgcBuild(const std::string& modules, const LWOOBJID bigId, const std::optional<uint32_t> characterId) = 0;
virtual void InsertUgcBuild(const std::string& modules, const LWOOBJID bigId, const std::optional<LWOOBJID> characterId) = 0;
virtual void DeleteUgcBuild(const LWOOBJID bigId) = 0;
};

View File

@@ -100,7 +100,7 @@ void MySQLDatabase::SetAutoCommit(bool value) {
con->setAutoCommit(value);
}
void MySQLDatabase::DeleteCharacter(const uint32_t characterId) {
void MySQLDatabase::DeleteCharacter(const LWOOBJID characterId) {
ExecuteDelete("DELETE FROM charxml WHERE id=? LIMIT 1;", characterId);
ExecuteDelete("DELETE FROM command_log WHERE character_id=?;", characterId);
ExecuteDelete("DELETE FROM friends WHERE player_id=? OR friend_id=?;", characterId, characterId);

View File

@@ -40,33 +40,50 @@ public:
std::vector<std::string> GetApprovedCharacterNames() override;
std::vector<FriendData> GetFriendsList(uint32_t charID) override;
std::vector<FriendData> GetFriendsList(LWOOBJID charID) override;
std::optional<IFriends::BestFriendStatus> GetBestFriendStatus(const uint32_t playerCharacterId, const uint32_t friendCharacterId) override;
void SetBestFriendStatus(const uint32_t playerAccountId, const uint32_t friendAccountId, const uint32_t bestFriendStatus) override;
void AddFriend(const uint32_t playerAccountId, const uint32_t friendAccountId) override;
void RemoveFriend(const uint32_t playerAccountId, const uint32_t friendAccountId) override;
void UpdateActivityLog(const uint32_t characterId, const eActivityType activityType, const LWOMAPID mapId) override;
std::optional<IFriends::BestFriendStatus> GetBestFriendStatus(const LWOOBJID playerCharacterId, const LWOOBJID friendCharacterId) override;
void SetBestFriendStatus(const LWOOBJID playerAccountId, const LWOOBJID friendAccountId, const uint32_t bestFriendStatus) override;
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;
void CreateMigrationHistoryTable() override;
bool IsMigrationRun(const std::string_view str) override;
void InsertMigration(const std::string_view str) override;
std::optional<ICharInfo::Info> GetCharacterInfo(const uint32_t charId) override;
std::optional<ICharInfo::Info> GetCharacterInfo(const LWOOBJID charId) override;
std::optional<ICharInfo::Info> GetCharacterInfo(const std::string_view charId) override;
std::string GetCharacterXml(const uint32_t accountId) override;
void UpdateCharacterXml(const uint32_t characterId, const std::string_view lxfml) override;
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 uint32_t accountId, const std::string_view lxfml) override;
std::vector<uint32_t> GetAccountCharacterIds(uint32_t accountId) override;
void DeleteCharacter(const uint32_t characterId) override;
void SetCharacterName(const uint32_t characterId, const std::string_view name) override;
void SetPendingCharacterName(const uint32_t characterId, const std::string_view name) override;
void UpdateLastLoggedInCharacter(const uint32_t characterId) override;
void InsertCharacterXml(const LWOOBJID accountId, const std::string_view lxfml) override;
std::vector<LWOOBJID> GetAccountCharacterIds(LWOOBJID accountId) override;
void DeleteCharacter(const LWOOBJID characterId) override;
void SetCharacterName(const LWOOBJID characterId, const std::string_view name) override;
void SetPendingCharacterName(const LWOOBJID characterId, const std::string_view name) override;
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,55 +96,114 @@ 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(
std::stringstream& sd0Data,
const uint32_t blueprintId,
const uint64_t blueprintId,
const uint32_t accountId,
const uint32_t characterId) override;
std::vector<MailInfo> GetMailForPlayer(const uint32_t characterId, const uint32_t numberOfMail) override;
const LWOOBJID characterId) override;
std::vector<MailInfo> GetMailForPlayer(const LWOOBJID characterId, const uint32_t numberOfMail) override;
std::optional<MailInfo> GetMail(const uint64_t mailId) override;
uint32_t GetUnreadMailCount(const uint32_t characterId) override;
uint32_t GetUnreadMailCount(const LWOOBJID characterId) override;
void MarkMailRead(const uint64_t mailId) override;
void DeleteMail(const uint64_t mailId) override;
void ClaimMailItem(const uint64_t mailId) override;
void InsertSlashCommandUsage(const uint32_t characterId, const std::string_view command) 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;
void InsertNewAccount(const std::string_view username, const std::string_view bcryptpassword) override;
void SetMasterInfo(const IServers::MasterInfo& info) override;
std::optional<uint32_t> GetCurrentPersistentId() override;
std::optional<uint64_t> GetCurrentPersistentId() override;
IObjectIdTracker::Range GetPersistentIdRange() override;
void InsertDefaultPersistentId() override;
void UpdatePersistentId(const uint32_t id) 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 uint32_t playerId, const uint32_t ignoredPlayerId) override;
void RemoveIgnore(const uint32_t playerId, const uint32_t ignoredPlayerId) override;
std::vector<IIgnoreList::Info> GetIgnoreList(const uint32_t playerId) override;
void AddIgnore(const LWOOBJID playerId, const LWOOBJID ignoredPlayerId) override;
void RemoveIgnore(const LWOOBJID playerId, const LWOOBJID ignoredPlayerId) override;
std::vector<IIgnoreList::Info> GetIgnoreList(const LWOOBJID playerId) override;
void InsertRewardCode(const uint32_t account_id, const uint32_t reward_code) override;
std::vector<uint32_t> GetRewardCodesByAccountID(const uint32_t account_id) override;
void AddBehavior(const IBehaviors::Info& info) override;
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;
std::vector<ILeaderboard::Entry> GetNsLeaderboard(const uint32_t activityId) override;
std::vector<ILeaderboard::Entry> GetAgsLeaderboard(const uint32_t activityId) override;
void SaveScore(const uint32_t playerId, const uint32_t gameId, const Score& score) override;
void UpdateScore(const uint32_t playerId, const uint32_t gameId, const Score& score) override;
std::optional<ILeaderboard::Score> GetPlayerScore(const uint32_t playerId, const uint32_t gameId) override;
void IncrementNumWins(const uint32_t playerId, const uint32_t gameId) override;
void IncrementTimesPlayed(const uint32_t playerId, const uint32_t gameId) override;
void InsertUgcBuild(const std::string& modules, const LWOOBJID bigId, const std::optional<uint32_t> characterId) override;
void SaveScore(const LWOOBJID playerId, const uint32_t gameId, const Score& score) override;
void UpdateScore(const LWOOBJID playerId, const uint32_t gameId, const Score& score) override;
std::optional<ILeaderboard::Score> GetPlayerScore(const LWOOBJID playerId, const uint32_t gameId) override;
void IncrementNumWins(const LWOOBJID playerId, const uint32_t gameId) override;
void IncrementTimesPlayed(const LWOOBJID playerId, const uint32_t gameId) override;
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;
IPropertyContents::Model GetModel(const LWOOBJID modelID) 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:
@@ -168,91 +244,91 @@ private:
template<>
inline void SetParam(UniquePreppedStmtRef stmt, const int index, const std::string_view param) {
// LOG("%s", param.data());
LOG_DEBUG("%s", param.data());
stmt->setString(index, param.data());
}
template<>
inline void SetParam(UniquePreppedStmtRef stmt, const int index, const char* param) {
// LOG("%s", param);
LOG_DEBUG("%s", param);
stmt->setString(index, param);
}
template<>
inline void SetParam(UniquePreppedStmtRef stmt, const int index, const std::string param) {
// LOG("%s", param.c_str());
LOG_DEBUG("%s", param.c_str());
stmt->setString(index, param.c_str());
}
template<>
inline void SetParam(UniquePreppedStmtRef stmt, const int index, const int8_t param) {
// LOG("%u", param);
LOG_DEBUG("%u", param);
stmt->setByte(index, param);
}
template<>
inline void SetParam(UniquePreppedStmtRef stmt, const int index, const uint8_t param) {
// LOG("%d", param);
LOG_DEBUG("%d", param);
stmt->setByte(index, param);
}
template<>
inline void SetParam(UniquePreppedStmtRef stmt, const int index, const int16_t param) {
// LOG("%u", param);
LOG_DEBUG("%u", param);
stmt->setShort(index, param);
}
template<>
inline void SetParam(UniquePreppedStmtRef stmt, const int index, const uint16_t param) {
// LOG("%d", param);
LOG_DEBUG("%d", param);
stmt->setShort(index, param);
}
template<>
inline void SetParam(UniquePreppedStmtRef stmt, const int index, const uint32_t param) {
// LOG("%u", param);
LOG_DEBUG("%u", param);
stmt->setUInt(index, param);
}
template<>
inline void SetParam(UniquePreppedStmtRef stmt, const int index, const int32_t param) {
// LOG("%d", param);
LOG_DEBUG("%d", param);
stmt->setInt(index, param);
}
template<>
inline void SetParam(UniquePreppedStmtRef stmt, const int index, const int64_t param) {
// LOG("%llu", param);
LOG_DEBUG("%llu", param);
stmt->setInt64(index, param);
}
template<>
inline void SetParam(UniquePreppedStmtRef stmt, const int index, const uint64_t param) {
// LOG("%llu", param);
LOG_DEBUG("%llu", param);
stmt->setUInt64(index, param);
}
template<>
inline void SetParam(UniquePreppedStmtRef stmt, const int index, const float param) {
// LOG("%f", param);
LOG_DEBUG("%f", param);
stmt->setFloat(index, param);
}
template<>
inline void SetParam(UniquePreppedStmtRef stmt, const int index, const double param) {
// LOG("%f", param);
LOG_DEBUG("%f", param);
stmt->setDouble(index, param);
}
template<>
inline void SetParam(UniquePreppedStmtRef stmt, const int index, const bool param) {
// LOG("%d", param);
LOG_DEBUG("%s", param ? "true" : "false");
stmt->setBoolean(index, param);
}
template<>
inline void SetParam(UniquePreppedStmtRef stmt, const int index, const std::istream* param) {
// LOG("Blob");
LOG_DEBUG("Blob");
// This is the one time you will ever see me use const_cast.
stmt->setBlob(index, const_cast<std::istream*>(param));
}
@@ -260,10 +336,21 @@ inline void SetParam(UniquePreppedStmtRef stmt, const int index, const std::istr
template<>
inline void SetParam(UniquePreppedStmtRef stmt, const int index, const std::optional<uint32_t> param) {
if (param) {
// LOG("%d", param.value());
LOG_DEBUG("%d", param.value());
stmt->setInt(index, param.value());
} else {
// LOG("Null");
LOG_DEBUG("Null");
stmt->setNull(index, sql::DataType::SQLNULL);
}
}
template<>
inline void SetParam(UniquePreppedStmtRef stmt, const int index, const std::optional<LWOOBJID> param) {
if (param) {
LOG_DEBUG("%d", param.value());
stmt->setInt64(index, param.value());
} else {
LOG_DEBUG("Null");
stmt->setNull(index, sql::DataType::SQLNULL);
}
}

View File

@@ -3,7 +3,7 @@
#include "eGameMasterLevel.h"
std::optional<IAccounts::Info> MySQLDatabase::GetAccountInfo(const std::string_view username) {
auto result = ExecuteSelect("SELECT id, password, banned, locked, play_key_id, gm_level FROM accounts WHERE name = ? LIMIT 1;", username);
auto result = ExecuteSelect("SELECT id, password, banned, locked, play_key_id, gm_level, mute_expire FROM accounts WHERE name = ? LIMIT 1;", username);
if (!result->next()) {
return std::nullopt;
@@ -16,6 +16,7 @@ std::optional<IAccounts::Info> MySQLDatabase::GetAccountInfo(const std::string_v
toReturn.banned = result->getBoolean("banned");
toReturn.locked = result->getBoolean("locked");
toReturn.playKeyId = result->getUInt("play_key_id");
toReturn.muteExpire = result->getUInt64("mute_expire");
return toReturn;
}
@@ -40,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

@@ -1,6 +1,63 @@
#include "MySQLDatabase.h"
void MySQLDatabase::UpdateActivityLog(const uint32_t characterId, const eActivityType activityType, const LWOMAPID mapId) {
void MySQLDatabase::UpdateActivityLog(const LWOOBJID characterId, const eActivityType activityType, const LWOMAPID mapId) {
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;
}

Some files were not shown because too many files have changed in this diff Show More