mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-06-13 18:24:20 +00:00
Compare commits
35 Commits
limit-user
...
web-dashbo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3467465b4 | ||
|
|
d532a9b063 | ||
|
|
5453d163a3 | ||
|
|
62ac65c520 | ||
|
|
5d5bce53d0 | ||
|
|
5791c55a9e | ||
|
|
17d0c45382 | ||
|
|
7dbbef81ac | ||
|
|
06958cb9cd | ||
|
|
69b1a694a6 | ||
|
|
b2609ff6cb | ||
|
|
e8c0b3e6da | ||
|
|
25418fd8b2 | ||
|
|
502c965d97 | ||
|
|
205c190c61 | ||
|
|
670cb124c0 | ||
|
|
76c2f380bf | ||
|
|
b5a3cc9187 | ||
|
|
74e1d36bb1 | ||
|
|
64faac714c | ||
|
|
4a5dd68e87 | ||
|
|
4a577f233d | ||
|
|
bb05b3ac0d | ||
|
|
06022e4b19 | ||
|
|
6389876c6e | ||
|
|
68f2e2dee2 | ||
|
|
b798da8ef8 | ||
|
|
154112050f | ||
|
|
6d3bf2fdc3 | ||
|
|
566a18df38 | ||
|
|
f6c13d9ee6 | ||
|
|
8198ad70f6 | ||
|
|
4c3bace601 | ||
|
|
6d2a21450b | ||
|
|
f9e74e6994 |
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#endif
|
||||
|
||||
#include "NiQuaternion.h"
|
||||
#include <glm/ext/quaternion_float.hpp>
|
||||
|
||||
// MARK: Getters / Setters
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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__
|
||||
|
||||
@@ -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__
|
||||
|
||||
@@ -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
|
||||
|
||||
55
dDashboardServer/CMakeLists.txt
Normal file
55
dDashboardServer/CMakeLists.txt
Normal 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"
|
||||
)
|
||||
33
dDashboardServer/DashboardHelpers.cpp
Normal file
33
dDashboardServer/DashboardHelpers.cpp
Normal 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
|
||||
24
dDashboardServer/DashboardHelpers.h
Normal file
24
dDashboardServer/DashboardHelpers.h
Normal 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);
|
||||
|
||||
}
|
||||
248
dDashboardServer/DashboardServer.cpp
Normal file
248
dDashboardServer/DashboardServer.cpp
Normal 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
|
||||
}
|
||||
187
dDashboardServer/DashboardShared.h
Normal file
187
dDashboardServer/DashboardShared.h
Normal 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__
|
||||
153
dDashboardServer/DashboardWeb.cpp
Normal file
153
dDashboardServer/DashboardWeb.cpp
Normal 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
|
||||
19
dDashboardServer/DashboardWeb.h
Normal file
19
dDashboardServer/DashboardWeb.h
Normal 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__
|
||||
143
dDashboardServer/better-templates/base.mustache
Normal file
143
dDashboardServer/better-templates/base.mustache
Normal 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>
|
||||
113
dDashboardServer/better-templates/header.mustache
Normal file
113
dDashboardServer/better-templates/header.mustache
Normal 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>
|
||||
1344
dDashboardServer/blueprints/ApiBlueprint.cpp
Normal file
1344
dDashboardServer/blueprints/ApiBlueprint.cpp
Normal file
File diff suppressed because it is too large
Load Diff
17
dDashboardServer/blueprints/ApiBlueprint.h
Normal file
17
dDashboardServer/blueprints/ApiBlueprint.h
Normal 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
|
||||
129
dDashboardServer/blueprints/AuthBlueprint.cpp
Normal file
129
dDashboardServer/blueprints/AuthBlueprint.cpp
Normal 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
|
||||
17
dDashboardServer/blueprints/AuthBlueprint.h
Normal file
17
dDashboardServer/blueprints/AuthBlueprint.h
Normal 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
|
||||
234
dDashboardServer/blueprints/BugReportsBlueprint.cpp
Normal file
234
dDashboardServer/blueprints/BugReportsBlueprint.cpp
Normal 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
|
||||
20
dDashboardServer/blueprints/BugReportsBlueprint.h
Normal file
20
dDashboardServer/blueprints/BugReportsBlueprint.h
Normal 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__
|
||||
14
dDashboardServer/blueprints/CMakeLists.txt
Normal file
14
dDashboardServer/blueprints/CMakeLists.txt
Normal 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()
|
||||
263
dDashboardServer/blueprints/CharactersBlueprint.cpp
Normal file
263
dDashboardServer/blueprints/CharactersBlueprint.cpp
Normal 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
|
||||
20
dDashboardServer/blueprints/CharactersBlueprint.h
Normal file
20
dDashboardServer/blueprints/CharactersBlueprint.h
Normal 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__
|
||||
207
dDashboardServer/blueprints/MailBlueprint.cpp
Normal file
207
dDashboardServer/blueprints/MailBlueprint.cpp
Normal 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
|
||||
20
dDashboardServer/blueprints/MailBlueprint.h
Normal file
20
dDashboardServer/blueprints/MailBlueprint.h
Normal 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__
|
||||
279
dDashboardServer/blueprints/ModerationBlueprint.cpp
Normal file
279
dDashboardServer/blueprints/ModerationBlueprint.cpp
Normal 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
|
||||
20
dDashboardServer/blueprints/ModerationBlueprint.h
Normal file
20
dDashboardServer/blueprints/ModerationBlueprint.h
Normal 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__
|
||||
380
dDashboardServer/blueprints/PageBlueprint.cpp
Normal file
380
dDashboardServer/blueprints/PageBlueprint.cpp
Normal 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
|
||||
17
dDashboardServer/blueprints/PageBlueprint.h
Normal file
17
dDashboardServer/blueprints/PageBlueprint.h
Normal 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
|
||||
288
dDashboardServer/blueprints/PlayKeysBlueprint.cpp
Normal file
288
dDashboardServer/blueprints/PlayKeysBlueprint.cpp
Normal 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
|
||||
20
dDashboardServer/blueprints/PlayKeysBlueprint.h
Normal file
20
dDashboardServer/blueprints/PlayKeysBlueprint.h
Normal 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__
|
||||
144
dDashboardServer/static/css/dashboard.css
Normal file
144
dDashboardServer/static/css/dashboard.css
Normal 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 */
|
||||
144
dDashboardServer/static/js/api.js
Normal file
144
dDashboardServer/static/js/api.js
Normal 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';
|
||||
}
|
||||
}
|
||||
188
dDashboardServer/static/js/dashboard.js
Normal file
188
dDashboardServer/static/js/dashboard.js
Normal 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);
|
||||
});
|
||||
});
|
||||
46
dDashboardServer/static/js/login.js
Normal file
46
dDashboardServer/static/js/login.js
Normal 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();
|
||||
}
|
||||
43
dDashboardServer/static/js/register.js
Normal file
43
dDashboardServer/static/js/register.js
Normal 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';
|
||||
}
|
||||
});
|
||||
});
|
||||
75
dDashboardServer/static/js/wait-for-jq-dt.js
Normal file
75
dDashboardServer/static/js/wait-for-jq-dt.js
Normal 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);
|
||||
102
dDashboardServer/templates/about.html
Normal file
102
dDashboardServer/templates/about.html
Normal 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>
|
||||
162
dDashboardServer/templates/accounts/index.html
Normal file
162
dDashboardServer/templates/accounts/index.html
Normal 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>
|
||||
214
dDashboardServer/templates/accounts/view.html
Normal file
214
dDashboardServer/templates/accounts/view.html
Normal 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>
|
||||
151
dDashboardServer/templates/bugreports/index.html
Normal file
151
dDashboardServer/templates/bugreports/index.html
Normal 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>
|
||||
163
dDashboardServer/templates/characters/index.html
Normal file
163
dDashboardServer/templates/characters/index.html
Normal 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>
|
||||
314
dDashboardServer/templates/characters/view.html
Normal file
314
dDashboardServer/templates/characters/view.html
Normal 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>
|
||||
293
dDashboardServer/templates/index.html
Normal file
293
dDashboardServer/templates/index.html
Normal 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>
|
||||
157
dDashboardServer/templates/layouts/base.html
Normal file
157
dDashboardServer/templates/layouts/base.html
Normal 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 © 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>
|
||||
31
dDashboardServer/templates/login.html
Normal file
31
dDashboardServer/templates/login.html
Normal 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>
|
||||
73
dDashboardServer/templates/logs/activities.html
Normal file
73
dDashboardServer/templates/logs/activities.html
Normal 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>
|
||||
139
dDashboardServer/templates/logs/audits.html
Normal file
139
dDashboardServer/templates/logs/audits.html
Normal 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>
|
||||
106
dDashboardServer/templates/logs/commands.html
Normal file
106
dDashboardServer/templates/logs/commands.html
Normal 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>
|
||||
80
dDashboardServer/templates/mail/send.html
Normal file
80
dDashboardServer/templates/mail/send.html
Normal 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>
|
||||
85
dDashboardServer/templates/moderation/pets.html
Normal file
85
dDashboardServer/templates/moderation/pets.html
Normal 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>
|
||||
82
dDashboardServer/templates/moderation/properties.html
Normal file
82
dDashboardServer/templates/moderation/properties.html
Normal 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>
|
||||
155
dDashboardServer/templates/playkeys/index.html
Normal file
155
dDashboardServer/templates/playkeys/index.html
Normal 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>
|
||||
31
dDashboardServer/templates/register.html
Normal file
31
dDashboardServer/templates/register.html
Normal 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>
|
||||
@@ -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})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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__
|
||||
|
||||
@@ -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__
|
||||
|
||||
@@ -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__
|
||||
|
||||
@@ -9,7 +9,7 @@ class IBehaviors {
|
||||
public:
|
||||
struct Info {
|
||||
LWOOBJID behaviorId{};
|
||||
uint32_t characterId{};
|
||||
LWOOBJID characterId{};
|
||||
std::string behaviorInfo;
|
||||
};
|
||||
|
||||
|
||||
@@ -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__
|
||||
|
||||
@@ -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__
|
||||
|
||||
@@ -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__
|
||||
|
||||
@@ -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__
|
||||
|
||||
58
dDatabase/GameDatabase/ITables/IDashboardAuditLog.h
Normal file
58
dDatabase/GameDatabase/ITables/IDashboardAuditLog.h
Normal 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__
|
||||
27
dDatabase/GameDatabase/ITables/IDashboardConfig.h
Normal file
27
dDatabase/GameDatabase/ITables/IDashboardConfig.h
Normal 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__
|
||||
@@ -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__
|
||||
|
||||
@@ -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__
|
||||
|
||||
@@ -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__
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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__
|
||||
|
||||
@@ -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__
|
||||
|
||||
@@ -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__
|
||||
|
||||
@@ -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__
|
||||
|
||||
@@ -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__
|
||||
|
||||
@@ -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__
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {};
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user