diff --git a/dDashboardServer/CMakeLists.txt b/dDashboardServer/CMakeLists.txt index efcb3eaa..39a6859b 100644 --- a/dDashboardServer/CMakeLists.txt +++ b/dDashboardServer/CMakeLists.txt @@ -1,17 +1,10 @@ set(DDASHBOARDSERVER_SOURCES "DashboardServer.cpp" - "MasterPacketHandler.cpp" - "routes/APIRoutes.cpp" - "routes/StaticRoutes.cpp" - "routes/DashboardRoutes.cpp" - "routes/WSRoutes.cpp" - "routes/AuthRoutes.cpp" - "auth/JWTUtils.cpp" - "auth/DashboardAuthService.cpp" - "auth/AuthMiddleware.cpp" - "auth/RequireAuthMiddleware.cpp" ) +add_subdirectory(routes) +add_subdirectory(auth) + add_executable(DashboardServer ${DDASHBOARDSERVER_SOURCES}) target_include_directories(DashboardServer PRIVATE @@ -29,11 +22,12 @@ target_include_directories(DashboardServer PRIVATE "${PROJECT_SOURCE_DIR}/dServer" "${PROJECT_SOURCE_DIR}/thirdparty" "${PROJECT_SOURCE_DIR}/thirdparty/nlohmann" + "${PROJECT_SOURCE_DIR}/dDashboardServer" "${PROJECT_SOURCE_DIR}/dDashboardServer/auth" "${PROJECT_SOURCE_DIR}/dDashboardServer/routes" ) -target_link_libraries(DashboardServer ${COMMON_LIBRARIES} dWeb dServer bcrypt OpenSSL::Crypto) +target_link_libraries(DashboardServer ${COMMON_LIBRARIES} dWeb dServer bcrypt OpenSSL::Crypto DashboardRoutes DashboardAuth) # Copy static files and templates to build directory (always copy) diff --git a/dDashboardServer/DashboardServer.cpp b/dDashboardServer/DashboardServer.cpp index d8ff6ff0..e7f5e8a2 100644 --- a/dDashboardServer/DashboardServer.cpp +++ b/dDashboardServer/DashboardServer.cpp @@ -20,14 +20,13 @@ #include "Diagnostics.h" #include "Web.h" #include "Server.h" -#include "MasterPacketHandler.h" -#include "routes/ServerState.h" -#include "routes/APIRoutes.h" -#include "routes/StaticRoutes.h" -#include "routes/DashboardRoutes.h" -#include "routes/WSRoutes.h" -#include "routes/AuthRoutes.h" +#include "ServerState.h" +#include "APIRoutes.h" +#include "StaticRoutes.h" +#include "DashboardRoutes.h" +#include "WSRoutes.h" +#include "AuthRoutes.h" #include "AuthMiddleware.h" namespace Game { @@ -157,12 +156,22 @@ int main(int argc, char** argv) { const auto elapsedSinceBroadcast = std::chrono::duration_cast(currentTime - lastBroadcast).count(); if (elapsed >= 1000.0f / 60.0f) { - // Handle master server packets - Packet* packet = g_Server->ReceiveFromMaster(); - if (packet) { - MasterPacketHandler::HandleMasterPacket(packet); - g_Server->DeallocateMasterPacket(packet); - } + // // Handle master server packets + // Packet* packet = g_Server->ReceiveFromMaster(); + // if (packet) { + // RakNet::BitStream bitStream(packet->data, packet->length, false); + // PacketHandler::HandlePacket(bitStream, packet->systemAddress); + // g_Server->DeallocateMasterPacket(packet); + // } + + // // Handle RakNet protocol packets from connected servers + // packet = g_Server->Receive(); + // while (packet) { + // RakNet::BitStream bitStream(packet->data, packet->length, false); + // PacketHandler::HandlePacket(bitStream, packet->systemAddress); + // g_Server->DeallocatePacket(packet); + // packet = g_Server->Receive(); + // } // Handle web requests Game::web.ReceiveRequests(); @@ -190,3 +199,5 @@ int main(int argc, char** argv) { return EXIT_SUCCESS; } + + diff --git a/dDashboardServer/MasterPacketHandler.cpp b/dDashboardServer/MasterPacketHandler.cpp deleted file mode 100644 index 85b2ed74..00000000 --- a/dDashboardServer/MasterPacketHandler.cpp +++ /dev/null @@ -1,209 +0,0 @@ -#include "MasterPacketHandler.h" - -#include "BitStreamUtils.h" -#include "dServer.h" -#include "Game.h" -#include "Logger.h" -#include "RakNetTypes.h" -#include "routes/ServerState.h" -#include -#include - -namespace MasterPacketHandler { - namespace { - std::map()>> g_Handlers = { - {MessageType::Master::SERVER_INFO, []() { - return std::make_unique(); - }}, - {MessageType::Master::PLAYER_ADDED, []() { - return std::make_unique(); - }}, - {MessageType::Master::PLAYER_REMOVED, []() { - return std::make_unique(); - }}, - {MessageType::Master::SHUTDOWN_RESPONSE, []() { - return std::make_unique(); - }}, - {MessageType::Master::SHUTDOWN, []() { - return std::make_unique(); - }}, - }; - } - - bool ServerInfo::Deserialize(RakNet::BitStream& bitStream) { - VALIDATE_READ(bitStream.Read(port)); - VALIDATE_READ(bitStream.Read(zoneID)); - VALIDATE_READ(bitStream.Read(instanceID)); - VALIDATE_READ(bitStream.Read(serverType)); - LUString ipStr{}; - VALIDATE_READ(bitStream.Read(ipStr)); - ip = ipStr.string; - return true; - } - - void ServerInfo::Handle() { - std::lock_guard lock(ServerState::g_StatusMutex); - - LOG("MasterPacketHandler: Processing SERVER_INFO for service type %i, zone %u, instance %u, port %u", serverType, zoneID, instanceID, port); - - switch (serverType) { - case ServiceType::AUTH: - ServerState::g_AuthStatus.online = true; - ServerState::g_AuthStatus.lastSeen = std::chrono::steady_clock::now(); - LOG("Updated Auth server status: online"); - break; - case ServiceType::CHAT: - ServerState::g_ChatStatus.online = true; - ServerState::g_ChatStatus.lastSeen = std::chrono::steady_clock::now(); - LOG("Updated Chat server status: online"); - break; - case ServiceType::WORLD: { - // Update or add world instance - bool found = false; - for (auto& world : ServerState::g_WorldInstances) { - if (world.mapID == zoneID && world.instanceID == instanceID) { - world.ip = ip; - world.port = port; - found = true; - break; - } - } - if (!found) { - WorldInstanceInfo info{}; - info.mapID = zoneID; - info.instanceID = instanceID; - info.cloneID = 0; - info.players = 0; - info.ip = ip; - info.port = port; - info.isPrivate = false; - ServerState::g_WorldInstances.push_back(info); - LOG("Added world instance: map %u instance %u", zoneID, instanceID); - } - break; - } - default: - break; - } - } - - bool PlayerAdded::Deserialize(RakNet::BitStream& bitStream) { - VALIDATE_READ(bitStream.Read(zoneID)); - VALIDATE_READ(bitStream.Read(instanceID)); - return true; - } - - void PlayerAdded::Handle() { - std::lock_guard lock(ServerState::g_StatusMutex); - for (auto& world : ServerState::g_WorldInstances) { - if (world.mapID == zoneID && world.instanceID == instanceID) { - world.players++; - LOG_DEBUG("Player added to map %u instance %u, now %u players", zoneID, instanceID, world.players); - break; - } - } - } - - bool PlayerRemoved::Deserialize(RakNet::BitStream& bitStream) { - VALIDATE_READ(bitStream.Read(zoneID)); - VALIDATE_READ(bitStream.Read(instanceID)); - return true; - } - - void PlayerRemoved::Handle() { - std::lock_guard lock(ServerState::g_StatusMutex); - for (auto& world : ServerState::g_WorldInstances) { - if (world.mapID == zoneID && world.instanceID == instanceID) { - if (world.players > 0) world.players--; - LOG_DEBUG("Player removed from map %u instance %u, now %u players", zoneID, instanceID, world.players); - break; - } - } - } - - bool ShutdownResponse::Deserialize(RakNet::BitStream& bitStream) { - VALIDATE_READ(bitStream.Read(zoneID)); - VALIDATE_READ(bitStream.Read(instanceID)); - VALIDATE_READ(bitStream.Read(serverType)); - return true; - } - - void ShutdownResponse::Handle() { - std::lock_guard lock(ServerState::g_StatusMutex); - - switch (serverType) { - case ServiceType::AUTH: - ServerState::g_AuthStatus.online = false; - LOG_DEBUG("Auth server shutdown"); - break; - case ServiceType::CHAT: - ServerState::g_ChatStatus.online = false; - LOG_DEBUG("Chat server shutdown"); - break; - case ServiceType::WORLD: - for (auto it = ServerState::g_WorldInstances.begin(); it != ServerState::g_WorldInstances.end(); ++it) { - if (it->mapID == zoneID && it->instanceID == instanceID) { - ServerState::g_WorldInstances.erase(it); - LOG_DEBUG("Removed shutdown instance: map %u instance %u", zoneID, instanceID); - break; - } - } - break; - default: - break; - } - } - - bool Shutdown::Deserialize(RakNet::BitStream& bitStream) { - // SHUTDOWN message has no additional data - return true; - } - - void Shutdown::Handle() { - LOG("Received SHUTDOWN command from Master"); - Game::lastSignal = -1; // Trigger shutdown - } - - void HandleMasterPacket(Packet* packet) { - if (!packet) return; - - switch (packet->data[0]) { - case ID_DISCONNECTION_NOTIFICATION: - case ID_CONNECTION_LOST: - LOG("Lost connection to Master Server"); - { - std::lock_guard lock(ServerState::g_StatusMutex); - ServerState::g_AuthStatus.online = false; - ServerState::g_ChatStatus.online = false; - ServerState::g_WorldInstances.clear(); - } - break; - case ID_CONNECTION_REQUEST_ACCEPTED: - LOG("Connected to Master Server"); - break; - case ID_USER_PACKET_ENUM: { - RakNet::BitStream inStream(packet->data, packet->length, false); - uint64_t header{}; - inStream.Read(header); - - const auto packetType = static_cast(header); - LOG_DEBUG("Received Master packet type: %i", packetType); - - auto it = g_Handlers.find(packetType); - if (it != g_Handlers.end()) { - auto handler = it->second(); - if (!handler->Deserialize(inStream)) { - LOG_DEBUG("Error deserializing Master packet type %i", packetType); - return; - } - handler->Handle(); - } else { - LOG_DEBUG("Unhandled Master packet type: %i", packetType); - } - break; - } - default: - break; - } - } -} diff --git a/dDashboardServer/MasterPacketHandler.h b/dDashboardServer/MasterPacketHandler.h deleted file mode 100644 index a2cd26d9..00000000 --- a/dDashboardServer/MasterPacketHandler.h +++ /dev/null @@ -1,79 +0,0 @@ -#pragma once - -#include -#include -#include - -#include "dCommonVars.h" -#include "MessageType/Master.h" -#include "BitStream.h" - -struct Packet; - -namespace MasterPacketHandler { - // Base class for all master packet handlers - class MasterPacket { - public: - virtual ~MasterPacket() = default; - virtual bool Deserialize(RakNet::BitStream& bitStream) = 0; - virtual void Handle() = 0; - }; - - // SERVER_INFO packet handler - class ServerInfo : public MasterPacket { - public: - bool Deserialize(RakNet::BitStream& bitStream) override; - void Handle() override; - - private: - uint32_t port{0}; - uint32_t zoneID{0}; - uint32_t instanceID{0}; - ServiceType serverType{}; - std::string ip{}; - }; - - // PLAYER_ADDED packet handler - class PlayerAdded : public MasterPacket { - public: - bool Deserialize(RakNet::BitStream& bitStream) override; - void Handle() override; - - private: - LWOMAPID zoneID{}; - LWOINSTANCEID instanceID{}; - }; - - // PLAYER_REMOVED packet handler - class PlayerRemoved : public MasterPacket { - public: - bool Deserialize(RakNet::BitStream& bitStream) override; - void Handle() override; - - private: - LWOMAPID zoneID{}; - LWOINSTANCEID instanceID{}; - }; - - // SHUTDOWN_RESPONSE packet handler - class ShutdownResponse : public MasterPacket { - public: - bool Deserialize(RakNet::BitStream& bitStream) override; - void Handle() override; - - private: - uint32_t zoneID{}; - uint32_t instanceID{}; - ServiceType serverType{}; - }; - - // SHUTDOWN packet handler - class Shutdown : public MasterPacket { - public: - bool Deserialize(RakNet::BitStream& bitStream) override; - void Handle() override; - }; - - // Main handler function - void HandleMasterPacket(Packet* packet); -} diff --git a/dDashboardServer/auth/CMakeLists.txt b/dDashboardServer/auth/CMakeLists.txt new file mode 100644 index 00000000..8f7a4aec --- /dev/null +++ b/dDashboardServer/auth/CMakeLists.txt @@ -0,0 +1,28 @@ +set(DASHBOARDAUTH_SOURCES + "JWTUtils.cpp" + "DashboardAuthService.cpp" + "AuthMiddleware.cpp" + "RequireAuthMiddleware.cpp" +) + +add_library(DashboardAuth STATIC ${DASHBOARDAUTH_SOURCES}) + +target_include_directories(DashboardAuth PRIVATE + "${PROJECT_SOURCE_DIR}/dCommon" + "${PROJECT_SOURCE_DIR}/dCommon/dClient" + "${PROJECT_SOURCE_DIR}/dCommon/dEnums" + "${PROJECT_SOURCE_DIR}/dDatabase" + "${PROJECT_SOURCE_DIR}/dDatabase/CDClientDatabase" + "${PROJECT_SOURCE_DIR}/dDatabase/CDClientDatabase/CDClientTables" + "${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase" + "${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase/ITables" + "${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase/MySQL" + "${PROJECT_SOURCE_DIR}/dNet" + "${PROJECT_SOURCE_DIR}/dWeb" + "${PROJECT_SOURCE_DIR}/dServer" + "${PROJECT_SOURCE_DIR}/thirdparty" + "${PROJECT_SOURCE_DIR}/thirdparty/nlohmann" + "${PROJECT_SOURCE_DIR}/dDashboardServer/auth" +) + +target_link_libraries(DashboardAuth PRIVATE ${COMMON_LIBRARIES} dWeb dServer bcrypt OpenSSL::Crypto) diff --git a/dDashboardServer/routes/APIRoutes.cpp b/dDashboardServer/routes/APIRoutes.cpp index 2e4345e5..49dadd77 100644 --- a/dDashboardServer/routes/APIRoutes.cpp +++ b/dDashboardServer/routes/APIRoutes.cpp @@ -17,30 +17,7 @@ void RegisterAPIRoutes() { .method = eHTTPMethod::GET, .middleware = { std::make_shared(0) }, .handle = [](HTTPReply& reply, const HTTPContext& context) { - std::lock_guard lock(ServerState::g_StatusMutex); - - nlohmann::json response = { - {"auth", { - {"online", ServerState::g_AuthStatus.online}, - {"players", ServerState::g_AuthStatus.players}, - {"version", ServerState::g_AuthStatus.version} - }}, - {"chat", { - {"online", ServerState::g_ChatStatus.online}, - {"players", ServerState::g_ChatStatus.players} - }}, - {"worlds", nlohmann::json::array()} - }; - - for (const auto& world : ServerState::g_WorldInstances) { - response["worlds"].push_back({ - {"mapID", world.mapID}, - {"instanceID", world.instanceID}, - {"cloneID", world.cloneID}, - {"players", world.players}, - {"isPrivate", world.isPrivate} - }); - } + nlohmann::json response = ServerState::GetServerStateJson(); reply.status = eHTTPStatusCode::OK; reply.message = response.dump(); @@ -92,10 +69,375 @@ void RegisterAPIRoutes() { .method = eHTTPMethod::GET, .middleware = { std::make_shared(0) }, .handle = [](HTTPReply& reply, const HTTPContext& context) { - nlohmann::json response = {{"count", 0}, {"note", "Not yet implemented"}}; - reply.status = eHTTPStatusCode::OK; - reply.message = response.dump(); - reply.contentType = eContentType::APPLICATION_JSON; + try { + const uint32_t count = Database::Get()->GetCharacterCount(); + nlohmann::json response = {{"count", count}}; + reply.status = eHTTPStatusCode::OK; + reply.message = response.dump(); + reply.contentType = eContentType::APPLICATION_JSON; + } catch (std::exception& ex) { + LOG("Error in /api/characters/count: %s", ex.what()); + reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR; + reply.message = "{\"error\":\"Database error\"}"; + reply.contentType = eContentType::APPLICATION_JSON; + } + } + }); + + // POST /api/tables/accounts - Get accounts table data (DataTables.js format) + Game::web.RegisterHTTPRoute({ + .path = "/api/tables/accounts", + .method = eHTTPMethod::POST, + .middleware = { std::make_shared(0) }, + .handle = [](HTTPReply& reply, const HTTPContext& context) { + try { + // Only admins (GM > 0) can access table data + if (context.gmLevel == 0) { + reply.status = eHTTPStatusCode::FORBIDDEN; + reply.message = "{\"error\":\"Forbidden - Admin access required\"}"; + reply.contentType = eContentType::APPLICATION_JSON; + return; + } + + nlohmann::json requestData = nlohmann::json::parse(context.body); + + // Extract DataTables parameters + uint32_t draw = requestData.value("draw", 1); + uint32_t start = requestData.value("start", 0); + uint32_t length = requestData.value("length", 10); + + // Extract search - it can be a string or an object with a "value" property + std::string search = ""; + if (requestData.contains("search")) { + if (requestData["search"].is_string()) { + search = requestData["search"].get(); + } else if (requestData["search"].is_object() && requestData["search"].contains("value")) { + search = requestData["search"]["value"].get(); + } + } + + uint32_t orderColumn = 0; + bool orderAsc = true; + + // Extract order parameters + if (requestData.contains("order") && requestData["order"].is_array() && requestData["order"].size() > 0) { + orderColumn = requestData["order"][0].value("column", 0); + orderAsc = requestData["order"][0].value("dir", "asc") == "asc"; + } + + // Get the accounts table data + nlohmann::json response = Database::Get()->GetAccountsTable(start, length, search, orderColumn, orderAsc); + + reply.status = eHTTPStatusCode::OK; + reply.message = response.dump(); + reply.contentType = eContentType::APPLICATION_JSON; + } catch (const nlohmann::json::exception& jsonEx) { + LOG("JSON error in /api/tables/accounts: %s", jsonEx.what()); + reply.status = eHTTPStatusCode::BAD_REQUEST; + reply.message = "{\"error\":\"Invalid JSON\"}"; + reply.contentType = eContentType::APPLICATION_JSON; + } catch (std::exception& ex) { + LOG("Error in /api/tables/accounts: %s", ex.what()); + reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR; + reply.message = "{\"error\":\"Database error\"}"; + reply.contentType = eContentType::APPLICATION_JSON; + } + } + }); + + // POST /api/tables/characters - Get characters table data (DataTables.js format) + Game::web.RegisterHTTPRoute({ + .path = "/api/tables/characters", + .method = eHTTPMethod::POST, + .middleware = { std::make_shared(0) }, + .handle = [](HTTPReply& reply, const HTTPContext& context) { + try { + // Only admins (GM > 0) can access table data + if (context.gmLevel == 0) { + reply.status = eHTTPStatusCode::FORBIDDEN; + reply.message = "{\"error\":\"Forbidden - Admin access required\"}"; + reply.contentType = eContentType::APPLICATION_JSON; + return; + } + + nlohmann::json requestData = nlohmann::json::parse(context.body); + + uint32_t draw = requestData.value("draw", 1); + uint32_t start = requestData.value("start", 0); + uint32_t length = requestData.value("length", 10); + + std::string search = ""; + if (requestData.contains("search")) { + if (requestData["search"].is_string()) { + search = requestData["search"].get(); + } else if (requestData["search"].is_object() && requestData["search"].contains("value")) { + search = requestData["search"]["value"].get(); + } + } + + uint32_t orderColumn = 0; + bool orderAsc = true; + + if (requestData.contains("order") && requestData["order"].is_array() && requestData["order"].size() > 0) { + orderColumn = requestData["order"][0].value("column", 0); + orderAsc = requestData["order"][0].value("dir", "asc") == "asc"; + } + + std::string tableData = Database::Get()->GetCharactersTable(start, length, search, orderColumn, orderAsc); + + nlohmann::json response = nlohmann::json::parse(tableData); + response["draw"] = draw; + + reply.status = eHTTPStatusCode::OK; + reply.message = response.dump(); + reply.contentType = eContentType::APPLICATION_JSON; + } catch (const nlohmann::json::exception& jsonEx) { + LOG("JSON error in /api/tables/characters: %s", jsonEx.what()); + reply.status = eHTTPStatusCode::BAD_REQUEST; + reply.message = "{\"error\":\"Invalid JSON\"}"; + reply.contentType = eContentType::APPLICATION_JSON; + } catch (std::exception& ex) { + LOG("Error in /api/tables/characters: %s", ex.what()); + reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR; + reply.message = "{\"error\":\"Database error\"}"; + reply.contentType = eContentType::APPLICATION_JSON; + } + } + }); + + // POST /api/tables/play_keys - Get play keys table data (DataTables.js format) + Game::web.RegisterHTTPRoute({ + .path = "/api/tables/play_keys", + .method = eHTTPMethod::POST, + .middleware = { std::make_shared(0) }, + .handle = [](HTTPReply& reply, const HTTPContext& context) { + try { // Only admins (GM > 0) can access table data + if (context.gmLevel == 0) { + reply.status = eHTTPStatusCode::FORBIDDEN; + reply.message = "{\"error\":\"Forbidden - Admin access required\"}"; + reply.contentType = eContentType::APPLICATION_JSON; + return; + } + nlohmann::json requestData = nlohmann::json::parse(context.body); + + uint32_t draw = requestData.value("draw", 1); + uint32_t start = requestData.value("start", 0); + uint32_t length = requestData.value("length", 10); + + std::string search = ""; + if (requestData.contains("search")) { + if (requestData["search"].is_string()) { + search = requestData["search"].get(); + } else if (requestData["search"].is_object() && requestData["search"].contains("value")) { + search = requestData["search"]["value"].get(); + } + } + + uint32_t orderColumn = 0; + bool orderAsc = true; + + if (requestData.contains("order") && requestData["order"].is_array() && requestData["order"].size() > 0) { + orderColumn = requestData["order"][0].value("column", 0); + orderAsc = requestData["order"][0].value("dir", "asc") == "asc"; + } + + std::string tableData = Database::Get()->GetPlayKeysTable(start, length, search, orderColumn, orderAsc); + + nlohmann::json response = nlohmann::json::parse(tableData); + response["draw"] = draw; + + reply.status = eHTTPStatusCode::OK; + reply.message = response.dump(); + reply.contentType = eContentType::APPLICATION_JSON; + } catch (const nlohmann::json::exception& jsonEx) { + LOG("JSON error in /api/tables/play_keys: %s", jsonEx.what()); + reply.status = eHTTPStatusCode::BAD_REQUEST; + reply.message = "{\"error\":\"Invalid JSON\"}"; + reply.contentType = eContentType::APPLICATION_JSON; + } catch (std::exception& ex) { + LOG("Error in /api/tables/play_keys: %s", ex.what()); + reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR; + reply.message = "{\"error\":\"Database error\"}"; + reply.contentType = eContentType::APPLICATION_JSON; + } + } + }); + + // POST /api/tables/properties - Get properties table data (DataTables.js format) + Game::web.RegisterHTTPRoute({ + .path = "/api/tables/properties", + .method = eHTTPMethod::POST, + .middleware = { std::make_shared(0) }, + .handle = [](HTTPReply& reply, const HTTPContext& context) { + try { + // Only admins (GM > 0) can access table data + if (context.gmLevel == 0) { + reply.status = eHTTPStatusCode::FORBIDDEN; + reply.message = "{\"error\":\"Forbidden - Admin access required\"}"; + reply.contentType = eContentType::APPLICATION_JSON; + return; + } + + nlohmann::json requestData = nlohmann::json::parse(context.body); + + uint32_t draw = requestData.value("draw", 1); + uint32_t start = requestData.value("start", 0); + uint32_t length = requestData.value("length", 10); + + std::string search = ""; + if (requestData.contains("search")) { + if (requestData["search"].is_string()) { + search = requestData["search"].get(); + } else if (requestData["search"].is_object() && requestData["search"].contains("value")) { + search = requestData["search"]["value"].get(); + } + } + + uint32_t orderColumn = 0; + bool orderAsc = true; + + if (requestData.contains("order") && requestData["order"].is_array() && requestData["order"].size() > 0) { + orderColumn = requestData["order"][0].value("column", 0); + orderAsc = requestData["order"][0].value("dir", "asc") == "asc"; + } + + std::string tableData = Database::Get()->GetPropertiesTable(start, length, search, orderColumn, orderAsc); + + nlohmann::json response = nlohmann::json::parse(tableData); + response["draw"] = draw; + + reply.status = eHTTPStatusCode::OK; + reply.message = response.dump(); + reply.contentType = eContentType::APPLICATION_JSON; + } catch (const nlohmann::json::exception& jsonEx) { + LOG("JSON error in /api/tables/properties: %s", jsonEx.what()); + reply.status = eHTTPStatusCode::BAD_REQUEST; + reply.message = "{\"error\":\"Invalid JSON\"}"; + reply.contentType = eContentType::APPLICATION_JSON; + } catch (std::exception& ex) { + LOG("Error in /api/tables/properties: %s", ex.what()); + reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR; + reply.message = "{\"error\":\"Database error\"}"; + reply.contentType = eContentType::APPLICATION_JSON; + } + } + }); + + // POST /api/tables/bug_reports - Get bug reports table data (DataTables.js format) + Game::web.RegisterHTTPRoute({ + .path = "/api/tables/bug_reports", + .method = eHTTPMethod::POST, + .middleware = { std::make_shared(0) }, + .handle = [](HTTPReply& reply, const HTTPContext& context) { + try { // Only admins (GM > 0) can access table data + if (context.gmLevel == 0) { + reply.status = eHTTPStatusCode::FORBIDDEN; + reply.message = "{\"error\":\"Forbidden - Admin access required\"}"; + reply.contentType = eContentType::APPLICATION_JSON; + return; + } + nlohmann::json requestData = nlohmann::json::parse(context.body); + + uint32_t draw = requestData.value("draw", 1); + uint32_t start = requestData.value("start", 0); + uint32_t length = requestData.value("length", 10); + + std::string search = ""; + if (requestData.contains("search")) { + if (requestData["search"].is_string()) { + search = requestData["search"].get(); + } else if (requestData["search"].is_object() && requestData["search"].contains("value")) { + search = requestData["search"]["value"].get(); + } + } + + uint32_t orderColumn = 0; + bool orderAsc = true; + + if (requestData.contains("order") && requestData["order"].is_array() && requestData["order"].size() > 0) { + orderColumn = requestData["order"][0].value("column", 0); + orderAsc = requestData["order"][0].value("dir", "asc") == "asc"; + } + + std::string tableData = Database::Get()->GetBugReportsTable(start, length, search, orderColumn, orderAsc); + + nlohmann::json response = nlohmann::json::parse(tableData); + response["draw"] = draw; + + reply.status = eHTTPStatusCode::OK; + reply.message = response.dump(); + reply.contentType = eContentType::APPLICATION_JSON; + } catch (const nlohmann::json::exception& jsonEx) { + LOG("JSON error in /api/tables/bug_reports: %s", jsonEx.what()); + reply.status = eHTTPStatusCode::BAD_REQUEST; + reply.message = "{\"error\":\"Invalid JSON\"}"; + reply.contentType = eContentType::APPLICATION_JSON; + } catch (std::exception& ex) { + LOG("Error in /api/tables/bug_reports: %s", ex.what()); + reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR; + reply.message = "{\"error\":\"Database error\"}"; + reply.contentType = eContentType::APPLICATION_JSON; + } + } + }); + + // GET /api/accounts/:id - Get single account by ID + Game::web.RegisterHTTPRoute({ + .path = "/api/accounts/:id", + .method = eHTTPMethod::GET, + .middleware = { std::make_shared(0) }, + .handle = [](HTTPReply& reply, const HTTPContext& context) { + try { + // Extract account ID from URL path + const std::string path = context.path; + size_t lastSlash = path.rfind('/'); + if (lastSlash == std::string::npos) { + reply.status = eHTTPStatusCode::BAD_REQUEST; + reply.message = "{\"error\":\"Invalid account ID\"}"; + reply.contentType = eContentType::APPLICATION_JSON; + return; + } + + std::string idStr = path.substr(lastSlash + 1); + uint32_t accountId = 0; + try { + accountId = std::stoul(idStr); + } catch (...) { + reply.status = eHTTPStatusCode::BAD_REQUEST; + reply.message = "{\"error\":\"Invalid account ID\"}"; + reply.contentType = eContentType::APPLICATION_JSON; + return; + } + + // Permission check: GM 0 can only view own account, GM > 0 can view any account + if (context.gmLevel == 0) { + // Regular user - get their own account ID + auto currentUserInfo = Database::Get()->GetAccountInfo(context.authenticatedUser); + if (!currentUserInfo.has_value() || currentUserInfo->id != accountId) { + reply.status = eHTTPStatusCode::FORBIDDEN; + reply.message = "{\"error\":\"Forbidden - You do not have permission to view this account\"}"; + reply.contentType = eContentType::APPLICATION_JSON; + return; + } + } + + // Get account data + nlohmann::json response = Database::Get()->GetAccountById(accountId); + reply.status = eHTTPStatusCode::OK; + reply.message = response.dump(); + reply.contentType = eContentType::APPLICATION_JSON; + } catch (const nlohmann::json::exception& jsonEx) { + LOG("JSON error in /api/accounts/:id: %s", jsonEx.what()); + reply.status = eHTTPStatusCode::BAD_REQUEST; + reply.message = "{\"error\":\"Invalid JSON\"}"; + reply.contentType = eContentType::APPLICATION_JSON; + } catch (std::exception& ex) { + LOG("Error in /api/accounts/:id: %s", ex.what()); + reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR; + reply.message = "{\"error\":\"Database error\"}"; + reply.contentType = eContentType::APPLICATION_JSON; + } } }); } + diff --git a/dDashboardServer/routes/CMakeLists.txt b/dDashboardServer/routes/CMakeLists.txt new file mode 100644 index 00000000..98d8b14a --- /dev/null +++ b/dDashboardServer/routes/CMakeLists.txt @@ -0,0 +1,30 @@ +set(DASHBOARDROUTES_SOURCES + "APIRoutes.cpp" + "StaticRoutes.cpp" + "DashboardRoutes.cpp" + "WSRoutes.cpp" + "AuthRoutes.cpp" +) + +add_library(DashboardRoutes STATIC ${DASHBOARDROUTES_SOURCES}) + +target_include_directories(DashboardRoutes PRIVATE + "${PROJECT_SOURCE_DIR}/dCommon" + "${PROJECT_SOURCE_DIR}/dCommon/dClient" + "${PROJECT_SOURCE_DIR}/dCommon/dEnums" + "${PROJECT_SOURCE_DIR}/dDatabase" + "${PROJECT_SOURCE_DIR}/dDatabase/CDClientDatabase" + "${PROJECT_SOURCE_DIR}/dDatabase/CDClientDatabase/CDClientTables" + "${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase" + "${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase/ITables" + "${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase/MySQL" + "${PROJECT_SOURCE_DIR}/dNet" + "${PROJECT_SOURCE_DIR}/dWeb" + "${PROJECT_SOURCE_DIR}/dServer" + "${PROJECT_SOURCE_DIR}/thirdparty" + "${PROJECT_SOURCE_DIR}/thirdparty/nlohmann" + "${PROJECT_SOURCE_DIR}/dDashboardServer/auth" + "${PROJECT_SOURCE_DIR}/dDashboardServer/routes" +) + +target_link_libraries(DashboardRoutes PRIVATE ${COMMON_LIBRARIES} dWeb dServer) diff --git a/dDashboardServer/routes/DashboardRoutes.cpp b/dDashboardServer/routes/DashboardRoutes.cpp index 720e5b7b..92b84069 100644 --- a/dDashboardServer/routes/DashboardRoutes.cpp +++ b/dDashboardServer/routes/DashboardRoutes.cpp @@ -25,35 +25,16 @@ void RegisterDashboardRoutes() { env.set_lstrip_blocks(true); // Prepare data for template - nlohmann::json data; - // Get username from auth context - data["username"] = context.authenticatedUser; - data["gmLevel"] = context.gmLevel; + nlohmann::json data = context.GetUserDataJson(); - // Server status (placeholder data - will be updated with real data from master) - data["auth"]["online"] = ServerState::g_AuthStatus.online; - data["auth"]["players"] = ServerState::g_AuthStatus.players; - data["chat"]["online"] = ServerState::g_ChatStatus.online; - data["chat"]["players"] = ServerState::g_ChatStatus.players; - - // World instances - std::lock_guard lock(ServerState::g_StatusMutex); - data["worlds"] = nlohmann::json::array(); - for (const auto& world : ServerState::g_WorldInstances) { - data["worlds"].push_back({ - {"mapID", world.mapID}, - {"instanceID", world.instanceID}, - {"cloneID", world.cloneID}, - {"players", world.players}, - {"isPrivate", world.isPrivate} - }); - } + // Server status - merge with server state + nlohmann::json serverState = ServerState::GetServerStateJson(); + data.merge_patch(serverState); // Statistics - const uint32_t accountCount = Database::Get()->GetAccountCount(); data["stats"]["onlinePlayers"] = 0; // TODO: Get from server communication - data["stats"]["totalAccounts"] = accountCount; - data["stats"]["totalCharacters"] = 0; // TODO: Add GetCharacterCount to database interface + data["stats"]["totalAccounts"] = Database::Get()->GetAccountCount(); + data["stats"]["totalCharacters"] = Database::Get()->GetCharacterCount(); // Render template const std::string html = env.render_file("index.jinja2", data); @@ -82,9 +63,8 @@ void RegisterDashboardRoutes() { env.set_trim_blocks(true); env.set_lstrip_blocks(true); - // Render template with empty username - nlohmann::json data; - data["username"] = ""; + // Render template with empty user data (not authenticated) + nlohmann::json data = context.GetUserDataJson(); const std::string html = env.render_file("login.jinja2", data); reply.status = eHTTPStatusCode::OK; @@ -98,4 +78,214 @@ void RegisterDashboardRoutes() { } } }); + + // GET /accounts/:id - View single account + Game::web.RegisterHTTPRoute({ + .path = "/accounts/:id", + .method = eHTTPMethod::GET, + .middleware = { std::make_shared(0) }, + .handle = [](HTTPReply& reply, const HTTPContext& context) { + try { + // Extract account ID from URL path + const std::string path = context.path; + size_t lastSlash = path.rfind('/'); + if (lastSlash == std::string::npos) { + reply.status = eHTTPStatusCode::NOT_FOUND; + reply.message = "

404 - Account not found

"; + reply.contentType = eContentType::TEXT_HTML; + return; + } + + std::string idStr = path.substr(lastSlash + 1); + uint32_t accountId = 0; + try { + accountId = std::stoul(idStr); + } catch (...) { + reply.status = eHTTPStatusCode::NOT_FOUND; + reply.message = "

404 - Invalid account ID

"; + reply.contentType = eContentType::TEXT_HTML; + return; + } + + // Permission check: GM 0 can only view own account, GM > 0 can view any account + if (context.gmLevel == 0) { + LOG("Regular user '%s' (GM level 0) is trying to access account ID %u", context.authenticatedUser.c_str(), accountId); + // Regular user - get their own account ID + auto currentUserInfo = Database::Get()->GetAccountInfo(context.authenticatedUser); + if (!currentUserInfo.has_value() || currentUserInfo->id != accountId) { + LOG("Permission denied: user '%s' cannot access account ID %u", context.authenticatedUser.c_str(), accountId); + reply.status = eHTTPStatusCode::FORBIDDEN; + reply.message = "

403 - Forbidden

You do not have permission to view this account.

"; + reply.contentType = eContentType::TEXT_HTML; + return; + } + } + + // Get account data from API + nlohmann::json account = Database::Get()->GetAccountById(accountId); + + // Check if account was found + if (account.contains("error")) { + reply.status = eHTTPStatusCode::NOT_FOUND; + reply.message = "

404 - Account not found

"; + reply.contentType = eContentType::TEXT_HTML; + return; + } + // Initialize inja environment + inja::Environment env{"dDashboardServer/templates/"}; + env.set_trim_blocks(true); + env.set_lstrip_blocks(true); + + // Prepare data for template + nlohmann::json data = context.GetUserDataJson(); + data["account"] = account; + + // Render template + const std::string html = env.render_file("account-view.jinja2", data); + + reply.status = eHTTPStatusCode::OK; + reply.message = html; + reply.contentType = eContentType::TEXT_HTML; + } catch (const std::exception& ex) { + LOG("Error rendering account view template: %s", ex.what()); + reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR; + reply.message = "

500 - Server Error

"; + reply.contentType = eContentType::TEXT_HTML; + } + } + }); + + // GET /accounts - Accounts management page + Game::web.RegisterHTTPRoute({ + .path = "/accounts", + .method = eHTTPMethod::GET, + .middleware = { std::make_shared(1) }, + .handle = [](HTTPReply& reply, const HTTPContext& context) { + try { + // Initialize inja environment + inja::Environment env{"dDashboardServer/templates/"}; + env.set_trim_blocks(true); + env.set_lstrip_blocks(true); + + // Prepare data for template + nlohmann::json data = context.GetUserDataJson(); + + // Render template + const std::string html = env.render_file("accounts.jinja2", data); + + reply.status = eHTTPStatusCode::OK; + reply.message = html; + reply.contentType = eContentType::TEXT_HTML; + } catch (const std::exception& ex) { + LOG("Error rendering accounts template: %s", ex.what()); + reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR; + reply.message = "{\"error\":\"Failed to render accounts page\"}"; + reply.contentType = eContentType::APPLICATION_JSON; + } + } + }); + + // GET /characters - Characters management page + Game::web.RegisterHTTPRoute({ + .path = "/characters", + .method = eHTTPMethod::GET, + .middleware = { std::make_shared(1) }, + .handle = [](HTTPReply& reply, const HTTPContext& context) { + try { + inja::Environment env{"dDashboardServer/templates/"}; + env.set_trim_blocks(true); + env.set_lstrip_blocks(true); + + nlohmann::json data = context.GetUserDataJson(); + const std::string html = env.render_file("characters.jinja2", data); + + reply.status = eHTTPStatusCode::OK; + reply.message = html; + reply.contentType = eContentType::TEXT_HTML; + } catch (const std::exception& ex) { + LOG("Error rendering characters template: %s", ex.what()); + reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR; + reply.message = "{\"error\":\"Failed to render characters page\"}"; + reply.contentType = eContentType::APPLICATION_JSON; + } + } + }); + + // GET /play_keys - Play keys management page + Game::web.RegisterHTTPRoute({ + .path = "/play_keys", + .method = eHTTPMethod::GET, + .middleware = { std::make_shared(1) }, + .handle = [](HTTPReply& reply, const HTTPContext& context) { + try { + inja::Environment env{"dDashboardServer/templates/"}; + env.set_trim_blocks(true); + env.set_lstrip_blocks(true); + + nlohmann::json data = context.GetUserDataJson(); + const std::string html = env.render_file("play_keys.jinja2", data); + + reply.status = eHTTPStatusCode::OK; + reply.message = html; + reply.contentType = eContentType::TEXT_HTML; + } catch (const std::exception& ex) { + LOG("Error rendering play_keys template: %s", ex.what()); + reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR; + reply.message = "{\"error\":\"Failed to render play_keys page\"}"; + reply.contentType = eContentType::APPLICATION_JSON; + } + } + }); + + // GET /properties - Properties management page + Game::web.RegisterHTTPRoute({ + .path = "/properties", + .method = eHTTPMethod::GET, + .middleware = { std::make_shared(1) }, + .handle = [](HTTPReply& reply, const HTTPContext& context) { + try { + inja::Environment env{"dDashboardServer/templates/"}; + env.set_trim_blocks(true); + env.set_lstrip_blocks(true); + + nlohmann::json data = context.GetUserDataJson(); + const std::string html = env.render_file("properties.jinja2", data); + + reply.status = eHTTPStatusCode::OK; + reply.message = html; + reply.contentType = eContentType::TEXT_HTML; + } catch (const std::exception& ex) { + LOG("Error rendering properties template: %s", ex.what()); + reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR; + reply.message = "{\"error\":\"Failed to render properties page\"}"; + reply.contentType = eContentType::APPLICATION_JSON; + } + } + }); + + // GET /bug_reports - Bug reports management page + Game::web.RegisterHTTPRoute({ + .path = "/bug_reports", + .method = eHTTPMethod::GET, + .middleware = { std::make_shared(1) }, + .handle = [](HTTPReply& reply, const HTTPContext& context) { + try { + inja::Environment env{"dDashboardServer/templates/"}; + env.set_trim_blocks(true); + env.set_lstrip_blocks(true); + + nlohmann::json data = context.GetUserDataJson(); + const std::string html = env.render_file("bug_reports.jinja2", data); + + reply.status = eHTTPStatusCode::OK; + reply.message = html; + reply.contentType = eContentType::TEXT_HTML; + } catch (const std::exception& ex) { + LOG("Error rendering bug_reports template: %s", ex.what()); + reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR; + reply.message = "{\"error\":\"Failed to render bug_reports page\"}"; + reply.contentType = eContentType::APPLICATION_JSON; + } + } + }); } diff --git a/dDashboardServer/routes/DashboardRoutes.h b/dDashboardServer/routes/DashboardRoutes.h index 52064955..8eebbac2 100644 --- a/dDashboardServer/routes/DashboardRoutes.h +++ b/dDashboardServer/routes/DashboardRoutes.h @@ -1,3 +1,7 @@ #pragma once +#include "json.hpp" + +class HTTPContext; + void RegisterDashboardRoutes(); diff --git a/dDashboardServer/routes/ServerState.h b/dDashboardServer/routes/ServerState.h index 0b2592f8..1deead74 100644 --- a/dDashboardServer/routes/ServerState.h +++ b/dDashboardServer/routes/ServerState.h @@ -1,10 +1,10 @@ #pragma once #include -#include #include #include #include +#include "json.hpp" struct ServerStatus { bool online{false}; @@ -27,5 +27,26 @@ namespace ServerState { extern ServerStatus g_AuthStatus; extern ServerStatus g_ChatStatus; extern std::vector g_WorldInstances; - extern std::mutex g_StatusMutex; + + // Helper function to get all server state as JSON + inline nlohmann::json GetServerStateJson() { + nlohmann::json data; + data["auth"]["online"] = g_AuthStatus.online; + data["auth"]["players"] = g_AuthStatus.players; + data["chat"]["online"] = g_ChatStatus.online; + data["chat"]["players"] = g_ChatStatus.players; + + data["worlds"] = nlohmann::json::array(); + for (const auto& world : g_WorldInstances) { + data["worlds"].push_back({ + {"mapID", world.mapID}, + {"instanceID", world.instanceID}, + {"cloneID", world.cloneID}, + {"players", world.players}, + {"isPrivate", world.isPrivate} + }); + } + + return data; + } } diff --git a/dDashboardServer/routes/StaticRoutes.cpp b/dDashboardServer/routes/StaticRoutes.cpp index 9722b3e9..d2d42cba 100644 --- a/dDashboardServer/routes/StaticRoutes.cpp +++ b/dDashboardServer/routes/StaticRoutes.cpp @@ -65,8 +65,4 @@ void RegisterStaticRoutes() { // Serve JavaScript files ServeStaticFile("/js/dashboard.js", "dDashboardServer/static/js/dashboard.js"); ServeStaticFile("/js/login.js", "dDashboardServer/static/js/login.js"); - - // Also serve from /static/ paths for backwards compatibility - ServeStaticFile("/static/css/dashboard.css", "dDashboardServer/static/css/dashboard.css"); - ServeStaticFile("/static/js/dashboard.js", "dDashboardServer/static/js/dashboard.js"); } diff --git a/dDashboardServer/routes/WSRoutes.cpp b/dDashboardServer/routes/WSRoutes.cpp index b20c40f2..673a5872 100644 --- a/dDashboardServer/routes/WSRoutes.cpp +++ b/dDashboardServer/routes/WSRoutes.cpp @@ -18,37 +18,14 @@ void RegisterWSRoutes() { } void BroadcastDashboardUpdate() { - std::lock_guard lock(ServerState::g_StatusMutex); - - nlohmann::json data = { - {"auth", { - {"online", ServerState::g_AuthStatus.online}, - {"players", ServerState::g_AuthStatus.players}, - {"version", ServerState::g_AuthStatus.version} - }}, - {"chat", { - {"online", ServerState::g_ChatStatus.online}, - {"players", ServerState::g_ChatStatus.players} - }}, - {"worlds", nlohmann::json::array()} - }; - - for (const auto& world : ServerState::g_WorldInstances) { - data["worlds"].push_back({ - {"mapID", world.mapID}, - {"instanceID", world.instanceID}, - {"cloneID", world.cloneID}, - {"players", world.players}, - {"isPrivate", world.isPrivate} - }); - } + // Get server state data (auth, chat, worlds) - mutex is acquired internally + nlohmann::json data = ServerState::GetServerStateJson(); // Add statistics try { - const uint32_t accountCount = Database::Get()->GetAccountCount(); data["stats"]["onlinePlayers"] = 0; // TODO: Get from server communication - data["stats"]["totalAccounts"] = accountCount; - data["stats"]["totalCharacters"] = 0; // TODO: Add GetCharacterCount to database interface + data["stats"]["totalAccounts"] = Database::Get()->GetAccountCount(); + data["stats"]["totalCharacters"] = Database::Get()->GetCharacterCount(); } catch (const std::exception& ex) { LOG_DEBUG("Error getting stats: %s", ex.what()); } diff --git a/dDashboardServer/static/css/dashboard.css b/dDashboardServer/static/css/dashboard.css index 450d67cc..ff397ebc 100644 --- a/dDashboardServer/static/css/dashboard.css +++ b/dDashboardServer/static/css/dashboard.css @@ -2,11 +2,15 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; - background-color: #f8f9fa; margin: 0; padding: 0; } +body.d-flex.bg-dark.text-white { + background-color: #0d0d0d; + color: #fff; +} + /* Sidebar adjustments */ .navbar.flex-column { box-shadow: 0.125rem 0 0.25rem rgba(0, 0, 0, 0.075); @@ -91,21 +95,6 @@ main { margin-bottom: 20px; } -.card { - background: white; - padding: 25px; - border-radius: 10px; - box-shadow: 0 10px 30px rgba(0,0,0,0.2); -} - -.card h2 { - color: #333; - margin-bottom: 15px; - font-size: 1.5em; - border-bottom: 2px solid #667eea; - padding-bottom: 10px; -} - .stat { display: flex; justify-content: space-between; @@ -175,3 +164,332 @@ main { padding: 20px; color: #666; } + +/* Dark theme for data tables and containers */ + +/* Container margin for sidebar layout */ +.account-view-container, +.accounts-container, +.characters-container, +.play-keys-container, +.properties-container, +.bug-reports-container { + margin-left: 280px; + padding: 20px; +} + +/* Table card styling */ +.table-card { + background-color: #1a1a1a; + border: 1px solid #333; + border-radius: 0.5rem; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); +} + +.table-header { + background-color: #222; + padding: 1.5rem; + border-bottom: 1px solid #333; + border-radius: 0.5rem 0.5rem 0 0; +} + +.table-header h2 { + margin: 0; + color: #fff; +} + +.table-body { + padding: 1.5rem; +} + +/* Bootstrap card overrides for dark theme */ +.card { + background-color: #1a1a1a; + border: 1px solid #333; + border-radius: 0.5rem; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + margin-bottom: 1.5rem; +} + +.card-header { + background-color: #222; + padding: 1.5rem; + border-bottom: 1px solid #333; + border-radius: 0.5rem 0.5rem 0 0; +} + +.card-header h2 { + margin: 0; + color: #fff; +} + +.card-body { + padding: 1.5rem; +} + +/* Table styling */ +.table-dark { + color: #fff; +} + +.table-dark thead { + background-color: #2a2a2a; +} + +.table-dark thead th { + border-bottom: 2px solid #444; + color: #aaa; + font-weight: 600; + text-transform: uppercase; + font-size: 0.875rem; + padding: 1rem; +} + +.table-dark tbody td { + padding: 0.875rem 1rem; + border-bottom: 1px solid #333; + vertical-align: middle; +} + +.table-dark tbody tr:hover { + background-color: #252525; +} + +/* DataTables customization */ +.dataTables_wrapper { + color: #fff; +} + +.dataTables_wrapper .dataTables_filter input { + background-color: #2a2a2a; + color: #fff; + border: 1px solid #444; + border-radius: 0.25rem; + padding: 0.4rem 0.6rem; +} + +.dataTables_wrapper .dataTables_filter input::placeholder { + color: #888; +} + +.dataTables_wrapper .dataTables_info { + color: #aaa; + padding: 1rem; +} + +.dataTables_wrapper .dataTables_paginate .paginate_button { + background: #2a2a2a; + color: #fff; + border: 1px solid #444; + margin: 0 2px; + padding: 0.4rem 0.8rem; + border-radius: 0.25rem; + cursor: pointer; + transition: all 0.2s; +} + +.dataTables_wrapper .dataTables_paginate .paginate_button:hover { + background: #3a3a3a; + border: 1px solid #555; +} + +.dataTables_wrapper .dataTables_paginate .paginate_button.current { + background: #0d6efd; + border: 1px solid #0d6efd; +} + +.dataTables_wrapper .dataTables_paginate .paginate_button.disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.dataTables_wrapper .dataTables_length select { + background-color: #2a2a2a; + color: #fff; + border: 1px solid #444; + padding: 0.4rem 0.6rem; + border-radius: 0.25rem; +} + +/* Detail grid layout */ +.detail-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 1.5rem; +} + +.detail-item { + background-color: #0a0a0a; + padding: 1rem; + border-radius: 0.25rem; + border-left: 3px solid #0d6efd; +} + +.detail-label { + color: #999; + font-size: 0.875rem; + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 0.5rem; +} + +.detail-value { + color: #fff; + font-size: 1rem; + font-weight: 500; +} + +/* Badge styling */ +.badge { + display: inline-block; + padding: 0.35rem 0.6rem; + font-size: 0.8rem; + font-weight: 500; + border-radius: 0.25rem; +} + +.badge-active { + background-color: #28a745; + color: #fff; +} + +.badge-inactive { + background-color: #6c757d; + color: #fff; +} + +.badge-banned { + background-color: #dc3545; + color: #fff; +} + +.badge-locked { + background-color: #ffc107; + color: #000; +} + +.badge-gm { + background-color: #17a2b8; + color: #fff; +} + +.badge-approved { + background-color: #28a745; + color: #fff; +} + +.badge-pending { + background-color: #ffc107; + color: #000; +} + +/* Button styling */ +.button-group { + display: flex; + gap: 0.5rem; + margin-top: 1.5rem; +} + +.btn { + padding: 0.5rem 1rem; + border-radius: 0.25rem; + border: none; + cursor: pointer; + font-weight: 500; + transition: all 0.2s; +} + +.btn-sm { + padding: 0.25rem 0.5rem; + font-size: 0.75rem; +} + +.btn-primary { + background-color: #0d6efd; + color: #fff; +} + +.btn-primary:hover { + background-color: #0b5ed7; +} + +.btn-secondary { + background-color: #6c757d; + color: #fff; +} + +.btn-secondary:hover { + background-color: #5c636a; +} + +.btn-danger { + background-color: #dc3545; + color: #fff; +} + +.btn-danger:hover { + background-color: #c82333; +} + +.btn-info { + background-color: #17a2b8; + color: #fff; +} + +.btn-info:hover { + background-color: #138496; +} + +.btn-warning { + background-color: #ffc107; + color: #000; +} + +.btn-warning:hover { + background-color: #e0a800; +} + +/* Action buttons */ +.account-actions { + display: flex; + gap: 0.5rem; +} + +/* Utility classes */ +.text-muted { + color: #999 !important; +} + +.back-link { + color: #0d6efd; + text-decoration: none; + margin-bottom: 1rem; + display: inline-block; +} + +.back-link:hover { + text-decoration: underline; +} + +.report-preview { + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.search-section { + margin-bottom: 1.5rem; +} + +.search-input { + background-color: #2a2a2a; + border: 1px solid #444; + color: #fff; + padding: 0.5rem; + border-radius: 0.25rem; +} + +.search-input::placeholder { + color: #888; +} \ No newline at end of file diff --git a/dDashboardServer/templates/account-view.jinja2 b/dDashboardServer/templates/account-view.jinja2 new file mode 100644 index 00000000..3f926413 --- /dev/null +++ b/dDashboardServer/templates/account-view.jinja2 @@ -0,0 +1,137 @@ +{% extends "base.jinja2" %} + +{% block title %}Account - DarkflameServer{% endblock %} + +{% block css %}{% endblock %} + +{% block content %} + +{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/dDashboardServer/templates/accounts.jinja2 b/dDashboardServer/templates/accounts.jinja2 new file mode 100644 index 00000000..3c8b9620 --- /dev/null +++ b/dDashboardServer/templates/accounts.jinja2 @@ -0,0 +1,133 @@ +{% extends "base.jinja2" %} + +{% block title %}Accounts - DarkflameServer{% endblock %} + +{% block css %}{% endblock %} + +{% block content %} +
+
+
+
+

Accounts

+

View and manage user accounts

+
+
+
+ + + + + + + + + + + + + + + + +
IDUsernameBannedLockedGM LevelMute ExpiresCreatedActions
+
+
+
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/dDashboardServer/templates/base.jinja2 b/dDashboardServer/templates/base.jinja2 index 17c96a08..66715167 100644 --- a/dDashboardServer/templates/base.jinja2 +++ b/dDashboardServer/templates/base.jinja2 @@ -4,8 +4,8 @@ {% block title %}DarkflameServer{% endblock %} - - + + {% block css %}{% endblock %} @@ -26,9 +26,8 @@ {% endblock %} - - - + + {% block scripts %}{% endblock %} diff --git a/dDashboardServer/templates/bug_reports.jinja2 b/dDashboardServer/templates/bug_reports.jinja2 new file mode 100644 index 00000000..14fbc253 --- /dev/null +++ b/dDashboardServer/templates/bug_reports.jinja2 @@ -0,0 +1,97 @@ +{% extends "base.jinja2" %} + +{% block title %}Bug Reports - DarkflameServer{% endblock %} + +{% block css %}{% endblock %} + +{% block content %} +
+
+
+

Bug Reports

+

View and manage bug reports from players

+
+
+ + + + + + + + + + + + + + +
IDPlayerClient VersionSubmittedReport PreviewActions
+
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/dDashboardServer/templates/characters.jinja2 b/dDashboardServer/templates/characters.jinja2 new file mode 100644 index 00000000..01a91846 --- /dev/null +++ b/dDashboardServer/templates/characters.jinja2 @@ -0,0 +1,90 @@ +{% extends "base.jinja2" %} + +{% block title %}Characters - DarkflameServer{% endblock %} + +{% block css %}{% endblock %} + +{% block content %} +
+
+
+

Characters

+

View and manage player characters

+
+
+ + + + + + + + + + + + + +
IDNameAccountLast LoginActions
+
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/dDashboardServer/templates/header.jinja2 b/dDashboardServer/templates/header.jinja2 index 4240ad99..afee40ba 100644 --- a/dDashboardServer/templates/header.jinja2 +++ b/dDashboardServer/templates/header.jinja2 @@ -1,27 +1,27 @@ {# Navigation #}