mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-07-13 19:01:56 +00:00
Compare commits
9 Commits
chore--mou
...
web-dashbo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e006a0e35 | ||
|
|
35337291fa | ||
|
|
5d523a1e7b | ||
|
|
5745742c91 | ||
|
|
62f58f5307 | ||
|
|
83707e2210 | ||
|
|
f658da19a3 | ||
|
|
8372202d8f | ||
|
|
f1847d1f20 |
@@ -115,6 +115,8 @@ set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
|
||||
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
|
||||
|
||||
find_package(MariaDB)
|
||||
find_package(OpenSSL REQUIRED)
|
||||
|
||||
|
||||
# Create a /resServer directory
|
||||
make_directory(${CMAKE_BINARY_DIR}/resServer)
|
||||
@@ -131,7 +133,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" "worldconfig.ini" "masterconfig.ini" "dashboardconfig.ini" "blocklist.dcf")
|
||||
message(STATUS "Checking resource file integrity")
|
||||
|
||||
include(Utils)
|
||||
@@ -327,6 +329,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(
|
||||
|
||||
@@ -19,23 +19,24 @@
|
||||
#include "eGameMasterLevel.h"
|
||||
#include "dChatFilter.h"
|
||||
#include "TeamContainer.h"
|
||||
#include "HTTPContext.h"
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
void HandleHTTPPlayersRequest(HTTPReply& reply, std::string body) {
|
||||
void HandleHTTPPlayersRequest(HTTPReply& reply, const HTTPContext& context) {
|
||||
const json data = Game::playerContainer;
|
||||
reply.status = data.empty() ? eHTTPStatusCode::NO_CONTENT : eHTTPStatusCode::OK;
|
||||
reply.message = data.empty() ? "{\"error\":\"No Players Online\"}" : data.dump();
|
||||
}
|
||||
|
||||
void HandleHTTPTeamsRequest(HTTPReply& reply, std::string body) {
|
||||
void HandleHTTPTeamsRequest(HTTPReply& reply, const HTTPContext& context) {
|
||||
const json data = TeamContainer::GetTeamContainer();
|
||||
reply.status = data.empty() ? eHTTPStatusCode::NO_CONTENT : eHTTPStatusCode::OK;
|
||||
reply.message = data.empty() ? "{\"error\":\"No Teams Online\"}" : data.dump();
|
||||
}
|
||||
|
||||
void HandleHTTPAnnounceRequest(HTTPReply& reply, std::string body) {
|
||||
auto data = GeneralUtils::TryParse<json>(body);
|
||||
void HandleHTTPAnnounceRequest(HTTPReply& reply, const HTTPContext& context) {
|
||||
auto data = GeneralUtils::TryParse<json>(context.body);
|
||||
if (!data) {
|
||||
reply.status = eHTTPStatusCode::BAD_REQUEST;
|
||||
reply.message = "{\"error\":\"Invalid JSON\"}";
|
||||
@@ -96,18 +97,21 @@ namespace ChatWeb {
|
||||
Game::web.RegisterHTTPRoute({
|
||||
.path = v1_route + "players",
|
||||
.method = eHTTPMethod::GET,
|
||||
.middleware = {},
|
||||
.handle = HandleHTTPPlayersRequest
|
||||
});
|
||||
|
||||
Game::web.RegisterHTTPRoute({
|
||||
.path = v1_route + "teams",
|
||||
.method = eHTTPMethod::GET,
|
||||
.middleware = {},
|
||||
.handle = HandleHTTPTeamsRequest
|
||||
});
|
||||
|
||||
Game::web.RegisterHTTPRoute({
|
||||
.path = v1_route + "announce",
|
||||
.method = eHTTPMethod::POST,
|
||||
.middleware = {},
|
||||
.handle = HandleHTTPAnnounceRequest
|
||||
});
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include "dCommonVars.h"
|
||||
#include "Logger.h"
|
||||
#include "Game.h"
|
||||
|
||||
#include <type_traits>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
@@ -368,9 +367,37 @@ public:
|
||||
}
|
||||
|
||||
template<typename AmfType = AMFArrayValue>
|
||||
AmfType& PushDebug(const std::string_view name) {
|
||||
AmfType& PushDebug(const std::string_view name, const std::string& objectType = "", const uint32_t warningLevel = 0) {
|
||||
size_t i = 0;
|
||||
for (; i < m_Dense.size(); i++) {
|
||||
const auto& cast = dynamic_cast<AMFArrayValue*>(m_Dense[i].get());
|
||||
if (!cast) continue;
|
||||
|
||||
const auto& nameValue = cast->Get<std::string>("name");
|
||||
if (!nameValue || nameValue->GetValue() != name) continue;
|
||||
|
||||
if (!objectType.empty()) {
|
||||
cast->Insert<std::string>("type", objectType);
|
||||
}
|
||||
|
||||
if (warningLevel != 0) {
|
||||
cast->Insert<double>("warningLevel", warningLevel);
|
||||
}
|
||||
|
||||
// found a duplicate, return this instead
|
||||
auto valueCast = dynamic_cast<AmfType*>(cast->Get("value"));
|
||||
if (valueCast) return *valueCast;
|
||||
}
|
||||
|
||||
auto* value = PushArray();
|
||||
value->Insert("name", name.data());
|
||||
value->Insert<std::string>("name", name.data());
|
||||
if (!objectType.empty()) {
|
||||
value->Insert<std::string>("type", objectType);
|
||||
}
|
||||
|
||||
if (warningLevel != 0) {
|
||||
value->Insert<double>("warningLevel", warningLevel);
|
||||
}
|
||||
return value->Insert<AmfType>("value", std::make_unique<AmfType>());
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ namespace MessageType {
|
||||
AFFIRM_TRANSFER_REQUEST,
|
||||
AFFIRM_TRANSFER_RESPONSE,
|
||||
|
||||
NEW_SESSION_ALERT
|
||||
NEW_SESSION_ALERT,
|
||||
|
||||
REQUEST_SERVER_LIST
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@ enum class ServiceType : uint16_t {
|
||||
COMMON = 0,
|
||||
AUTH,
|
||||
CHAT,
|
||||
WORLD = 4,
|
||||
DASHBOARD,
|
||||
WORLD,
|
||||
CLIENT,
|
||||
MASTER,
|
||||
UNKNOWN
|
||||
|
||||
58
dDashboardServer/CMakeLists.txt
Normal file
58
dDashboardServer/CMakeLists.txt
Normal file
@@ -0,0 +1,58 @@
|
||||
set(DDASHBOARDSERVER_SOURCES
|
||||
"DashboardServer.cpp"
|
||||
)
|
||||
|
||||
add_subdirectory(routes)
|
||||
add_subdirectory(auth)
|
||||
|
||||
add_executable(DashboardServer ${DDASHBOARDSERVER_SOURCES})
|
||||
|
||||
target_include_directories(DashboardServer 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"
|
||||
"${PROJECT_SOURCE_DIR}/dDashboardServer/auth"
|
||||
"${PROJECT_SOURCE_DIR}/dDashboardServer/routes"
|
||||
)
|
||||
|
||||
target_link_libraries(DashboardServer ${COMMON_LIBRARIES} dWeb dServer bcrypt OpenSSL::Crypto DashboardRoutes DashboardAuth)
|
||||
|
||||
|
||||
# Copy static files and templates to build directory (always copy)
|
||||
add_custom_command(TARGET DashboardServer POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E remove_directory
|
||||
${CMAKE_BINARY_DIR}/dDashboardServer/static
|
||||
COMMENT "Removing old static files"
|
||||
)
|
||||
|
||||
add_custom_command(TARGET DashboardServer POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/static
|
||||
${CMAKE_BINARY_DIR}/dDashboardServer/static
|
||||
COMMENT "Copying DashboardServer static files"
|
||||
)
|
||||
|
||||
add_custom_command(TARGET DashboardServer POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E remove_directory
|
||||
${CMAKE_BINARY_DIR}/dDashboardServer/templates
|
||||
COMMENT "Removing old templates"
|
||||
)
|
||||
|
||||
add_custom_command(TARGET DashboardServer POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/templates
|
||||
${CMAKE_BINARY_DIR}/dDashboardServer/templates
|
||||
COMMENT "Copying DashboardServer templates"
|
||||
)
|
||||
211
dDashboardServer/DashboardServer.cpp
Normal file
211
dDashboardServer/DashboardServer.cpp
Normal file
@@ -0,0 +1,211 @@
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
#include <csignal>
|
||||
#include <memory>
|
||||
|
||||
#include "CDClientDatabase.h"
|
||||
#include "CDClientManager.h"
|
||||
#include "Database.h"
|
||||
#include "dConfig.h"
|
||||
#include "Logger.h"
|
||||
#include "dServer.h"
|
||||
#include "AssetManager.h"
|
||||
#include "BinaryPathFinder.h"
|
||||
#include "ServiceType.h"
|
||||
#include "MessageType/Master.h"
|
||||
#include "Game.h"
|
||||
#include "BitStreamUtils.h"
|
||||
#include "Diagnostics.h"
|
||||
#include "Web.h"
|
||||
#include "Server.h"
|
||||
|
||||
#include "ServerState.h"
|
||||
#include "APIRoutes.h"
|
||||
#include "StaticRoutes.h"
|
||||
#include "DashboardRoutes.h"
|
||||
#include "WSRoutes.h"
|
||||
#include "AuthRoutes.h"
|
||||
#include "AuthMiddleware.h"
|
||||
#include "DashboardAuthService.h"
|
||||
#include "AuthTokenHandler.h"
|
||||
|
||||
namespace Game {
|
||||
Logger* logger = nullptr;
|
||||
dServer* server = nullptr;
|
||||
dConfig* config = nullptr;
|
||||
Game::signal_t lastSignal = 0;
|
||||
std::mt19937 randomEngine;
|
||||
}
|
||||
|
||||
// Define global server state
|
||||
namespace ServerState {
|
||||
ServerStatus g_AuthStatus{};
|
||||
ServerStatus g_ChatStatus{};
|
||||
std::vector<WorldInstanceInfo> g_WorldInstances{};
|
||||
std::mutex g_StatusMutex{};
|
||||
}
|
||||
|
||||
namespace {
|
||||
dServer* g_Server = nullptr;
|
||||
bool g_RequestedServerList = false;
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
Diagnostics::SetProduceMemoryDump(true);
|
||||
std::signal(SIGINT, Game::OnSignal);
|
||||
std::signal(SIGTERM, Game::OnSignal);
|
||||
|
||||
uint32_t maxClients = 999;
|
||||
uint32_t ourPort = 2006;
|
||||
std::string ourIP = "127.0.0.1";
|
||||
|
||||
// Read config
|
||||
Game::config = new dConfig("dashboardconfig.ini");
|
||||
|
||||
// Setup logger
|
||||
Server::SetupLogger("DashboardServer");
|
||||
if (!Game::logger) return EXIT_FAILURE;
|
||||
Game::config->LogSettings();
|
||||
|
||||
LOG("Starting Dashboard Server");
|
||||
|
||||
// Load settings
|
||||
if (Game::config->GetValue("max_clients") != "")
|
||||
maxClients = std::stoi(Game::config->GetValue("max_clients"));
|
||||
|
||||
if (Game::config->GetValue("port") != "")
|
||||
ourPort = std::atoi(Game::config->GetValue("port").c_str());
|
||||
|
||||
if (Game::config->GetValue("listen_ip") != "")
|
||||
ourIP = Game::config->GetValue("listen_ip");
|
||||
|
||||
// Connect to CDClient database
|
||||
try {
|
||||
const std::string cdclientPath = BinaryPathFinder::GetBinaryDir() / "resServer/CDServer.sqlite";
|
||||
CDClientDatabase::Connect(cdclientPath);
|
||||
} catch (std::exception& ex) {
|
||||
LOG("Failed to connect to CDClient database: %s", ex.what());
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
// Connect to the database
|
||||
try {
|
||||
Database::Connect();
|
||||
} catch (std::exception& ex) {
|
||||
LOG("Failed to connect to the database: %s", ex.what());
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
// Get master info from database
|
||||
std::string masterIP = "localhost";
|
||||
uint32_t masterPort = 1000;
|
||||
std::string masterPassword;
|
||||
auto masterInfo = Database::Get()->GetMasterInfo();
|
||||
if (masterInfo) {
|
||||
masterIP = masterInfo->ip;
|
||||
masterPort = masterInfo->port;
|
||||
masterPassword = masterInfo->password;
|
||||
}
|
||||
|
||||
// Setup network server for communicating with Master
|
||||
g_Server = new dServer(
|
||||
masterIP,
|
||||
ourPort,
|
||||
0,
|
||||
maxClients,
|
||||
false,
|
||||
false,
|
||||
Game::logger,
|
||||
masterIP,
|
||||
masterPort,
|
||||
ServiceType::DASHBOARD, // Connect as dashboard to master
|
||||
Game::config,
|
||||
&Game::lastSignal,
|
||||
masterPassword
|
||||
);
|
||||
|
||||
// Initialize web server
|
||||
if (!Game::web.Startup(ourIP, ourPort)) {
|
||||
LOG("Failed to start web server on %s:%d", ourIP.c_str(), ourPort);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
// Set up WebSocket authentication callback using consolidated handler
|
||||
Game::web.SetWSAuthCallback([](const std::string& token) -> bool {
|
||||
auto result = AuthTokenHandler::ValidateToken(token);
|
||||
return result.isValid;
|
||||
});
|
||||
|
||||
// Register global middleware
|
||||
Game::web.AddGlobalMiddleware(std::make_shared<AuthMiddleware>());
|
||||
|
||||
// Register routes in order: API, Static, Auth, WebSocket, Dashboard (dashboard MUST be last)
|
||||
RegisterAPIRoutes();
|
||||
RegisterStaticRoutes();
|
||||
RegisterAuthRoutes();
|
||||
RegisterWSRoutes();
|
||||
RegisterDashboardRoutes(); // Must be last - catches all unmatched routes
|
||||
|
||||
LOG("Dashboard Server started successfully on %s:%d", ourIP.c_str(), ourPort);
|
||||
LOG("Connected to Master Server at %s:%d", masterIP.c_str(), masterPort);
|
||||
|
||||
// Main loop
|
||||
auto lastTime = std::chrono::high_resolution_clock::now();
|
||||
auto lastBroadcast = lastTime;
|
||||
auto currentTime = lastTime;
|
||||
constexpr float deltaTime = 1.0f / 60.0f; // 60 FPS
|
||||
constexpr float broadcastInterval = 2000.0f; // Broadcast every 2 seconds
|
||||
|
||||
while (!Game::ShouldShutdown()) {
|
||||
currentTime = std::chrono::high_resolution_clock::now();
|
||||
const auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(currentTime - lastTime).count();
|
||||
const auto elapsedSinceBroadcast = std::chrono::duration_cast<std::chrono::milliseconds>(currentTime - lastBroadcast).count();
|
||||
|
||||
if (elapsed >= 1000.0f / 60.0f) {
|
||||
// // 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();
|
||||
|
||||
// Broadcast dashboard updates periodically
|
||||
if (elapsedSinceBroadcast >= broadcastInterval) {
|
||||
BroadcastDashboardUpdate();
|
||||
lastBroadcast = currentTime;
|
||||
}
|
||||
|
||||
lastTime = currentTime;
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
Database::Destroy("DashboardServer");
|
||||
delete g_Server;
|
||||
g_Server = nullptr;
|
||||
delete Game::logger;
|
||||
Game::logger = nullptr;
|
||||
delete Game::config;
|
||||
Game::config = nullptr;
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
|
||||
6
dDashboardServer/auth/AuthMiddleware.cpp
Normal file
6
dDashboardServer/auth/AuthMiddleware.cpp
Normal file
@@ -0,0 +1,6 @@
|
||||
#include "AuthMiddleware.h"
|
||||
#include "AuthTokenHandler.h"
|
||||
|
||||
bool AuthMiddleware::Process(HTTPContext& context, HTTPReply& reply) {
|
||||
return AuthTokenHandler::ProcessHTTPContext(context, reply);
|
||||
}
|
||||
28
dDashboardServer/auth/AuthMiddleware.h
Normal file
28
dDashboardServer/auth/AuthMiddleware.h
Normal file
@@ -0,0 +1,28 @@
|
||||
#ifndef __AUTHMIDDLEWARE_H__
|
||||
#define __AUTHMIDDLEWARE_H__
|
||||
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include "IHTTPMiddleware.h"
|
||||
|
||||
/**
|
||||
* AuthMiddleware: Extracts and verifies authentication tokens
|
||||
*
|
||||
* Token extraction sources (in priority order):
|
||||
* 1. Query parameter: ?token=eyJhbGc...
|
||||
* 2. Cookie: dashboardToken=...
|
||||
* 3. Authorization header: Bearer <token> or Token <token>
|
||||
*
|
||||
* Sets HTTPContext.isAuthenticated, HTTPContext.authenticatedUser,
|
||||
* and HTTPContext.gmLevel if token is valid.
|
||||
*/
|
||||
class AuthMiddleware final : public IHTTPMiddleware {
|
||||
public:
|
||||
AuthMiddleware() = default;
|
||||
~AuthMiddleware() override = default;
|
||||
|
||||
bool Process(HTTPContext& context, HTTPReply& reply) override;
|
||||
std::string GetName() const override { return "AuthMiddleware"; }
|
||||
};
|
||||
|
||||
#endif // !__AUTHMIDDLEWARE_H__
|
||||
186
dDashboardServer/auth/AuthTokenHandler.cpp
Normal file
186
dDashboardServer/auth/AuthTokenHandler.cpp
Normal file
@@ -0,0 +1,186 @@
|
||||
#include "AuthTokenHandler.h"
|
||||
#include "DashboardAuthService.h"
|
||||
#include "Game.h"
|
||||
#include "Logger.h"
|
||||
#include "HTTPContext.h"
|
||||
#include "Web.h"
|
||||
|
||||
// Helper to extract cookie value from header
|
||||
static std::string ExtractCookieValue(const std::string& cookieHeader, const std::string& cookieName) {
|
||||
std::string searchStr = cookieName + "=";
|
||||
size_t pos = cookieHeader.find(searchStr);
|
||||
|
||||
if (pos == std::string::npos) {
|
||||
return "";
|
||||
}
|
||||
|
||||
size_t valueStart = pos + searchStr.length();
|
||||
size_t valueEnd = cookieHeader.find(";", valueStart);
|
||||
|
||||
if (valueEnd == std::string::npos) {
|
||||
valueEnd = cookieHeader.length();
|
||||
}
|
||||
|
||||
std::string value = cookieHeader.substr(valueStart, valueEnd - valueStart);
|
||||
|
||||
// URL decode the value
|
||||
std::string decoded;
|
||||
for (size_t i = 0; i < value.length(); ++i) {
|
||||
if (value[i] == '%' && i + 2 < value.length()) {
|
||||
std::string hex = value.substr(i + 1, 2);
|
||||
char* endptr;
|
||||
int charCode = static_cast<int>(std::strtol(hex.c_str(), &endptr, 16));
|
||||
if (endptr - hex.c_str() == 2) {
|
||||
decoded += static_cast<char>(charCode);
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
decoded += value[i];
|
||||
}
|
||||
|
||||
return decoded;
|
||||
}
|
||||
|
||||
std::string AuthTokenHandler::ExtractTokenFromQueryString(const std::string& queryString) {
|
||||
if (queryString.empty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Parse query string to find token parameter
|
||||
// Expected format: "?token=eyJhbGc..."
|
||||
std::string tokenPrefix = "token=";
|
||||
size_t tokenPos = queryString.find(tokenPrefix);
|
||||
|
||||
if (tokenPos == std::string::npos) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Extract token value (from "token=" to next "&" or end of string)
|
||||
size_t valueStart = tokenPos + tokenPrefix.length();
|
||||
size_t valueEnd = queryString.find("&", valueStart);
|
||||
|
||||
if (valueEnd == std::string::npos) {
|
||||
valueEnd = queryString.length();
|
||||
}
|
||||
|
||||
return queryString.substr(valueStart, valueEnd - valueStart);
|
||||
}
|
||||
|
||||
std::string AuthTokenHandler::ExtractTokenFromCookieHeader(const std::string& cookieHeader) {
|
||||
if (cookieHeader.empty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Extract dashboardToken cookie value
|
||||
return ExtractCookieValue(cookieHeader, "dashboardToken");
|
||||
}
|
||||
|
||||
std::string AuthTokenHandler::ExtractTokenFromAuthHeader(const std::string& authHeader) {
|
||||
if (authHeader.empty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Check for "Bearer <token>" format
|
||||
if (authHeader.length() >= 7 && authHeader.substr(0, 7) == "Bearer ") {
|
||||
return authHeader.substr(7);
|
||||
}
|
||||
|
||||
// Check for "Token <token>" format
|
||||
if (authHeader.length() >= 6 && authHeader.substr(0, 6) == "Token ") {
|
||||
return authHeader.substr(6);
|
||||
}
|
||||
|
||||
// If no prefix, assume raw token
|
||||
return authHeader;
|
||||
}
|
||||
|
||||
std::string AuthTokenHandler::ExtractToken(
|
||||
const std::string& queryString,
|
||||
const std::string& cookieHeader,
|
||||
const std::string& authHeader
|
||||
) {
|
||||
// Try in priority order: query string, cookie, auth header
|
||||
std::string token = ExtractTokenFromQueryString(queryString);
|
||||
|
||||
if (!token.empty()) {
|
||||
return token;
|
||||
}
|
||||
|
||||
token = ExtractTokenFromCookieHeader(cookieHeader);
|
||||
|
||||
if (!token.empty()) {
|
||||
return token;
|
||||
}
|
||||
|
||||
token = ExtractTokenFromAuthHeader(authHeader);
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
AuthTokenHandler::TokenValidationResult AuthTokenHandler::ValidateToken(const std::string& token) {
|
||||
TokenValidationResult result;
|
||||
|
||||
if (token.empty()) {
|
||||
result.isValid = false;
|
||||
result.errorMessage = "No token provided";
|
||||
return result;
|
||||
}
|
||||
|
||||
// Verify JWT token
|
||||
std::string username;
|
||||
uint8_t gmLevel = 0;
|
||||
|
||||
if (!DashboardAuthService::VerifyToken(token, username, gmLevel)) {
|
||||
result.isValid = false;
|
||||
result.errorMessage = "Invalid or expired token";
|
||||
LOG_DEBUG("Token validation failed");
|
||||
return result;
|
||||
}
|
||||
|
||||
result.isValid = true;
|
||||
result.username = username;
|
||||
result.gmLevel = gmLevel;
|
||||
LOG_DEBUG("Token validated successfully for user: %s (GM Level: %d)", username.c_str(), gmLevel);
|
||||
return result;
|
||||
}
|
||||
|
||||
AuthTokenHandler::TokenValidationResult AuthTokenHandler::ExtractAndValidateToken(
|
||||
const std::string& queryString,
|
||||
const std::string& cookieHeader,
|
||||
const std::string& authHeader
|
||||
) {
|
||||
TokenValidationResult result;
|
||||
|
||||
// Extract token from any source
|
||||
std::string token = ExtractToken(queryString, cookieHeader, authHeader);
|
||||
|
||||
if (token.empty()) {
|
||||
result.isValid = false;
|
||||
result.errorMessage = "No authentication token found";
|
||||
return result;
|
||||
}
|
||||
|
||||
// Validate the token
|
||||
return ValidateToken(token);
|
||||
}
|
||||
|
||||
bool AuthTokenHandler::ProcessHTTPContext(HTTPContext& context, HTTPReply& reply) {
|
||||
// Extract and validate token from all available sources
|
||||
const std::string& queryString = context.queryString;
|
||||
const std::string& cookieHeader = context.GetHeader("Cookie");
|
||||
const std::string& authHeader = context.GetHeader("Authorization");
|
||||
|
||||
auto result = ExtractAndValidateToken(queryString, cookieHeader, authHeader);
|
||||
|
||||
if (result.isValid) {
|
||||
context.isAuthenticated = true;
|
||||
context.authenticatedUser = result.username;
|
||||
context.gmLevel = result.gmLevel;
|
||||
LOG_DEBUG("User %s authenticated via API token (GM level %d)", result.username.c_str(), result.gmLevel);
|
||||
return true;
|
||||
} else {
|
||||
LOG_DEBUG("Authentication token validation failed: %s", result.errorMessage.c_str());
|
||||
return true; // Continue - let routes decide if auth is required
|
||||
}
|
||||
}
|
||||
90
dDashboardServer/auth/AuthTokenHandler.h
Normal file
90
dDashboardServer/auth/AuthTokenHandler.h
Normal file
@@ -0,0 +1,90 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <cstdint>
|
||||
|
||||
/**
|
||||
* Centralized authentication token handler
|
||||
* Consolidates token extraction from multiple sources and validation
|
||||
* Used by both HTTP API routes and WebSocket connections
|
||||
*/
|
||||
class AuthTokenHandler {
|
||||
public:
|
||||
/**
|
||||
* Result of token extraction and validation
|
||||
*/
|
||||
struct TokenValidationResult {
|
||||
bool isValid{false};
|
||||
std::string username{};
|
||||
uint8_t gmLevel{0};
|
||||
std::string errorMessage{};
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract token from query string
|
||||
* Expected format: "?token=eyJhbGc..." or "?token=xyz&other=abc"
|
||||
* @param queryString The query string from the request
|
||||
* @return The token value, or empty string if not found
|
||||
*/
|
||||
static std::string ExtractTokenFromQueryString(const std::string& queryString);
|
||||
|
||||
/**
|
||||
* Extract token from Cookie header
|
||||
* Looks for "dashboardToken=<value>" in the cookie string
|
||||
* @param cookieHeader The Cookie header value
|
||||
* @return The token value, or empty string if not found
|
||||
*/
|
||||
static std::string ExtractTokenFromCookieHeader(const std::string& cookieHeader);
|
||||
|
||||
/**
|
||||
* Extract token from Authorization header
|
||||
* Supports "Bearer <token>", "Token <token>", or raw token
|
||||
* @param authHeader The Authorization header value
|
||||
* @return The token value, or empty string if not found
|
||||
*/
|
||||
static std::string ExtractTokenFromAuthHeader(const std::string& authHeader);
|
||||
|
||||
/**
|
||||
* Extract token from any available source
|
||||
* Tries in priority order: query string, cookie, auth header
|
||||
* @param queryString The query string
|
||||
* @param cookieHeader The Cookie header
|
||||
* @param authHeader The Authorization header
|
||||
* @return The first token found, or empty string
|
||||
*/
|
||||
static std::string ExtractToken(
|
||||
const std::string& queryString,
|
||||
const std::string& cookieHeader,
|
||||
const std::string& authHeader
|
||||
);
|
||||
|
||||
/**
|
||||
* Validate a token and extract user information
|
||||
* Checks JWT signature, expiration, and user permissions
|
||||
* @param token The JWT token
|
||||
* @return TokenValidationResult with validity status and user info
|
||||
*/
|
||||
static TokenValidationResult ValidateToken(const std::string& token);
|
||||
|
||||
/**
|
||||
* Convenience method: Extract and validate token in one call
|
||||
* @param queryString Query string from request
|
||||
* @param cookieHeader Cookie header from request
|
||||
* @param authHeader Authorization header from request
|
||||
* @return TokenValidationResult with validity status and user info
|
||||
*/
|
||||
static TokenValidationResult ExtractAndValidateToken(
|
||||
const std::string& queryString,
|
||||
const std::string& cookieHeader,
|
||||
const std::string& authHeader
|
||||
);
|
||||
|
||||
/**
|
||||
* Process authentication for HTTP middleware use
|
||||
* Extracts and validates token from request, sets HTTPContext properties
|
||||
* @param context HTTP request context (modified to include auth info)
|
||||
* @param reply HTTP reply (not modified unless validation fails silently)
|
||||
* @return true to continue middleware chain, false to stop
|
||||
*/
|
||||
static bool ProcessHTTPContext(class HTTPContext& context, class HTTPReply& reply);
|
||||
};
|
||||
29
dDashboardServer/auth/CMakeLists.txt
Normal file
29
dDashboardServer/auth/CMakeLists.txt
Normal file
@@ -0,0 +1,29 @@
|
||||
set(DASHBOARDAUTH_SOURCES
|
||||
"JWTUtils.cpp"
|
||||
"DashboardAuthService.cpp"
|
||||
"AuthMiddleware.cpp"
|
||||
"AuthTokenHandler.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)
|
||||
144
dDashboardServer/auth/DashboardAuthService.cpp
Normal file
144
dDashboardServer/auth/DashboardAuthService.cpp
Normal file
@@ -0,0 +1,144 @@
|
||||
#include "DashboardAuthService.h"
|
||||
#include "JWTUtils.h"
|
||||
#include "Database.h"
|
||||
#include "Logger.h"
|
||||
#include "Game.h"
|
||||
#include "dConfig.h"
|
||||
#include "GeneralUtils.h"
|
||||
#include <bcrypt/bcrypt.h>
|
||||
#include <ctime>
|
||||
|
||||
namespace {
|
||||
constexpr int64_t LOCKOUT_DURATION = 15 * 60; // 15 minutes in seconds
|
||||
|
||||
}
|
||||
|
||||
DashboardAuthService::LoginResult DashboardAuthService::Login(
|
||||
const std::string& username,
|
||||
const std::string& password,
|
||||
bool rememberMe) {
|
||||
|
||||
LoginResult result;
|
||||
|
||||
if (username.empty() || password.empty()) {
|
||||
result.message = "Username and password are required";
|
||||
return result;
|
||||
}
|
||||
|
||||
if (password.length() > 40) {
|
||||
result.message = "Password exceeds maximum length (40 characters)";
|
||||
return result;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get account info
|
||||
auto accountInfo = Database::Get()->GetAccountInfo(username);
|
||||
if (!accountInfo) {
|
||||
result.message = "Invalid username or password";
|
||||
LOG_DEBUG("Login attempt for non-existent user: %s", username.c_str());
|
||||
return result;
|
||||
}
|
||||
|
||||
uint32_t accountId = accountInfo->id;
|
||||
|
||||
// Check if account is locked
|
||||
bool isLockedOut = Database::Get()->IsLockedOut(accountId);
|
||||
|
||||
if (isLockedOut) {
|
||||
// Record failed attempt even without checking password
|
||||
Database::Get()->RecordFailedAttempt(accountId);
|
||||
uint8_t failedAttempts = Database::Get()->GetFailedAttempts(accountId);
|
||||
|
||||
result.message = "Account is locked due to too many failed attempts";
|
||||
result.accountLocked = true;
|
||||
LOG("Login attempt on locked account: %s (failed attempts: %d)", username.c_str(), failedAttempts);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Check password
|
||||
if (::bcrypt_checkpw(password.c_str(), accountInfo->bcryptPassword.c_str()) != 0) {
|
||||
// Record failed attempt
|
||||
Database::Get()->RecordFailedAttempt(accountId);
|
||||
uint8_t newFailedAttempts = Database::Get()->GetFailedAttempts(accountId);
|
||||
|
||||
// Lock account after 3 failed attempts
|
||||
if (newFailedAttempts >= 3) {
|
||||
int64_t lockoutUntil = std::time(nullptr) + LOCKOUT_DURATION;
|
||||
Database::Get()->SetLockout(accountId, lockoutUntil);
|
||||
result.message = "Account locked due to too many failed attempts";
|
||||
result.accountLocked = true;
|
||||
LOG("Account locked after failed attempts: %s", username.c_str());
|
||||
} else {
|
||||
result.message = "Invalid username or password";
|
||||
LOG_DEBUG("Failed login attempt for user: %s (attempt %d/3)",
|
||||
username.c_str(), newFailedAttempts);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Check GM level
|
||||
if (!HasDashboardAccess(static_cast<uint8_t>(accountInfo->maxGmLevel))) {
|
||||
result.message = "Access denied: insufficient permissions";
|
||||
LOG("Access denied for non-admin user: %s", username.c_str());
|
||||
return result;
|
||||
}
|
||||
|
||||
// Successful login
|
||||
Database::Get()->ClearFailedAttempts(accountId);
|
||||
result.success = true;
|
||||
result.gmLevel = static_cast<uint8_t>(accountInfo->maxGmLevel);
|
||||
result.token = JWTUtils::GenerateToken(username, result.gmLevel, rememberMe);
|
||||
result.message = "Login successful";
|
||||
|
||||
LOG("Successful login: %s (GM Level: %d)", username.c_str(), result.gmLevel);
|
||||
return result;
|
||||
|
||||
} catch (const std::exception& ex) {
|
||||
result.message = "An error occurred during login";
|
||||
LOG("Error during login process: %s", ex.what());
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
bool DashboardAuthService::VerifyToken(const std::string& token, std::string& username, uint8_t& gmLevel) {
|
||||
JWTUtils::JWTPayload payload;
|
||||
if (!JWTUtils::ValidateToken(token, payload)) {
|
||||
LOG_DEBUG("Token validation failed: invalid or expired JWT");
|
||||
return false;
|
||||
}
|
||||
|
||||
username = payload.username;
|
||||
gmLevel = payload.gmLevel;
|
||||
|
||||
// Optionally verify user still exists and has access
|
||||
try {
|
||||
auto accountInfo = Database::Get()->GetAccountInfo(username);
|
||||
if (!accountInfo || !HasDashboardAccess(static_cast<uint8_t>(accountInfo->maxGmLevel))) {
|
||||
LOG_DEBUG("Token verification failed: user no longer has access");
|
||||
return false;
|
||||
}
|
||||
} catch (const std::exception& ex) {
|
||||
LOG_DEBUG("Error verifying user during token validation: %s", ex.what());
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_DEBUG("Token verified successfully for user: %s (GM Level: %d)", username.c_str(), gmLevel);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DashboardAuthService::HasDashboardAccess(uint8_t gmLevel) {
|
||||
// Get minimum GM level from config (default 0 = any user)
|
||||
uint8_t minGmLevel = 0;
|
||||
|
||||
if (Game::config) {
|
||||
const std::string& minGmLevelStr = Game::config->GetValue("min_dashboard_gm_level");
|
||||
if (!minGmLevelStr.empty()) {
|
||||
const auto parsed = GeneralUtils::TryParse<uint8_t>(minGmLevelStr);
|
||||
if (parsed) {
|
||||
minGmLevel = parsed.value();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return gmLevel >= minGmLevel;
|
||||
}
|
||||
47
dDashboardServer/auth/DashboardAuthService.h
Normal file
47
dDashboardServer/auth/DashboardAuthService.h
Normal file
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <cstdint>
|
||||
|
||||
/**
|
||||
* Dashboard authentication service
|
||||
* Handles user login, password verification, and account lockout
|
||||
*/
|
||||
class DashboardAuthService {
|
||||
public:
|
||||
/**
|
||||
* Login result structure
|
||||
*/
|
||||
struct LoginResult {
|
||||
bool success{false};
|
||||
std::string message{};
|
||||
std::string token{}; // JWT token if successful
|
||||
uint8_t gmLevel{0}; // GM level if successful
|
||||
bool accountLocked{false}; // Account is locked out
|
||||
};
|
||||
|
||||
/**
|
||||
* Attempt to log in with username and password
|
||||
* @param username The username
|
||||
* @param password The plaintext password (max 40 characters)
|
||||
* @param rememberMe If true, extends token expiration to 30 days
|
||||
* @return LoginResult with success status and JWT token if successful
|
||||
*/
|
||||
static LoginResult Login(const std::string& username, const std::string& password, bool rememberMe = false);
|
||||
|
||||
/**
|
||||
* Verify that a token is valid and get the username
|
||||
* @param token The JWT token
|
||||
* @param username Output parameter for the username
|
||||
* @param gmLevel Output parameter for the GM level
|
||||
* @return true if token is valid
|
||||
*/
|
||||
static bool VerifyToken(const std::string& token, std::string& username, uint8_t& gmLevel);
|
||||
|
||||
/**
|
||||
* Check if user has required GM level for dashboard access
|
||||
* @param gmLevel The user's GM level
|
||||
* @return true if user can access dashboard (GM level > 0)
|
||||
*/
|
||||
static bool HasDashboardAccess(uint8_t gmLevel);
|
||||
};
|
||||
186
dDashboardServer/auth/JWTUtils.cpp
Normal file
186
dDashboardServer/auth/JWTUtils.cpp
Normal file
@@ -0,0 +1,186 @@
|
||||
#include "JWTUtils.h"
|
||||
#include "GeneralUtils.h"
|
||||
#include "Logger.h"
|
||||
#include "json.hpp"
|
||||
#include <ctime>
|
||||
#include <cstring>
|
||||
#include <openssl/hmac.h>
|
||||
#include <openssl/sha.h>
|
||||
|
||||
namespace {
|
||||
std::string g_Secret = "default-secret-change-me";
|
||||
|
||||
// Simple base64 encoding
|
||||
std::string Base64Encode(const std::string& input) {
|
||||
static const char* base64_chars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
std::string ret;
|
||||
int i = 0;
|
||||
unsigned char char_array_3[3];
|
||||
unsigned char char_array_4[4];
|
||||
|
||||
for (size_t n = 0; n < input.length(); n++) {
|
||||
char_array_3[i++] = input[n];
|
||||
if (i == 3) {
|
||||
char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
|
||||
char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
|
||||
char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
|
||||
char_array_4[3] = char_array_3[2] & 0x3f;
|
||||
for (i = 0; i < 4; i++) ret += base64_chars[char_array_4[i]];
|
||||
i = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (i) {
|
||||
for (int j = i; j < 3; j++) char_array_3[j] = '\0';
|
||||
char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
|
||||
char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
|
||||
char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
|
||||
for (int j = 0; j <= i; j++) ret += base64_chars[char_array_4[j]];
|
||||
while (i++ < 3) ret += '=';
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Simple base64 decoding
|
||||
std::string Base64Decode(const std::string& encoded_string) {
|
||||
static const std::string base64_chars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
int in_len = encoded_string.size();
|
||||
int i = 0, j = 0, in_ = 0;
|
||||
unsigned char char_array_4[4], char_array_3[3];
|
||||
std::string ret;
|
||||
|
||||
while (in_len-- && (encoded_string[in_] != '=') &&
|
||||
(isalnum(encoded_string[in_]) || encoded_string[in_] == '+' || encoded_string[in_] == '/')) {
|
||||
char_array_4[i++] = encoded_string[in_]; in_++;
|
||||
if (i == 4) {
|
||||
for (i = 0; i < 4; i++) char_array_4[i] = base64_chars.find(char_array_4[i]);
|
||||
char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
|
||||
char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
|
||||
char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];
|
||||
for (i = 0; i < 3; i++) ret += char_array_3[i];
|
||||
i = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (i) {
|
||||
for (j = i; j < 4; j++) char_array_4[j] = 0;
|
||||
for (j = 0; j < 4; j++) char_array_4[j] = base64_chars.find(char_array_4[j]);
|
||||
char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
|
||||
char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
|
||||
for (j = 0; j < i - 1; j++) ret += char_array_3[j];
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// HMAC-SHA256
|
||||
std::string HmacSha256(const std::string& key, const std::string& message) {
|
||||
unsigned char* digest = HMAC(EVP_sha256(),
|
||||
reinterpret_cast<const unsigned char*>(key.c_str()), key.length(),
|
||||
reinterpret_cast<const unsigned char*>(message.c_str()), message.length(),
|
||||
nullptr, nullptr);
|
||||
|
||||
std::string result(reinterpret_cast<char*>(digest), SHA256_DIGEST_LENGTH);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Create signature for JWT
|
||||
std::string CreateSignature(const std::string& header, const std::string& payload, const std::string& secret) {
|
||||
std::string message = header + "." + payload;
|
||||
std::string signature = HmacSha256(secret, message);
|
||||
return Base64Encode(signature);
|
||||
}
|
||||
|
||||
// Verify JWT signature
|
||||
bool VerifySignature(const std::string& header, const std::string& payload, const std::string& signature, const std::string& secret) {
|
||||
std::string expected = CreateSignature(header, payload, secret);
|
||||
return signature == expected;
|
||||
}
|
||||
}
|
||||
|
||||
namespace JWTUtils {
|
||||
void SetSecretKey(const std::string& secret) {
|
||||
if (secret.empty()) {
|
||||
LOG("Warning: JWT secret key is empty, using default");
|
||||
return;
|
||||
}
|
||||
g_Secret = secret;
|
||||
}
|
||||
|
||||
std::string GenerateToken(const std::string& username, uint8_t gmLevel, bool rememberMe) {
|
||||
// Header
|
||||
std::string header = R"({"alg":"HS256","typ":"JWT"})";
|
||||
std::string encodedHeader = Base64Encode(header);
|
||||
|
||||
// Payload
|
||||
int64_t now = std::time(nullptr);
|
||||
int64_t expiresAt = now + (rememberMe ? 30 * 24 * 60 * 60 : 24 * 60 * 60); // 30 days or 24 hours
|
||||
|
||||
std::string payload = R"({"username":")" + username + R"(","gmLevel":)" + std::to_string(gmLevel) +
|
||||
R"(,"rememberMe":)" + (rememberMe ? "true" : "false") +
|
||||
R"(,"iat":)" + std::to_string(now) +
|
||||
R"(,"exp":)" + std::to_string(expiresAt) + "}";
|
||||
std::string encodedPayload = Base64Encode(payload);
|
||||
|
||||
// Signature
|
||||
std::string signature = CreateSignature(encodedHeader, encodedPayload, g_Secret);
|
||||
|
||||
return encodedHeader + "." + encodedPayload + "." + signature;
|
||||
}
|
||||
|
||||
bool ValidateToken(const std::string& token, JWTPayload& payload) {
|
||||
// Split token into parts
|
||||
size_t firstDot = token.find('.');
|
||||
size_t secondDot = token.find('.', firstDot + 1);
|
||||
|
||||
if (firstDot == std::string::npos || secondDot == std::string::npos) {
|
||||
LOG_DEBUG("Invalid JWT format");
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string header = token.substr(0, firstDot);
|
||||
std::string encodedPayload = token.substr(firstDot + 1, secondDot - firstDot - 1);
|
||||
std::string signature = token.substr(secondDot + 1);
|
||||
|
||||
// Verify signature
|
||||
if (!VerifySignature(header, encodedPayload, signature, g_Secret)) {
|
||||
LOG_DEBUG("Invalid JWT signature");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Decode and parse payload
|
||||
std::string decodedPayload = Base64Decode(encodedPayload);
|
||||
try {
|
||||
auto json = nlohmann::json::parse(decodedPayload);
|
||||
|
||||
payload.username = json.value("username", "");
|
||||
payload.gmLevel = json.value("gmLevel", 0);
|
||||
payload.rememberMe = json.value("rememberMe", false);
|
||||
payload.issuedAt = json.value("iat", 0);
|
||||
payload.expiresAt = json.value("exp", 0);
|
||||
|
||||
if (payload.username.empty()) {
|
||||
LOG_DEBUG("JWT missing username");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check expiration
|
||||
if (IsTokenExpired(payload.expiresAt)) {
|
||||
LOG_DEBUG("JWT token expired");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (const std::exception& ex) {
|
||||
LOG_DEBUG("Error parsing JWT payload: %s", ex.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool IsTokenExpired(int64_t expiresAt) {
|
||||
return std::time(nullptr) > expiresAt;
|
||||
}
|
||||
}
|
||||
52
dDashboardServer/auth/JWTUtils.h
Normal file
52
dDashboardServer/auth/JWTUtils.h
Normal file
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <ctime>
|
||||
#include "json_fwd.hpp"
|
||||
|
||||
/**
|
||||
* JWT Token utilities for dashboard authentication
|
||||
* Provides secure token generation, validation, and parsing
|
||||
*/
|
||||
namespace JWTUtils {
|
||||
/**
|
||||
* JWT payload structure
|
||||
*/
|
||||
struct JWTPayload {
|
||||
std::string username{};
|
||||
uint8_t gmLevel{0};
|
||||
bool rememberMe{false};
|
||||
int64_t issuedAt{0};
|
||||
int64_t expiresAt{0};
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a new JWT token
|
||||
* @param username The username to encode in the token
|
||||
* @param gmLevel The GM level of the user
|
||||
* @param rememberMe If true, extends token expiration to 30 days; otherwise 24 hours
|
||||
* @return Signed JWT token string
|
||||
*/
|
||||
std::string GenerateToken(const std::string& username, uint8_t gmLevel, bool rememberMe = false);
|
||||
|
||||
/**
|
||||
* Validate and decode a JWT token
|
||||
* @param token The JWT token to validate
|
||||
* @param payload Output parameter for the decoded payload
|
||||
* @return true if token is valid and not expired, false otherwise
|
||||
*/
|
||||
bool ValidateToken(const std::string& token, JWTPayload& payload);
|
||||
|
||||
/**
|
||||
* Check if a token is expired
|
||||
* @param expiresAt Expiration timestamp
|
||||
* @return true if token is expired
|
||||
*/
|
||||
bool IsTokenExpired(int64_t expiresAt);
|
||||
|
||||
/**
|
||||
* Set the JWT secret key (must be called once at startup)
|
||||
* @param secret The secret key for signing tokens
|
||||
*/
|
||||
void SetSecretKey(const std::string& secret);
|
||||
}
|
||||
35
dDashboardServer/auth/RequireAuthMiddleware.cpp
Normal file
35
dDashboardServer/auth/RequireAuthMiddleware.cpp
Normal file
@@ -0,0 +1,35 @@
|
||||
#include "RequireAuthMiddleware.h"
|
||||
#include "HTTPContext.h"
|
||||
#include "Web.h"
|
||||
#include "Game.h"
|
||||
#include "Logger.h"
|
||||
|
||||
RequireAuthMiddleware::RequireAuthMiddleware(uint8_t minGmLevel) : minGmLevel(minGmLevel) {}
|
||||
|
||||
bool RequireAuthMiddleware::Process(HTTPContext& context, HTTPReply& reply) {
|
||||
// Check if user is authenticated
|
||||
if (!context.isAuthenticated) {
|
||||
LOG_DEBUG("Unauthorized access attempt to %s from %s", context.path.c_str(), context.clientIP.c_str());
|
||||
reply.status = eHTTPStatusCode::FOUND;
|
||||
reply.message = "";
|
||||
reply.location = "/login";
|
||||
reply.contentType = eContentType::TEXT_HTML;
|
||||
return false; // Stop middleware chain and send reply
|
||||
}
|
||||
|
||||
// Check if user has required GM level
|
||||
if (context.gmLevel < minGmLevel) {
|
||||
LOG_DEBUG("Forbidden access attempt by user %s (GM level %d < %d required) to %s from %s",
|
||||
context.authenticatedUser.c_str(), context.gmLevel, minGmLevel,
|
||||
context.path.c_str(), context.clientIP.c_str());
|
||||
reply.status = eHTTPStatusCode::FORBIDDEN;
|
||||
reply.message = "{\"error\":\"Forbidden - Insufficient permissions\"}";
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
return false; // Stop middleware chain and send reply
|
||||
}
|
||||
|
||||
// Authentication passed
|
||||
LOG_DEBUG("User %s authenticated with GM level %d accessing %s",
|
||||
context.authenticatedUser.c_str(), context.gmLevel, context.path.c_str());
|
||||
return true; // Continue to next middleware or route handler
|
||||
}
|
||||
30
dDashboardServer/auth/RequireAuthMiddleware.h
Normal file
30
dDashboardServer/auth/RequireAuthMiddleware.h
Normal file
@@ -0,0 +1,30 @@
|
||||
#ifndef __REQUIREAUTHMIDDLEWARE_H__
|
||||
#define __REQUIREAUTHMIDDLEWARE_H__
|
||||
|
||||
#include <memory>
|
||||
#include <cstdint>
|
||||
#include "IHTTPMiddleware.h"
|
||||
|
||||
/**
|
||||
* RequireAuthMiddleware: Enforces authentication on protected routes
|
||||
*
|
||||
* Returns 401 Unauthorized if user is not authenticated
|
||||
* Returns 403 Forbidden if user's GM level is below minimum required
|
||||
*/
|
||||
class RequireAuthMiddleware final : public IHTTPMiddleware {
|
||||
public:
|
||||
/**
|
||||
* @param minGmLevel Minimum GM level required to access this route
|
||||
* 0 = any authenticated user, higher numbers = GM-only
|
||||
*/
|
||||
explicit RequireAuthMiddleware(uint8_t minGmLevel = 0);
|
||||
~RequireAuthMiddleware() override = default;
|
||||
|
||||
bool Process(HTTPContext& context, HTTPReply& reply) override;
|
||||
std::string GetName() const override { return "RequireAuthMiddleware"; }
|
||||
|
||||
private:
|
||||
uint8_t minGmLevel;
|
||||
};
|
||||
|
||||
#endif // !__REQUIREAUTHMIDDLEWARE_H__
|
||||
443
dDashboardServer/routes/APIRoutes.cpp
Normal file
443
dDashboardServer/routes/APIRoutes.cpp
Normal file
@@ -0,0 +1,443 @@
|
||||
#include "APIRoutes.h"
|
||||
#include "ServerState.h"
|
||||
#include "Web.h"
|
||||
#include "eHTTPMethod.h"
|
||||
#include "json.hpp"
|
||||
#include "Game.h"
|
||||
#include "Database.h"
|
||||
#include "Logger.h"
|
||||
#include "HTTPContext.h"
|
||||
#include "RequireAuthMiddleware.h"
|
||||
#include <memory>
|
||||
|
||||
void RegisterAPIRoutes() {
|
||||
// GET /api/status - Get overall server status
|
||||
Game::web.RegisterHTTPRoute({
|
||||
.path = "/api/status",
|
||||
.method = eHTTPMethod::GET,
|
||||
.middleware = { std::make_shared<RequireAuthMiddleware>(0) },
|
||||
.handle = [](HTTPReply& reply, const HTTPContext& context) {
|
||||
nlohmann::json response = ServerState::GetServerStateJson();
|
||||
|
||||
reply.status = eHTTPStatusCode::OK;
|
||||
reply.message = response.dump();
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/players - Get list of online players
|
||||
Game::web.RegisterHTTPRoute({
|
||||
.path = "/api/players",
|
||||
.method = eHTTPMethod::GET,
|
||||
.middleware = { std::make_shared<RequireAuthMiddleware>(0) },
|
||||
.handle = [](HTTPReply& reply, const HTTPContext& context) {
|
||||
nlohmann::json response = {
|
||||
{"players", nlohmann::json::array()},
|
||||
{"count", 0}
|
||||
};
|
||||
|
||||
reply.status = eHTTPStatusCode::OK;
|
||||
reply.message = response.dump();
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/accounts/count - Get total account count
|
||||
Game::web.RegisterHTTPRoute({
|
||||
.path = "/api/accounts/count",
|
||||
.method = eHTTPMethod::GET,
|
||||
.middleware = { std::make_shared<RequireAuthMiddleware>(0) },
|
||||
.handle = [](HTTPReply& reply, const HTTPContext& context) {
|
||||
try {
|
||||
const uint32_t count = Database::Get()->GetAccountCount();
|
||||
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/accounts/count: %s", ex.what());
|
||||
reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR;
|
||||
reply.message = "{\"error\":\"Database error\"}";
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/characters/count - Get total character count
|
||||
Game::web.RegisterHTTPRoute({
|
||||
.path = "/api/characters/count",
|
||||
.method = eHTTPMethod::GET,
|
||||
.middleware = { std::make_shared<RequireAuthMiddleware>(0) },
|
||||
.handle = [](HTTPReply& reply, const HTTPContext& context) {
|
||||
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<RequireAuthMiddleware>(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<std::string>();
|
||||
} else if (requestData["search"].is_object() && requestData["search"].contains("value")) {
|
||||
search = requestData["search"]["value"].get<std::string>();
|
||||
}
|
||||
}
|
||||
|
||||
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<RequireAuthMiddleware>(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<std::string>();
|
||||
} else if (requestData["search"].is_object() && requestData["search"].contains("value")) {
|
||||
search = requestData["search"]["value"].get<std::string>();
|
||||
}
|
||||
}
|
||||
|
||||
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<RequireAuthMiddleware>(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<std::string>();
|
||||
} else if (requestData["search"].is_object() && requestData["search"].contains("value")) {
|
||||
search = requestData["search"]["value"].get<std::string>();
|
||||
}
|
||||
}
|
||||
|
||||
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<RequireAuthMiddleware>(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<std::string>();
|
||||
} else if (requestData["search"].is_object() && requestData["search"].contains("value")) {
|
||||
search = requestData["search"]["value"].get<std::string>();
|
||||
}
|
||||
}
|
||||
|
||||
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<RequireAuthMiddleware>(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<std::string>();
|
||||
} else if (requestData["search"].is_object() && requestData["search"].contains("value")) {
|
||||
search = requestData["search"]["value"].get<std::string>();
|
||||
}
|
||||
}
|
||||
|
||||
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<RequireAuthMiddleware>(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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
3
dDashboardServer/routes/APIRoutes.h
Normal file
3
dDashboardServer/routes/APIRoutes.h
Normal file
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
void RegisterAPIRoutes();
|
||||
102
dDashboardServer/routes/AuthRoutes.cpp
Normal file
102
dDashboardServer/routes/AuthRoutes.cpp
Normal file
@@ -0,0 +1,102 @@
|
||||
#include "AuthRoutes.h"
|
||||
#include "DashboardAuthService.h"
|
||||
#include "json.hpp"
|
||||
#include "Logger.h"
|
||||
#include "GeneralUtils.h"
|
||||
#include "Web.h"
|
||||
#include "eHTTPMethod.h"
|
||||
#include "HTTPContext.h"
|
||||
|
||||
void RegisterAuthRoutes() {
|
||||
// POST /api/auth/login
|
||||
// Request body: { "username": "string", "password": "string", "rememberMe": boolean }
|
||||
// Response: { "success": boolean, "message": "string", "token": "string", "gmLevel": number }
|
||||
Game::web.RegisterHTTPRoute({
|
||||
.path = "/api/auth/login",
|
||||
.method = eHTTPMethod::POST,
|
||||
.middleware = {},
|
||||
.handle = [](HTTPReply& reply, const HTTPContext& context) {
|
||||
try {
|
||||
auto json = nlohmann::json::parse(context.body);
|
||||
std::string username = json.value("username", "");
|
||||
std::string password = json.value("password", "");
|
||||
bool rememberMe = json.value("rememberMe", false);
|
||||
|
||||
// Validate input
|
||||
if (username.empty() || password.empty()) {
|
||||
reply.message = R"({"success":false,"message":"Username and password are required"})";
|
||||
reply.status = eHTTPStatusCode::BAD_REQUEST;
|
||||
return;
|
||||
}
|
||||
|
||||
if (password.length() > 40) {
|
||||
reply.message = R"({"success":false,"message":"Password exceeds maximum length"})";
|
||||
reply.status = eHTTPStatusCode::BAD_REQUEST;
|
||||
return;
|
||||
}
|
||||
|
||||
// Attempt login
|
||||
auto result = DashboardAuthService::Login(username, password, rememberMe);
|
||||
|
||||
nlohmann::json response;
|
||||
response["success"] = result.success;
|
||||
response["message"] = result.message;
|
||||
if (result.success) {
|
||||
response["token"] = result.token;
|
||||
response["gmLevel"] = result.gmLevel;
|
||||
}
|
||||
|
||||
reply.message = response.dump();
|
||||
reply.status = result.success ? eHTTPStatusCode::OK : eHTTPStatusCode::UNAUTHORIZED;
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
} catch (const std::exception& ex) {
|
||||
LOG("Error processing login request: %s", ex.what());
|
||||
reply.message = R"({"success":false,"message":"Internal server error"})";
|
||||
reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR;
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/auth/verify
|
||||
// Request body: { "token": "string" }
|
||||
// Response: { "valid": boolean, "username": "string", "gmLevel": number }
|
||||
Game::web.RegisterHTTPRoute({
|
||||
.path = "/api/auth/verify",
|
||||
.method = eHTTPMethod::POST,
|
||||
.middleware = {},
|
||||
.handle = [](HTTPReply& reply, const HTTPContext& context) {
|
||||
try {
|
||||
auto json = nlohmann::json::parse(context.body);
|
||||
std::string token = json.value("token", "");
|
||||
|
||||
if (token.empty()) {
|
||||
reply.message = R"({"valid":false})";
|
||||
reply.status = eHTTPStatusCode::BAD_REQUEST;
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
return;
|
||||
}
|
||||
|
||||
std::string username;
|
||||
uint8_t gmLevel{};
|
||||
bool valid = DashboardAuthService::VerifyToken(token, username, gmLevel);
|
||||
|
||||
nlohmann::json response;
|
||||
response["valid"] = valid;
|
||||
if (valid) {
|
||||
response["username"] = username;
|
||||
response["gmLevel"] = gmLevel;
|
||||
}
|
||||
|
||||
reply.message = response.dump();
|
||||
reply.status = eHTTPStatusCode::OK;
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
} catch (const std::exception& ex) {
|
||||
LOG("Error processing verify request: %s", ex.what());
|
||||
reply.message = R"({"valid":false})";
|
||||
reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR;
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
10
dDashboardServer/routes/AuthRoutes.h
Normal file
10
dDashboardServer/routes/AuthRoutes.h
Normal file
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "Web.h"
|
||||
|
||||
/**
|
||||
* Register authentication routes
|
||||
* /api/auth/login - POST login endpoint
|
||||
* /api/auth/verify - POST verify token endpoint
|
||||
*/
|
||||
void RegisterAuthRoutes();
|
||||
30
dDashboardServer/routes/CMakeLists.txt
Normal file
30
dDashboardServer/routes/CMakeLists.txt
Normal file
@@ -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)
|
||||
291
dDashboardServer/routes/DashboardRoutes.cpp
Normal file
291
dDashboardServer/routes/DashboardRoutes.cpp
Normal file
@@ -0,0 +1,291 @@
|
||||
#include "DashboardRoutes.h"
|
||||
#include "ServerState.h"
|
||||
#include "Web.h"
|
||||
#include "HTTPContext.h"
|
||||
#include "eHTTPMethod.h"
|
||||
#include "json.hpp"
|
||||
#include "Game.h"
|
||||
#include "Database.h"
|
||||
#include "Logger.h"
|
||||
#include "inja.hpp"
|
||||
#include "AuthMiddleware.h"
|
||||
#include "RequireAuthMiddleware.h"
|
||||
|
||||
void RegisterDashboardRoutes() {
|
||||
// GET / - Main dashboard page (requires authentication)
|
||||
Game::web.RegisterHTTPRoute({
|
||||
.path = "/",
|
||||
.method = eHTTPMethod::GET,
|
||||
.middleware = { std::make_shared<RequireAuthMiddleware>(0) },
|
||||
.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();
|
||||
|
||||
// Server status - merge with server state
|
||||
nlohmann::json serverState = ServerState::GetServerStateJson();
|
||||
data.merge_patch(serverState);
|
||||
|
||||
// Statistics
|
||||
data["stats"]["onlinePlayers"] = 0; // TODO: Get from server communication
|
||||
data["stats"]["totalAccounts"] = Database::Get()->GetAccountCount();
|
||||
data["stats"]["totalCharacters"] = Database::Get()->GetCharacterCount();
|
||||
|
||||
// Render template
|
||||
const std::string html = env.render_file("index.jinja2", data);
|
||||
|
||||
reply.status = eHTTPStatusCode::OK;
|
||||
reply.message = html;
|
||||
reply.contentType = eContentType::TEXT_HTML;
|
||||
} catch (const std::exception& ex) {
|
||||
LOG("Error rendering template: %s", ex.what());
|
||||
reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR;
|
||||
reply.message = "{\"error\":\"Failed to render template\"}";
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// GET /login - Login page (no authentication required)
|
||||
Game::web.RegisterHTTPRoute({
|
||||
.path = "/login",
|
||||
.method = eHTTPMethod::GET,
|
||||
.middleware = {},
|
||||
.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);
|
||||
|
||||
// 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;
|
||||
reply.message = html;
|
||||
reply.contentType = eContentType::TEXT_HTML;
|
||||
} catch (const std::exception& ex) {
|
||||
LOG("Error rendering login template: %s", ex.what());
|
||||
reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR;
|
||||
reply.message = "{\"error\":\"Failed to render login page\"}";
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// GET /accounts/:id - View single account
|
||||
Game::web.RegisterHTTPRoute({
|
||||
.path = "/accounts/:id",
|
||||
.method = eHTTPMethod::GET,
|
||||
.middleware = { std::make_shared<RequireAuthMiddleware>(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 = "<h1>404 - Account not found</h1>";
|
||||
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 = "<h1>404 - Invalid account ID</h1>";
|
||||
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 = "<h1>403 - Forbidden</h1><p>You do not have permission to view this account.</p>";
|
||||
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 = "<h1>404 - Account not found</h1>";
|
||||
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 = "<h1>500 - Server Error</h1>";
|
||||
reply.contentType = eContentType::TEXT_HTML;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// GET /accounts - Accounts management page
|
||||
Game::web.RegisterHTTPRoute({
|
||||
.path = "/accounts",
|
||||
.method = eHTTPMethod::GET,
|
||||
.middleware = { std::make_shared<RequireAuthMiddleware>(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<RequireAuthMiddleware>(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<RequireAuthMiddleware>(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<RequireAuthMiddleware>(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<RequireAuthMiddleware>(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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
7
dDashboardServer/routes/DashboardRoutes.h
Normal file
7
dDashboardServer/routes/DashboardRoutes.h
Normal file
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "json.hpp"
|
||||
|
||||
class HTTPContext;
|
||||
|
||||
void RegisterDashboardRoutes();
|
||||
52
dDashboardServer/routes/ServerState.h
Normal file
52
dDashboardServer/routes/ServerState.h
Normal file
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <cstdint>
|
||||
#include "json.hpp"
|
||||
|
||||
struct ServerStatus {
|
||||
bool online{false};
|
||||
uint32_t players{0};
|
||||
std::string version{};
|
||||
std::chrono::steady_clock::time_point lastSeen{};
|
||||
};
|
||||
|
||||
struct WorldInstanceInfo {
|
||||
uint32_t mapID{0};
|
||||
uint32_t instanceID{0};
|
||||
uint32_t cloneID{0};
|
||||
uint32_t players{0};
|
||||
std::string ip{};
|
||||
uint32_t port{0};
|
||||
bool isPrivate{false};
|
||||
};
|
||||
|
||||
namespace ServerState {
|
||||
extern ServerStatus g_AuthStatus;
|
||||
extern ServerStatus g_ChatStatus;
|
||||
extern std::vector<WorldInstanceInfo> g_WorldInstances;
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
68
dDashboardServer/routes/StaticRoutes.cpp
Normal file
68
dDashboardServer/routes/StaticRoutes.cpp
Normal file
@@ -0,0 +1,68 @@
|
||||
#include "StaticRoutes.h"
|
||||
#include "Web.h"
|
||||
#include "HTTPContext.h"
|
||||
#include "eHTTPMethod.h"
|
||||
#include "Game.h"
|
||||
#include "Logger.h"
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
namespace {
|
||||
std::string ReadFileToString(const std::string& filePath) {
|
||||
std::ifstream file(filePath);
|
||||
if (!file.is_open()) {
|
||||
LOG("Failed to open file: %s", filePath.c_str());
|
||||
return "";
|
||||
}
|
||||
std::stringstream buffer{};
|
||||
buffer << file.rdbuf();
|
||||
return buffer.str();
|
||||
}
|
||||
|
||||
eContentType GetContentType(const std::string& filePath) {
|
||||
if (filePath.ends_with(".css")) {
|
||||
return eContentType::TEXT_CSS;
|
||||
} else if (filePath.ends_with(".js")) {
|
||||
return eContentType::TEXT_JAVASCRIPT;
|
||||
} else if (filePath.ends_with(".html")) {
|
||||
return eContentType::TEXT_HTML;
|
||||
} else if (filePath.ends_with(".png")) {
|
||||
return eContentType::IMAGE_PNG;
|
||||
} else if (filePath.ends_with(".jpg") || filePath.ends_with(".jpeg")) {
|
||||
return eContentType::IMAGE_JPEG;
|
||||
} else if (filePath.ends_with(".json")) {
|
||||
return eContentType::APPLICATION_JSON;
|
||||
}
|
||||
return eContentType::TEXT_PLAIN;
|
||||
}
|
||||
|
||||
void ServeStaticFile(const std::string& urlPath, const std::string& filePath) {
|
||||
Game::web.RegisterHTTPRoute({
|
||||
.path = urlPath,
|
||||
.method = eHTTPMethod::GET,
|
||||
.middleware = {},
|
||||
.handle = [filePath](HTTPReply& reply, const HTTPContext& context) {
|
||||
const std::string content = ReadFileToString(filePath);
|
||||
if (content.empty()) {
|
||||
reply.status = eHTTPStatusCode::NOT_FOUND;
|
||||
reply.message = "{\"error\":\"File not found\"}";
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
} else {
|
||||
reply.status = eHTTPStatusCode::OK;
|
||||
reply.message = content;
|
||||
reply.contentType = GetContentType(filePath);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void RegisterStaticRoutes() {
|
||||
// Serve CSS files
|
||||
ServeStaticFile("/css/dashboard.css", "dDashboardServer/static/css/dashboard.css");
|
||||
ServeStaticFile("/css/login.css", "dDashboardServer/static/css/login.css");
|
||||
|
||||
// Serve JavaScript files
|
||||
ServeStaticFile("/js/dashboard.js", "dDashboardServer/static/js/dashboard.js");
|
||||
ServeStaticFile("/js/login.js", "dDashboardServer/static/js/login.js");
|
||||
}
|
||||
3
dDashboardServer/routes/StaticRoutes.h
Normal file
3
dDashboardServer/routes/StaticRoutes.h
Normal file
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
void RegisterStaticRoutes();
|
||||
232
dDashboardServer/routes/WSRoutes.cpp
Normal file
232
dDashboardServer/routes/WSRoutes.cpp
Normal file
@@ -0,0 +1,232 @@
|
||||
#include "WSRoutes.h"
|
||||
#include "ServerState.h"
|
||||
#include "Web.h"
|
||||
#include "json.hpp"
|
||||
#include "Game.h"
|
||||
#include "Database.h"
|
||||
#include "Logger.h"
|
||||
|
||||
void RegisterWSRoutes() {
|
||||
// Register WebSocket subscriptions for real-time updates
|
||||
Game::web.RegisterWSSubscription("dashboard_update");
|
||||
Game::web.RegisterWSSubscription("server_status");
|
||||
Game::web.RegisterWSSubscription("player_joined");
|
||||
Game::web.RegisterWSSubscription("player_left");
|
||||
|
||||
// Account subscriptions
|
||||
Game::web.RegisterWSSubscription("account_update");
|
||||
Game::web.RegisterWSSubscription("accounts_table_update");
|
||||
Game::web.RegisterWSSubscription("account_list_changed");
|
||||
|
||||
// Character subscriptions
|
||||
Game::web.RegisterWSSubscription("character_update");
|
||||
Game::web.RegisterWSSubscription("characters_table_update");
|
||||
Game::web.RegisterWSSubscription("character_list_changed");
|
||||
|
||||
// Property subscriptions
|
||||
Game::web.RegisterWSSubscription("property_update");
|
||||
Game::web.RegisterWSSubscription("properties_table_update");
|
||||
Game::web.RegisterWSSubscription("property_list_changed");
|
||||
|
||||
// Play Key subscriptions
|
||||
Game::web.RegisterWSSubscription("play_key_update");
|
||||
Game::web.RegisterWSSubscription("play_keys_table_update");
|
||||
Game::web.RegisterWSSubscription("play_key_list_changed");
|
||||
|
||||
// Bug Report subscriptions
|
||||
Game::web.RegisterWSSubscription("bug_report_update");
|
||||
Game::web.RegisterWSSubscription("bug_reports_table_update");
|
||||
}
|
||||
|
||||
void BroadcastDashboardUpdate() {
|
||||
// Get server state data (auth, chat, worlds) - mutex is acquired internally
|
||||
nlohmann::json data = ServerState::GetServerStateJson();
|
||||
|
||||
// Add statistics
|
||||
try {
|
||||
data["stats"]["onlinePlayers"] = 0; // TODO: Get from server communication
|
||||
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());
|
||||
}
|
||||
|
||||
// Broadcast to all connected WebSocket clients subscribed to "dashboard_update"
|
||||
Game::web.SendWSMessage("dashboard_update", data);
|
||||
}
|
||||
|
||||
void BroadcastAccountUpdate(uint32_t accountId) {
|
||||
try {
|
||||
// Get updated account data
|
||||
nlohmann::json accountData = Database::Get()->GetAccountById(accountId);
|
||||
|
||||
// Only broadcast if account was found
|
||||
if (!accountData.contains("error")) {
|
||||
accountData["event"] = "account_update";
|
||||
accountData["accountId"] = accountId;
|
||||
Game::web.SendWSMessage("account_update", accountData);
|
||||
}
|
||||
} catch (const std::exception& ex) {
|
||||
LOG_DEBUG("Error broadcasting account update: %s", ex.what());
|
||||
}
|
||||
}
|
||||
|
||||
void BroadcastAccountsTableUpdate() {
|
||||
try {
|
||||
// Broadcast that the accounts table has been modified
|
||||
// Clients should reload the current page of data
|
||||
nlohmann::json message = {
|
||||
{"event", "accounts_table_update"},
|
||||
{"message", "Accounts table has been updated"}
|
||||
};
|
||||
Game::web.SendWSMessage("accounts_table_update", message);
|
||||
} catch (const std::exception& ex) {
|
||||
LOG_DEBUG("Error broadcasting accounts table update: %s", ex.what());
|
||||
}
|
||||
}
|
||||
|
||||
void BroadcastAccountListChanged() {
|
||||
try {
|
||||
// Broadcast that accounts list changed (new account created or deleted)
|
||||
nlohmann::json message = {
|
||||
{"event", "account_list_changed"},
|
||||
{"totalAccounts", Database::Get()->GetAccountCount()}
|
||||
};
|
||||
Game::web.SendWSMessage("account_list_changed", message);
|
||||
} catch (const std::exception& ex) {
|
||||
LOG_DEBUG("Error broadcasting account list change: %s", ex.what());
|
||||
}
|
||||
}
|
||||
|
||||
void BroadcastCharacterUpdate(uint32_t characterId) {
|
||||
try {
|
||||
// Get updated character data and broadcast
|
||||
nlohmann::json message = {
|
||||
{"event", "character_update"},
|
||||
{"characterId", characterId}
|
||||
};
|
||||
Game::web.SendWSMessage("character_update", message);
|
||||
} catch (const std::exception& ex) {
|
||||
LOG_DEBUG("Error broadcasting character update: %s", ex.what());
|
||||
}
|
||||
}
|
||||
|
||||
void BroadcastCharactersTableUpdate() {
|
||||
try {
|
||||
nlohmann::json message = {
|
||||
{"event", "characters_table_update"},
|
||||
{"message", "Characters table has been updated"}
|
||||
};
|
||||
Game::web.SendWSMessage("characters_table_update", message);
|
||||
} catch (const std::exception& ex) {
|
||||
LOG_DEBUG("Error broadcasting characters table update: %s", ex.what());
|
||||
}
|
||||
}
|
||||
|
||||
void BroadcastCharacterListChanged() {
|
||||
try {
|
||||
nlohmann::json message = {
|
||||
{"event", "character_list_changed"},
|
||||
{"totalCharacters", Database::Get()->GetCharacterCount()}
|
||||
};
|
||||
Game::web.SendWSMessage("character_list_changed", message);
|
||||
} catch (const std::exception& ex) {
|
||||
LOG_DEBUG("Error broadcasting character list change: %s", ex.what());
|
||||
}
|
||||
}
|
||||
|
||||
void BroadcastPropertyUpdate(uint32_t propertyId) {
|
||||
try {
|
||||
nlohmann::json message = {
|
||||
{"event", "property_update"},
|
||||
{"propertyId", propertyId}
|
||||
};
|
||||
Game::web.SendWSMessage("property_update", message);
|
||||
} catch (const std::exception& ex) {
|
||||
LOG_DEBUG("Error broadcasting property update: %s", ex.what());
|
||||
}
|
||||
}
|
||||
|
||||
void BroadcastPropertiesTableUpdate() {
|
||||
try {
|
||||
nlohmann::json message = {
|
||||
{"event", "properties_table_update"},
|
||||
{"message", "Properties table has been updated"}
|
||||
};
|
||||
Game::web.SendWSMessage("properties_table_update", message);
|
||||
} catch (const std::exception& ex) {
|
||||
LOG_DEBUG("Error broadcasting properties table update: %s", ex.what());
|
||||
}
|
||||
}
|
||||
|
||||
void BroadcastPropertyListChanged() {
|
||||
try {
|
||||
nlohmann::json message = {
|
||||
{"event", "property_list_changed"},
|
||||
{"totalProperties", 0} // TODO: Get from database
|
||||
};
|
||||
Game::web.SendWSMessage("property_list_changed", message);
|
||||
} catch (const std::exception& ex) {
|
||||
LOG_DEBUG("Error broadcasting property list change: %s", ex.what());
|
||||
}
|
||||
}
|
||||
|
||||
void BroadcastPlayKeyUpdate(uint32_t playKeyId) {
|
||||
try {
|
||||
nlohmann::json message = {
|
||||
{"event", "play_key_update"},
|
||||
{"playKeyId", playKeyId}
|
||||
};
|
||||
Game::web.SendWSMessage("play_key_update", message);
|
||||
} catch (const std::exception& ex) {
|
||||
LOG_DEBUG("Error broadcasting play key update: %s", ex.what());
|
||||
}
|
||||
}
|
||||
|
||||
void BroadcastPlayKeysTableUpdate() {
|
||||
try {
|
||||
nlohmann::json message = {
|
||||
{"event", "play_keys_table_update"},
|
||||
{"message", "Play Keys table has been updated"}
|
||||
};
|
||||
Game::web.SendWSMessage("play_keys_table_update", message);
|
||||
} catch (const std::exception& ex) {
|
||||
LOG_DEBUG("Error broadcasting play keys table update: %s", ex.what());
|
||||
}
|
||||
}
|
||||
|
||||
void BroadcastPlayKeyListChanged() {
|
||||
try {
|
||||
nlohmann::json message = {
|
||||
{"event", "play_key_list_changed"},
|
||||
{"totalPlayKeys", 0} // TODO: Get from database
|
||||
};
|
||||
Game::web.SendWSMessage("play_key_list_changed", message);
|
||||
} catch (const std::exception& ex) {
|
||||
LOG_DEBUG("Error broadcasting play key list change: %s", ex.what());
|
||||
}
|
||||
}
|
||||
|
||||
void BroadcastBugReportUpdate(uint32_t bugReportId) {
|
||||
try {
|
||||
nlohmann::json message = {
|
||||
{"event", "bug_report_update"},
|
||||
{"bugReportId", bugReportId}
|
||||
};
|
||||
Game::web.SendWSMessage("bug_report_update", message);
|
||||
} catch (const std::exception& ex) {
|
||||
LOG_DEBUG("Error broadcasting bug report update: %s", ex.what());
|
||||
}
|
||||
}
|
||||
|
||||
void BroadcastBugReportsTableUpdate() {
|
||||
try {
|
||||
nlohmann::json message = {
|
||||
{"event", "bug_reports_table_update"},
|
||||
{"message", "Bug Reports table has been updated"}
|
||||
};
|
||||
Game::web.SendWSMessage("bug_reports_table_update", message);
|
||||
} catch (const std::exception& ex) {
|
||||
LOG_DEBUG("Error broadcasting bug reports table update: %s", ex.what());
|
||||
}
|
||||
}
|
||||
31
dDashboardServer/routes/WSRoutes.h
Normal file
31
dDashboardServer/routes/WSRoutes.h
Normal file
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include "json.hpp"
|
||||
#include <cstdint>
|
||||
|
||||
void RegisterWSRoutes();
|
||||
void BroadcastDashboardUpdate();
|
||||
|
||||
// Account broadcasts
|
||||
void BroadcastAccountUpdate(uint32_t accountId);
|
||||
void BroadcastAccountsTableUpdate();
|
||||
void BroadcastAccountListChanged();
|
||||
|
||||
// Character broadcasts
|
||||
void BroadcastCharacterUpdate(uint32_t characterId);
|
||||
void BroadcastCharactersTableUpdate();
|
||||
void BroadcastCharacterListChanged();
|
||||
|
||||
// Property broadcasts
|
||||
void BroadcastPropertyUpdate(uint32_t propertyId);
|
||||
void BroadcastPropertiesTableUpdate();
|
||||
void BroadcastPropertyListChanged();
|
||||
|
||||
// Play Key broadcasts
|
||||
void BroadcastPlayKeyUpdate(uint32_t playKeyId);
|
||||
void BroadcastPlayKeysTableUpdate();
|
||||
void BroadcastPlayKeyListChanged();
|
||||
|
||||
// Bug Report broadcasts
|
||||
void BroadcastBugReportUpdate(uint32_t bugReportId);
|
||||
void BroadcastBugReportsTableUpdate();
|
||||
285
dDashboardServer/static/css/dashboard.css
Normal file
285
dDashboardServer/static/css/dashboard.css
Normal file
@@ -0,0 +1,285 @@
|
||||
/* DarkflameServer Dashboard - Minimal Branding */
|
||||
|
||||
:root {
|
||||
--primary-color: #667eea;
|
||||
--secondary-color: #764ba2;
|
||||
--dark-bg: #0f0f1e;
|
||||
--card-bg: rgba(26, 26, 46, 0.8);
|
||||
}
|
||||
|
||||
body {
|
||||
background: linear-gradient(135deg, var(--dark-bg) 0%, #1a1a2e 100%);
|
||||
display: grid;
|
||||
grid-template-columns: 280px 1fr;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Grid Layout */
|
||||
.navbar.flex-column {
|
||||
grid-row: 1 / -1;
|
||||
grid-column: 1;
|
||||
background: linear-gradient(180deg, #1a1a2e 0%, #16213e 100%);
|
||||
box-shadow: 2px 0 15px rgba(0, 0, 0, 0.3);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
main {
|
||||
grid-column: 2;
|
||||
grid-row: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.navbar:not(.flex-column) {
|
||||
grid-column: 1 / -1;
|
||||
grid-row: 1;
|
||||
background: rgba(26, 26, 46, 0.95);
|
||||
border-bottom: 1px solid rgba(102, 126, 234, 0.2);
|
||||
}
|
||||
|
||||
footer {
|
||||
grid-column: 1 / -1;
|
||||
grid-row: 3;
|
||||
background: rgba(26, 26, 46, 0.95);
|
||||
border-top: 1px solid rgba(102, 126, 234, 0.2);
|
||||
}
|
||||
|
||||
/* Sidebar Navigation */
|
||||
.navbar.flex-column .nav-link {
|
||||
border-left: 3px solid transparent;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.navbar.flex-column .nav-link:hover {
|
||||
background-color: rgba(102, 126, 234, 0.15);
|
||||
border-left-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.navbar.flex-column .nav-link.active {
|
||||
background-color: rgba(102, 126, 234, 0.25);
|
||||
border-left-color: var(--primary-color);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
.card {
|
||||
background: var(--card-bg);
|
||||
border: 1px solid rgba(102, 126, 234, 0.2);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
border-color: rgba(102, 126, 234, 0.4);
|
||||
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.15);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background: linear-gradient(135deg, rgba(102, 126, 234, 0.2) 0%, rgba(118, 75, 162, 0.2) 100%);
|
||||
border-bottom: 1px solid rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
.table-dark {
|
||||
border-color: rgba(102, 126, 234, 0.2);
|
||||
}
|
||||
|
||||
.table-dark thead th {
|
||||
background: rgba(102, 126, 234, 0.15);
|
||||
border-bottom: 2px solid rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.table-dark tbody tr:hover {
|
||||
background-color: rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
/* DataTables */
|
||||
.dataTables_wrapper .dataTables_filter input,
|
||||
.dataTables_wrapper .dataTables_paginate .paginate_button {
|
||||
background-color: rgba(42, 42, 58, 0.8);
|
||||
border: 1px solid rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.dataTables_wrapper .dataTables_paginate .paginate_button.current {
|
||||
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
|
||||
}
|
||||
|
||||
/* Custom Badge Styles */
|
||||
.badge-banned {
|
||||
background-color: #dc3545;
|
||||
}
|
||||
|
||||
.badge-locked {
|
||||
background-color: #fd7e14;
|
||||
}
|
||||
|
||||
.badge-gm {
|
||||
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
|
||||
}
|
||||
|
||||
.badge-active {
|
||||
background-color: #28a745;
|
||||
}
|
||||
|
||||
.badge-inactive {
|
||||
background-color: #6c757d;
|
||||
}
|
||||
|
||||
.badge-approved {
|
||||
background-color: #28a745;
|
||||
}
|
||||
|
||||
.badge-pending {
|
||||
background-color: #ffc107;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.badge-primary {
|
||||
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
|
||||
}
|
||||
|
||||
/* Status Indicators */
|
||||
.status.online::before,
|
||||
.status.offline::before {
|
||||
content: '';
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.status.online::before {
|
||||
background: #4caf50;
|
||||
}
|
||||
|
||||
.status.offline::before {
|
||||
background: #f44336;
|
||||
}
|
||||
|
||||
/* Detail Grid */
|
||||
.detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
border-left: 3px solid var(--primary-color);
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: 0.875rem;
|
||||
color: #999;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-size: 1rem;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s;
|
||||
display: inline-block;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.back-link:hover {
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
/* Container Wrappers - Use Bootstrap spacing */
|
||||
.accounts-container,
|
||||
.characters-container,
|
||||
.bug-reports-container,
|
||||
.properties-container,
|
||||
.play-keys-container,
|
||||
.account-view-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 991.98px) {
|
||||
body {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.navbar.flex-column {
|
||||
grid-column: 1 / -1;
|
||||
grid-row: 1;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
overflow-y: visible;
|
||||
}
|
||||
|
||||
.navbar.flex-column .nav-link {
|
||||
border-left: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
|
||||
.navbar.flex-column .nav-link:hover,
|
||||
.navbar.flex-column .nav-link.active {
|
||||
border-bottom-color: var(--primary-color);
|
||||
}
|
||||
|
||||
main {
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
footer {
|
||||
grid-column: 1;
|
||||
grid-row: 3;
|
||||
}
|
||||
|
||||
.detail-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.table-dark {
|
||||
font-size: 12px;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.table-dark th,
|
||||
.table-dark td {
|
||||
white-space: nowrap;
|
||||
padding: 8px 6px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 575.98px) {
|
||||
.navbar.flex-column .nav-link {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
296
dDashboardServer/static/css/login.css
Normal file
296
dDashboardServer/static/css/login.css
Normal file
@@ -0,0 +1,296 @@
|
||||
/* Modern Login Page Styling */
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body.d-flex.bg-dark.text-white {
|
||||
background: linear-gradient(135deg, #0f0f1e 0%, #1a1a2e 50%, #16213e 100%);
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Hide elements not needed on login */
|
||||
body .navbar,
|
||||
body > footer {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body .container-fluid {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.login-wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
width: 100%;
|
||||
max-width: 1100px;
|
||||
min-height: 600px;
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Left side - Branding */
|
||||
.login-left {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 60px 40px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.login-branding {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-branding .logo {
|
||||
font-size: 64px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.login-branding h1 {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 8px;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.login-branding p {
|
||||
font-size: 14px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.login-info {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-info h3 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.login-info p {
|
||||
font-size: 14px;
|
||||
opacity: 0.85;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Right side - Form */
|
||||
.login-right {
|
||||
padding: 60px 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.login-form-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.login-form-wrapper h2 {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #1a1a2e;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
padding: 12px 16px;
|
||||
font-size: 14px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
font-family: inherit;
|
||||
color: #1a1a2e;
|
||||
}
|
||||
|
||||
.form-group input::placeholder {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
.form-focus {
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, transparent, #667eea, transparent);
|
||||
border-radius: 2px;
|
||||
margin-top: -2px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.form-group input:focus ~ .form-focus {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.form-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 10px 0 20px 0;
|
||||
}
|
||||
|
||||
.form-checkbox input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
accent-color: #667eea;
|
||||
}
|
||||
|
||||
.form-checkbox label {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
}
|
||||
|
||||
.btn-login {
|
||||
padding: 12px 24px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.btn-login:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.btn-login:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.btn-login:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
border-top-color: white;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.alert-box {
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.alert-box.alert-danger {
|
||||
background-color: #fee;
|
||||
color: #c33;
|
||||
border: 1px solid #fcc;
|
||||
}
|
||||
|
||||
.alert-box.alert-success {
|
||||
background-color: #efe;
|
||||
color: #3c3;
|
||||
border: 1px solid #cfc;
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
text-align: center;
|
||||
margin-top: 30px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.login-footer p {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.login-wrapper {
|
||||
grid-template-columns: 1fr;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.login-left {
|
||||
padding: 40px 30px;
|
||||
justify-content: center;
|
||||
min-height: 250px;
|
||||
}
|
||||
|
||||
.login-right {
|
||||
padding: 40px 30px;
|
||||
}
|
||||
|
||||
.login-branding .logo {
|
||||
font-size: 48px;
|
||||
}
|
||||
|
||||
.login-branding h1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.login-form-wrapper h2 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
254
dDashboardServer/static/js/dashboard.js
Normal file
254
dDashboardServer/static/js/dashboard.js
Normal file
@@ -0,0 +1,254 @@
|
||||
let ws = null;
|
||||
let reconnectAttempts = 0;
|
||||
const maxReconnectAttempts = 5;
|
||||
const reconnectDelay = 3000;
|
||||
|
||||
// Helper function to get cookie value
|
||||
function getCookie(name) {
|
||||
const nameEQ = name + '=';
|
||||
const cookies = document.cookie.split(';');
|
||||
for (let cookie of cookies) {
|
||||
cookie = cookie.trim();
|
||||
if (cookie.indexOf(nameEQ) === 0) {
|
||||
return decodeURIComponent(cookie.substring(nameEQ.length));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Helper function to delete cookie
|
||||
function deleteCookie(name) {
|
||||
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; SameSite=Strict`;
|
||||
}
|
||||
|
||||
// Check authentication on page load
|
||||
function checkAuthentication() {
|
||||
// Check localStorage first (most secure)
|
||||
let token = localStorage.getItem('dashboardToken');
|
||||
|
||||
// Fallback to cookie if localStorage empty
|
||||
if (!token) {
|
||||
token = getCookie('dashboardToken');
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
// Redirect to login if no token
|
||||
window.location.href = '/login';
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify token is valid (asynchronous)
|
||||
fetch('/api/auth/verify', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token: token })
|
||||
})
|
||||
.then(res => {
|
||||
if (!res.ok) {
|
||||
console.error('Verify endpoint returned:', res.status);
|
||||
throw new Error(`HTTP ${res.status}`);
|
||||
}
|
||||
return res.json();
|
||||
})
|
||||
.then(data => {
|
||||
console.log('Token verification response:', data);
|
||||
if (!data.valid) {
|
||||
// Token is invalid/expired, delete cookies and redirect to login
|
||||
console.log('Token verification failed, redirecting to login');
|
||||
deleteCookie('dashboardToken');
|
||||
deleteCookie('gmLevel');
|
||||
localStorage.removeItem('dashboardToken');
|
||||
window.location.href = '/login';
|
||||
} else {
|
||||
// Update UI with username
|
||||
console.log('Token verified, user:', data.username);
|
||||
const usernameElement = document.querySelector('.username');
|
||||
if (usernameElement) {
|
||||
usernameElement.textContent = data.username || 'User';
|
||||
} else {
|
||||
console.warn('Username element not found in DOM');
|
||||
}
|
||||
// Now that verification is complete, connect to WebSocket
|
||||
setTimeout(() => {
|
||||
console.log('Starting WebSocket connection');
|
||||
connectWebSocket();
|
||||
}, 100);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Token verification error:', err);
|
||||
// Network error - log but don't redirect immediately
|
||||
// This prevents redirect loops on network issues
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get token from localStorage or cookie
|
||||
function getAuthToken() {
|
||||
let token = localStorage.getItem('dashboardToken');
|
||||
if (!token) {
|
||||
token = getCookie('dashboardToken');
|
||||
}
|
||||
console.log('getAuthToken called, token available:', !!token);
|
||||
return token;
|
||||
}
|
||||
|
||||
// Logout function
|
||||
function logout() {
|
||||
deleteCookie('dashboardToken');
|
||||
deleteCookie('gmLevel');
|
||||
localStorage.removeItem('dashboardToken');
|
||||
window.location.href = '/login';
|
||||
}
|
||||
|
||||
function connectWebSocket() {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const token = getAuthToken();
|
||||
if (!token) {
|
||||
console.error('No token available for WebSocket connection');
|
||||
window.location.href = '/login';
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`WebSocket connection attempt ${reconnectAttempts + 1}/${maxReconnectAttempts}`);
|
||||
|
||||
// Connect to WebSocket without token in URL (token is in cookies)
|
||||
const wsUrl = `${protocol}//${window.location.host}/ws`;
|
||||
console.log(`Connecting to WebSocket: ${wsUrl}`);
|
||||
|
||||
try {
|
||||
ws = new WebSocket(wsUrl);
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('WebSocket connected');
|
||||
reconnectAttempts = 0;
|
||||
|
||||
// Subscribe to dashboard updates
|
||||
ws.send(JSON.stringify({
|
||||
event: 'subscribe',
|
||||
subscription: 'dashboard_update'
|
||||
}));
|
||||
|
||||
// Mark connection as ready for other handlers
|
||||
wsConnectionReady = true;
|
||||
|
||||
// Initialize real-time table manager
|
||||
if (typeof realtimeManager !== 'undefined') {
|
||||
realtimeManager.Initialize();
|
||||
}
|
||||
|
||||
document.getElementById('connection-status')?.remove();
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
// Handle subscription confirmation
|
||||
if (data.subscribed) {
|
||||
console.log('Subscribed to:', data.subscribed);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle dashboard updates
|
||||
if (data.event === 'dashboard_update') {
|
||||
updateDashboard(data);
|
||||
}
|
||||
|
||||
// Route to generic realtime handler for all table updates
|
||||
if (typeof realtimeManager !== 'undefined') {
|
||||
realtimeManager.HandleMessage(data);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error parsing WebSocket message:', error);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log('WebSocket disconnected');
|
||||
ws = null;
|
||||
|
||||
// Show connection status
|
||||
showConnectionStatus('Disconnected - Attempting to reconnect...');
|
||||
|
||||
// Attempt to reconnect with exponential backoff
|
||||
if (reconnectAttempts < maxReconnectAttempts) {
|
||||
reconnectAttempts++;
|
||||
const backoffDelay = reconnectDelay * Math.pow(2, reconnectAttempts - 1);
|
||||
console.log(`Reconnecting in ${backoffDelay}ms (attempt ${reconnectAttempts}/${maxReconnectAttempts})`);
|
||||
setTimeout(connectWebSocket, backoffDelay);
|
||||
} else {
|
||||
console.error('Max reconnection attempts reached');
|
||||
showConnectionStatus('Connection lost - Reload page to reconnect');
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to create WebSocket:', error);
|
||||
showConnectionStatus('Failed to connect - Reload page to retry');
|
||||
}
|
||||
}
|
||||
|
||||
function showConnectionStatus(message) {
|
||||
let statusEl = document.getElementById('connection-status');
|
||||
if (!statusEl) {
|
||||
statusEl = document.createElement('div');
|
||||
statusEl.id = 'connection-status';
|
||||
statusEl.style.cssText = 'position: fixed; top: 10px; right: 10px; background: #f44336; color: white; padding: 10px 20px; border-radius: 4px; z-index: 1000;';
|
||||
document.body.appendChild(statusEl);
|
||||
}
|
||||
statusEl.textContent = message;
|
||||
}
|
||||
|
||||
function updateDashboard(data) {
|
||||
// Update server status
|
||||
if (data.auth) {
|
||||
document.getElementById('auth-status').textContent = data.auth.online ? 'Online' : 'Offline';
|
||||
document.getElementById('auth-status').className = 'status ' + (data.auth.online ? 'online' : 'offline');
|
||||
}
|
||||
|
||||
if (data.chat) {
|
||||
document.getElementById('chat-status').textContent = data.chat.online ? 'Online' : 'Offline';
|
||||
document.getElementById('chat-status').className = 'status ' + (data.chat.online ? 'online' : 'offline');
|
||||
}
|
||||
|
||||
// Update world instances
|
||||
if (data.worlds) {
|
||||
document.getElementById('world-count').textContent = data.worlds.length;
|
||||
|
||||
const worldList = document.getElementById('world-list');
|
||||
if (data.worlds.length === 0) {
|
||||
worldList.innerHTML = '<div class="loading">No active world instances</div>';
|
||||
} else {
|
||||
worldList.innerHTML = data.worlds.map(world => `
|
||||
<div class="world-item">
|
||||
<h3>Zone ${world.mapID} - Instance ${world.instanceID}</h3>
|
||||
<div class="world-detail">Clone ID: ${world.cloneID}</div>
|
||||
<div class="world-detail">Players: ${world.players}</div>
|
||||
<div class="world-detail">Type: ${world.isPrivate ? 'Private' : 'Public'}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
}
|
||||
|
||||
// Update statistics
|
||||
if (data.stats) {
|
||||
if (data.stats.onlinePlayers !== undefined) {
|
||||
document.getElementById('online-players').textContent = data.stats.onlinePlayers;
|
||||
}
|
||||
if (data.stats.totalAccounts !== undefined) {
|
||||
document.getElementById('total-accounts').textContent = data.stats.totalAccounts;
|
||||
}
|
||||
if (data.stats.totalCharacters !== undefined) {
|
||||
document.getElementById('total-characters').textContent = data.stats.totalCharacters;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Connect on page load
|
||||
connectWebSocket();
|
||||
99
dDashboardServer/static/js/login.js
Normal file
99
dDashboardServer/static/js/login.js
Normal file
@@ -0,0 +1,99 @@
|
||||
// Check if user is already logged in
|
||||
function checkExistingToken() {
|
||||
const token = localStorage.getItem('dashboardToken');
|
||||
if (token) {
|
||||
verifyTokenAndRedirect(token);
|
||||
}
|
||||
}
|
||||
|
||||
function verifyTokenAndRedirect(token) {
|
||||
fetch('/api/auth/verify', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token: token })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.valid) {
|
||||
window.location.href = '/';
|
||||
}
|
||||
})
|
||||
.catch(err => console.error('Token verification failed:', err));
|
||||
}
|
||||
|
||||
function showAlert(message, type) {
|
||||
const alert = document.getElementById('alert');
|
||||
alert.textContent = message;
|
||||
alert.className = 'alert';
|
||||
if (type === 'error') {
|
||||
alert.classList.add('alert-danger');
|
||||
} else if (type === 'success') {
|
||||
alert.classList.add('alert-success');
|
||||
}
|
||||
alert.style.display = 'block';
|
||||
}
|
||||
|
||||
// Wait for DOM to be ready
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const loginForm = document.getElementById('loginForm');
|
||||
if (!loginForm) {
|
||||
console.error('Login form not found');
|
||||
return;
|
||||
}
|
||||
|
||||
loginForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const username = document.getElementById('username').value;
|
||||
const password = document.getElementById('password').value;
|
||||
const rememberMe = document.getElementById('rememberMe').checked;
|
||||
|
||||
// Validate input
|
||||
if (!username || !password) {
|
||||
showAlert('Username and password are required', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (password.length > 40) {
|
||||
showAlert('Password exceeds maximum length (40 characters)', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
document.getElementById('loading').style.display = 'inline-block';
|
||||
document.getElementById('loginBtn').disabled = true;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password, rememberMe })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
// Store token in localStorage (also set as cookie for API calls)
|
||||
localStorage.setItem('dashboardToken', data.token);
|
||||
document.cookie = `dashboardToken=${data.token}; path=/; SameSite=Strict`;
|
||||
showAlert('Login successful! Redirecting...', 'success');
|
||||
|
||||
// Redirect after a short delay (no token in URL)
|
||||
setTimeout(() => {
|
||||
window.location.href = '/';
|
||||
}, 1000);
|
||||
} else {
|
||||
showAlert(data.message || 'Login failed', 'error');
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
document.getElementById('loginBtn').disabled = false;
|
||||
}
|
||||
} catch (error) {
|
||||
showAlert('Network error: ' + error.message, 'error');
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
document.getElementById('loginBtn').disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Check existing token on page load
|
||||
checkExistingToken();
|
||||
});
|
||||
339
dDashboardServer/static/js/realtime.js
Normal file
339
dDashboardServer/static/js/realtime.js
Normal file
@@ -0,0 +1,339 @@
|
||||
/**
|
||||
* Generic Real-time WebSocket Updates for Dashboard Tables
|
||||
* Provides reactive, non-blocking updates for all admin tables
|
||||
*/
|
||||
|
||||
const realtimeManager = {
|
||||
tables: {},
|
||||
currentEntityId: null,
|
||||
wsReady: false,
|
||||
|
||||
/**
|
||||
* Initialize real-time updates for a DataTable
|
||||
* @param {string} entityType - Type of entity (account, character, property, etc.)
|
||||
* @param {jQuery} tableElement - jQuery DataTable element
|
||||
*/
|
||||
RegisterTable(entityType, tableElement) {
|
||||
if (!tableElement || !tableElement.DataTable) {
|
||||
console.warn(`Invalid table element for entity type: ${entityType}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.tables[entityType] = {
|
||||
element: tableElement,
|
||||
table: tableElement.DataTable(),
|
||||
lastUpdate: Date.now()
|
||||
};
|
||||
|
||||
console.log(`Registered real-time table for entity type: ${entityType}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize WebSocket listeners for all entity types
|
||||
*/
|
||||
Initialize() {
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||
console.warn('WebSocket not ready for realtime initialization');
|
||||
return;
|
||||
}
|
||||
|
||||
this.wsReady = true;
|
||||
console.log('Initialized real-time WebSocket listeners');
|
||||
this.SubscribeToAll();
|
||||
},
|
||||
|
||||
/**
|
||||
* Subscribe to all registered entity types
|
||||
*/
|
||||
SubscribeToAll() {
|
||||
const subscriptions = [
|
||||
'account_update',
|
||||
'accounts_table_update',
|
||||
'account_list_changed',
|
||||
'character_update',
|
||||
'characters_table_update',
|
||||
'character_list_changed',
|
||||
'property_update',
|
||||
'properties_table_update',
|
||||
'property_list_changed',
|
||||
'play_key_update',
|
||||
'play_keys_table_update',
|
||||
'play_key_list_changed',
|
||||
'bug_report_update',
|
||||
'bug_reports_table_update'
|
||||
];
|
||||
|
||||
for (const subscription of subscriptions) {
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) break;
|
||||
|
||||
ws.send(JSON.stringify({
|
||||
event: 'subscribe',
|
||||
subscription: subscription
|
||||
}));
|
||||
}
|
||||
|
||||
console.log(`Subscribed to ${subscriptions.length} real-time events`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle incoming WebSocket messages
|
||||
*/
|
||||
HandleMessage(data) {
|
||||
if (!data.event) return;
|
||||
|
||||
// Route to appropriate handler based on event type
|
||||
if (data.event.includes('account')) {
|
||||
this.HandleAccountEvent(data);
|
||||
} else if (data.event.includes('character')) {
|
||||
this.HandleCharacterEvent(data);
|
||||
} else if (data.event.includes('property')) {
|
||||
this.HandlePropertyEvent(data);
|
||||
} else if (data.event.includes('play_key')) {
|
||||
this.HandlePlayKeyEvent(data);
|
||||
} else if (data.event.includes('bug_report')) {
|
||||
this.HandleBugReportEvent(data);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle account-related events
|
||||
*/
|
||||
HandleAccountEvent(data) {
|
||||
if (data.event === 'account_update') {
|
||||
this.UpdateRow('account', data.accountId || data.id, data);
|
||||
// Also update detail panel if viewing this account
|
||||
this.UpdateDetailPanel('account', data);
|
||||
} else if (data.event === 'accounts_table_update') {
|
||||
this.ReloadTable('account');
|
||||
} else if (data.event === 'account_list_changed') {
|
||||
this.UpdateListCount('account', data.totalAccounts);
|
||||
this.ReloadTable('account', true);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle character-related events
|
||||
*/
|
||||
HandleCharacterEvent(data) {
|
||||
if (data.event === 'character_update') {
|
||||
this.UpdateRow('character', data.characterId || data.id, data);
|
||||
} else if (data.event === 'characters_table_update') {
|
||||
this.ReloadTable('character');
|
||||
} else if (data.event === 'character_list_changed') {
|
||||
this.UpdateListCount('character', data.totalCharacters);
|
||||
this.ReloadTable('character', true);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle property-related events
|
||||
*/
|
||||
HandlePropertyEvent(data) {
|
||||
if (data.event === 'property_update') {
|
||||
this.UpdateRow('property', data.propertyId || data.id, data);
|
||||
} else if (data.event === 'properties_table_update') {
|
||||
this.ReloadTable('property');
|
||||
} else if (data.event === 'property_list_changed') {
|
||||
this.UpdateListCount('property', data.totalProperties);
|
||||
this.ReloadTable('property', true);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle play key-related events
|
||||
*/
|
||||
HandlePlayKeyEvent(data) {
|
||||
if (data.event === 'play_key_update') {
|
||||
this.UpdateRow('play_key', data.playKeyId || data.id, data);
|
||||
} else if (data.event === 'play_keys_table_update') {
|
||||
this.ReloadTable('play_key');
|
||||
} else if (data.event === 'play_key_list_changed') {
|
||||
this.UpdateListCount('play_key', data.totalPlayKeys);
|
||||
this.ReloadTable('play_key', true);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle bug report-related events
|
||||
*/
|
||||
HandleBugReportEvent(data) {
|
||||
if (data.event === 'bug_report_update') {
|
||||
this.UpdateRow('bug_report', data.bugReportId || data.id, data);
|
||||
} else if (data.event === 'bug_reports_table_update') {
|
||||
this.ReloadTable('bug_report');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update a single row in a DataTable
|
||||
*/
|
||||
UpdateRow(entityType, entityId, data) {
|
||||
if (!this.tables[entityType]) {
|
||||
console.debug(`No table registered for entity type: ${entityType}`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const table = this.tables[entityType].table;
|
||||
const rows = table.rows().nodes();
|
||||
|
||||
// Find and invalidate the matching row
|
||||
let found = false;
|
||||
for (let row of rows) {
|
||||
const rowData = table.row(row).data();
|
||||
if (rowData && (rowData.id === entityId || rowData.accountId === entityId ||
|
||||
rowData.characterId === entityId || rowData.propertyId === entityId ||
|
||||
rowData.playKeyId === entityId)) {
|
||||
table.row(row).invalidate().draw(false);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (found) {
|
||||
this.ShowToast('Updated', `${entityType} data has been refreshed`, 'info');
|
||||
}
|
||||
} catch (e) {
|
||||
console.debug(`Error updating row for ${entityType}:`, e);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Reload a table without page change
|
||||
*/
|
||||
ReloadTable(entityType, resetPage = false) {
|
||||
if (!this.tables[entityType]) {
|
||||
console.debug(`No table registered for entity type: ${entityType}`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const table = this.tables[entityType].table;
|
||||
|
||||
if (resetPage) {
|
||||
table.page(0).ajax.reload();
|
||||
} else {
|
||||
table.ajax.reload(null, false);
|
||||
}
|
||||
|
||||
this.ShowToast('Updated', `${entityType} list has been refreshed`, 'info');
|
||||
} catch (e) {
|
||||
console.error(`Error reloading table for ${entityType}:`, e);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update list count display
|
||||
*/
|
||||
UpdateListCount(entityType, count) {
|
||||
try {
|
||||
const selector = `.${entityType}-count`;
|
||||
const countEl = document.querySelector(selector);
|
||||
if (countEl) {
|
||||
countEl.textContent = count;
|
||||
}
|
||||
} catch (e) {
|
||||
console.debug(`Error updating count for ${entityType}:`, e);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Set current entity being viewed for detail updates
|
||||
*/
|
||||
SetCurrentEntity(entityType, entityId) {
|
||||
this.currentEntity = { type: entityType, id: entityId };
|
||||
console.log(`Viewing ${entityType} ID: ${entityId}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear current entity
|
||||
*/
|
||||
ClearCurrentEntity() {
|
||||
this.currentEntity = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update detail panel for current entity
|
||||
*/
|
||||
UpdateDetailPanel(entityType, data) {
|
||||
if (!this.currentEntity || this.currentEntity.type !== entityType ||
|
||||
this.currentEntity.id !== (data.id || data.accountId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Update all elements with data-field attributes
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
const selectors = [
|
||||
`.detail-${key}`,
|
||||
`[data-field="${key}"]`,
|
||||
`.${entityType}-${key}`
|
||||
];
|
||||
|
||||
for (const selector of selectors) {
|
||||
const elements = document.querySelectorAll(selector);
|
||||
for (const el of elements) {
|
||||
this.UpdateElement(el, key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.ShowToast('Updated', `${entityType} data refreshed`, 'info');
|
||||
} catch (e) {
|
||||
console.error(`Error updating detail panel:`, e);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update an individual element with new value
|
||||
*/
|
||||
UpdateElement(element, fieldName, value) {
|
||||
if (!element) return;
|
||||
|
||||
try {
|
||||
// Handle different field types
|
||||
if (fieldName.includes('banned') || fieldName.includes('banned') || fieldName.includes('locked')) {
|
||||
element.textContent = value ? 'Yes' : 'No';
|
||||
element.className = `badge ${value ? 'bg-danger' : 'bg-success'}`;
|
||||
} else if (fieldName.includes('count') || fieldName.includes('level')) {
|
||||
element.textContent = value || '-';
|
||||
} else if (fieldName.includes('date') || fieldName.includes('created') || fieldName.includes('updated')) {
|
||||
if (value) {
|
||||
element.textContent = new Date(value * 1000).toLocaleString();
|
||||
}
|
||||
} else {
|
||||
element.textContent = value || '-';
|
||||
}
|
||||
} catch (e) {
|
||||
console.debug(`Error updating element for field ${fieldName}:`, e);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Show toast notification
|
||||
*/
|
||||
ShowToast(title, message, type = 'info') {
|
||||
try {
|
||||
const toastEl = document.createElement('div');
|
||||
toastEl.className = `alert alert-${type === 'error' ? 'danger' : type} alert-dismissible fade show`;
|
||||
toastEl.setAttribute('role', 'alert');
|
||||
toastEl.style.cssText = 'position: fixed; bottom: 20px; right: 20px; z-index: 9999; min-width: 300px;';
|
||||
|
||||
toastEl.innerHTML = `
|
||||
<strong>${title}</strong><br>${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
`;
|
||||
|
||||
document.body.appendChild(toastEl);
|
||||
|
||||
setTimeout(() => {
|
||||
toastEl.remove();
|
||||
}, 4000);
|
||||
} catch (e) {
|
||||
console.log(`[${type}] ${title}: ${message}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Export to global scope
|
||||
window.realtimeManager = realtimeManager;
|
||||
154
dDashboardServer/templates/account-view.jinja2
Normal file
154
dDashboardServer/templates/account-view.jinja2
Normal file
@@ -0,0 +1,154 @@
|
||||
{% extends "base.jinja2" %}
|
||||
|
||||
{% block title %}Account - DarkflameServer{% endblock %}
|
||||
|
||||
{% block css %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="account-view-container">
|
||||
<div class="container-fluid">
|
||||
<a href="/accounts" class="back-link">← Back to Accounts</a>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>Account #{{ account.id }} - <span class="account-name">{{ account.name }}</span></h2>
|
||||
<p class="text-muted">View account details and manage settings</p>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="detail-grid">
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">Account ID</div>
|
||||
<div class="detail-value">{{ account.id }}</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">Username</div>
|
||||
<div class="detail-value"><span class="account-name">{{ account.name }}</span></div>
|
||||
</div>
|
||||
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">Created</div>
|
||||
<div class="detail-value">{{ account.created_at }}</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">GM Level</div>
|
||||
<div class="detail-value">
|
||||
{% if account.gm_level > 0 %}
|
||||
<span class="account-gm-level badge badge-gm">GM {{ account.gm_level }}</span>
|
||||
{% else %}
|
||||
<span class="account-gm-level badge badge-inactive">User</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">Ban Status</div>
|
||||
<div class="detail-value">
|
||||
{% if account.banned %}
|
||||
<span class="account-banned badge badge-banned">BANNED</span>
|
||||
{% else %}
|
||||
<span class="account-banned badge badge-active">Active</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">Lock Status</div>
|
||||
<div class="detail-value">
|
||||
{% if account.locked %}
|
||||
<span class="account-locked badge badge-locked">LOCKED</span>
|
||||
{% else %}
|
||||
<span class="account-locked badge badge-active">Unlocked</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">Mute Expires</div>
|
||||
<div class="detail-value">
|
||||
<span class="account-mute-expire">
|
||||
{% if account.mute_expire > 0 %}
|
||||
Muted until {{ account.mute_expire }}
|
||||
{% else %}
|
||||
Not muted
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<button class="btn btn-primary" onclick="EditAccount()">Edit Account</button>
|
||||
{% if not account.banned %}
|
||||
<button class="btn btn-danger" onclick="BanAccount()">Ban Account</button>
|
||||
{% else %}
|
||||
<button class="btn btn-secondary" onclick="UnbanAccount()">Unban Account</button>
|
||||
{% endif %}
|
||||
{% if not account.locked %}
|
||||
<button class="btn btn-danger" onclick="LockAccount()">Lock Account</button>
|
||||
{% else %}
|
||||
<button class="btn btn-secondary" onclick="UnlockAccount()">Unlock Account</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TODO: Add modals for edit, ban, lock operations -->
|
||||
<!-- TODO: Add character list for this account -->
|
||||
<!-- TODO: Add login history -->
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="/js/realtime.js"></script>
|
||||
<script>
|
||||
// Set current account for real-time detail panel updates
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
realtimeManager.SetCurrentEntity('account', {{ account.id }});
|
||||
|
||||
// Clean up when leaving the page
|
||||
window.addEventListener('beforeunload', () => {
|
||||
realtimeManager.ClearCurrentEntity();
|
||||
});
|
||||
});
|
||||
|
||||
function EditAccount() {
|
||||
alert("Edit functionality coming soon");
|
||||
// TODO: Open edit modal
|
||||
}
|
||||
|
||||
function BanAccount() {
|
||||
if (confirm("Are you sure you want to ban this account?")) {
|
||||
alert("Ban functionality coming soon");
|
||||
// TODO: Call ban API endpoint
|
||||
// After successful ban, call: BroadcastAccountUpdate({{ account.id }})
|
||||
}
|
||||
}
|
||||
|
||||
function UnbanAccount() {
|
||||
if (confirm("Are you sure you want to unban this account?")) {
|
||||
alert("Unban functionality coming soon");
|
||||
// TODO: Call unban API endpoint
|
||||
// After successful unban, call: BroadcastAccountUpdate({{ account.id }})
|
||||
}
|
||||
}
|
||||
|
||||
function LockAccount() {
|
||||
if (confirm("Are you sure you want to lock this account?")) {
|
||||
alert("Lock functionality coming soon");
|
||||
// TODO: Call lock API endpoint
|
||||
// After successful lock, call: BroadcastAccountUpdate({{ account.id }})
|
||||
}
|
||||
}
|
||||
|
||||
function UnlockAccount() {
|
||||
if (confirm("Are you sure you want to unlock this account?")) {
|
||||
alert("Unlock functionality coming soon");
|
||||
// TODO: Call unlock API endpoint
|
||||
// After successful unlock, call: BroadcastAccountUpdate({{ account.id }})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
153
dDashboardServer/templates/accounts.jinja2
Normal file
153
dDashboardServer/templates/accounts.jinja2
Normal file
@@ -0,0 +1,153 @@
|
||||
{% extends "base.jinja2" %}
|
||||
|
||||
{% block title %}Accounts - DarkflameServer{% endblock %}
|
||||
|
||||
{% block css %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="accounts-container">
|
||||
<div class="container-fluid">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>Accounts</h2>
|
||||
<p class="text-muted">View and manage user accounts - <span class="account-count">0</span> total</p>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table id="accountsTable" class="table table-dark table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Username</th>
|
||||
<th>Banned</th>
|
||||
<th>Locked</th>
|
||||
<th>GM Level</th>
|
||||
<th>Mute Expires</th>
|
||||
<th>Created</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Data populated by DataTables -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="/js/realtime.js"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Initialize DataTable with server-side processing
|
||||
const table = $('#accountsTable').DataTable({
|
||||
processing: true,
|
||||
serverSide: true,
|
||||
pageLength: 25,
|
||||
lengthMenu: [10, 25, 50, 100],
|
||||
ajax: {
|
||||
url: '/api/tables/accounts',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: function(d) {
|
||||
return JSON.stringify(d);
|
||||
},
|
||||
error: function(xhr, error, thrown) {
|
||||
console.error('Error loading accounts:', error);
|
||||
if (xhr.status === 401) {
|
||||
window.location.href = '/login';
|
||||
}
|
||||
}
|
||||
},
|
||||
columns: [
|
||||
{ data: 'id' },
|
||||
{ data: 'name' },
|
||||
{
|
||||
data: 'banned',
|
||||
render: function(data) {
|
||||
return data ? '<span class="badge badge-banned">Banned</span>' : '<span class="badge bg-success">Active</span>';
|
||||
}
|
||||
},
|
||||
{
|
||||
data: 'locked',
|
||||
render: function(data) {
|
||||
return data ? '<span class="badge badge-locked">Locked</span>' : '<span class="badge bg-secondary">Unlocked</span>';
|
||||
}
|
||||
},
|
||||
{
|
||||
data: 'gm_level',
|
||||
render: function(data) {
|
||||
if (data === 0) return '-';
|
||||
return '<span class="badge badge-gm">Level ' + data + '</span>';
|
||||
}
|
||||
},
|
||||
{
|
||||
data: 'mute_expire',
|
||||
render: function(data) {
|
||||
if (data === 0) return 'Not Muted';
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const isMuted = data > now;
|
||||
const date = new Date(data * 1000);
|
||||
const dateStr = date.toLocaleString();
|
||||
if (isMuted) {
|
||||
return '<span class="badge bg-danger">Muted until ' + dateStr + '</span>';
|
||||
} else {
|
||||
return '<span class="badge bg-success">Expired ' + dateStr + '</span>';
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
data: 'created_at',
|
||||
render: function(data) {
|
||||
return data ? new Date(data).toLocaleString() : '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
data: 'id',
|
||||
render: function(data) {
|
||||
return '<div class="account-actions">' +
|
||||
'<button class="btn btn-sm btn-info" onclick="viewAccount(' + data + ')" title="View">👁️</button>' +
|
||||
'<button class="btn btn-sm btn-warning" onclick="editAccount(' + data + ')" title="Edit">✏️</button>' +
|
||||
'</div>';
|
||||
},
|
||||
orderable: false,
|
||||
searchable: false
|
||||
}
|
||||
],
|
||||
order: [[0, 'asc']],
|
||||
stateSave: false
|
||||
});
|
||||
|
||||
// Register table for real-time updates
|
||||
realtimeManager.RegisterTable('account', table);
|
||||
|
||||
// Update account count when DataTable loads
|
||||
table.on('draw.dt', function() {
|
||||
const info = table.page.info();
|
||||
const countEl = document.querySelector('.account-count');
|
||||
if (countEl && info) {
|
||||
countEl.textContent = info.recordsTotal;
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize real-time listener when WebSocket is ready
|
||||
const waitForWS = setInterval(() => {
|
||||
if (typeof realtimeManager !== 'undefined' && wsConnectionReady) {
|
||||
clearInterval(waitForWS);
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
|
||||
function viewAccount(id) {
|
||||
window.location.href = '/accounts/' + id;
|
||||
}
|
||||
|
||||
function editAccount(id) {
|
||||
alert('Edit account: ' + id);
|
||||
// TODO: Implement account edit modal
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
68
dDashboardServer/templates/base.jinja2
Normal file
68
dDashboardServer/templates/base.jinja2
Normal file
@@ -0,0 +1,68 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}DarkflameServer{% endblock %}</title>
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
|
||||
<link href="https://cdn.datatables.net/v/bs5/jq-3.7.0/dt-2.3.7/b-3.2.6/fh-4.0.6/sc-2.4.3/datatables.min.css" rel="stylesheet" integrity="sha384-XMNDGLb5fN9IqhIrVXOAtGKcz4KCr+JSHXGZ1TDXQPDukbEAfmLPjHdCXhgK93fv" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="/css/dashboard.css">
|
||||
{% block css %}{% endblock %}
|
||||
</head>
|
||||
<body class="d-flex bg-dark text-white">
|
||||
{% if username and username != "" %}
|
||||
{% include "header.jinja2" %}
|
||||
{% endif %}
|
||||
|
||||
<div class="container-fluid py-3">
|
||||
{% block content_before %}{% endblock %}
|
||||
{% block content %}{% endblock %}
|
||||
{% block content_after %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<footer class="mt-5 pt-5 border-top border-secondary text-center pb-3">
|
||||
{% block footer %}
|
||||
<p class="text-muted small">DarkflameServer Dashboard © 2024</p>
|
||||
{% endblock %}
|
||||
</footer>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.3.0/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.datatables.net/v/bs5/jq-3.7.0/dt-2.3.7/b-3.2.6/fh-4.0.6/sc-2.4.3/datatables.min.js" integrity="sha384-BPUXtS4tH3onFfu5m+dPbFfpLOXQwSWGwrsNWxOAAwqqJx6tJHhFkGF6uitrmEui" crossorigin="anonymous"></script>
|
||||
<script>
|
||||
// Global logout function (available on all pages)
|
||||
function deleteCookie(name) {
|
||||
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; SameSite=Strict`;
|
||||
}
|
||||
|
||||
function logout() {
|
||||
deleteCookie('dashboardToken');
|
||||
deleteCookie('gmLevel');
|
||||
localStorage.removeItem('dashboardToken');
|
||||
window.location.href = '/login';
|
||||
}
|
||||
|
||||
// Setup logout button
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const logoutBtn = document.getElementById('logoutBtn');
|
||||
if (logoutBtn) {
|
||||
logoutBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
logout();
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Load dashboard.js only on dashboard pages (not on login) -->
|
||||
<script>
|
||||
const isLoginPage = document.querySelector('.login-container') !== null;
|
||||
if (!isLoginPage) {
|
||||
const script = document.createElement('script');
|
||||
script.src = '/js/dashboard.js';
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
</script>
|
||||
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
119
dDashboardServer/templates/bug_reports.jinja2
Normal file
119
dDashboardServer/templates/bug_reports.jinja2
Normal file
@@ -0,0 +1,119 @@
|
||||
{% extends "base.jinja2" %}
|
||||
|
||||
{% block title %}Bug Reports - DarkflameServer{% endblock %}
|
||||
|
||||
{% block css %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="bug-reports-container">
|
||||
<div class="container-fluid">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>Bug Reports <span class="bug_report-count">0</span></h2>
|
||||
<p class="text-muted">View and manage bug reports from players</p>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table id="bugReportsTable" class="table table-dark table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Player</th>
|
||||
<th>Client Version</th>
|
||||
<th>Submitted</th>
|
||||
<th>Report Preview</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Data populated by DataTables -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="/js/realtime.js"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Initialize DataTable with server-side processing
|
||||
const table = $('#bugReportsTable').DataTable({
|
||||
processing: true,
|
||||
serverSide: true,
|
||||
pageLength: 25,
|
||||
lengthMenu: [10, 25, 50, 100],
|
||||
ajax: {
|
||||
url: '/api/tables/bug_reports',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: function(d) {
|
||||
return JSON.stringify(d);
|
||||
}
|
||||
},
|
||||
columns: [
|
||||
{ data: 'id' },
|
||||
{ data: 'other_player_id' },
|
||||
{ data: 'client_version' },
|
||||
{
|
||||
data: 'submitted',
|
||||
render: function(data) {
|
||||
return data ? new Date(data).toLocaleString() : '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
data: 'body',
|
||||
render: function(data) {
|
||||
return '<span class="report-preview" title="' + (data || '') + '">' + (data || '-') + '</span>';
|
||||
}
|
||||
},
|
||||
{
|
||||
data: 'id',
|
||||
render: function(data) {
|
||||
return '<div class="account-actions">' +
|
||||
'<button class="btn btn-sm btn-info" onclick="viewReport(' + data + ')" title="View">👁️</button>' +
|
||||
'<button class="btn btn-sm btn-danger" onclick="deleteReport(' + data + ')" title="Delete">🗑️</button>' +
|
||||
'</div>';
|
||||
},
|
||||
orderable: false,
|
||||
searchable: false
|
||||
}
|
||||
],
|
||||
order: [[0, 'desc']],
|
||||
stateSave: false
|
||||
});
|
||||
|
||||
// Register table for real-time updates
|
||||
realtimeManager.RegisterTable('bug_report', table);
|
||||
|
||||
// Update bug report count on table draw
|
||||
table.on('draw.dt', function() {
|
||||
const info = table.page.info();
|
||||
const countEl = document.querySelector('.bug_report-count');
|
||||
if (countEl && info) {
|
||||
countEl.textContent = info.recordsTotal;
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize real-time listener when WebSocket is ready
|
||||
const waitForWS = setInterval(() => {
|
||||
if (typeof realtimeManager !== 'undefined' && wsConnectionReady) {
|
||||
clearInterval(waitForWS);
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
|
||||
function viewReport(id) {
|
||||
alert('View report: ' + id);
|
||||
// TODO: Implement report view modal
|
||||
}
|
||||
|
||||
function deleteReport(id) {
|
||||
if (confirm('Are you sure you want to delete this report?')) {
|
||||
alert('Delete report: ' + id);
|
||||
// TODO: Implement report deletion
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
112
dDashboardServer/templates/characters.jinja2
Normal file
112
dDashboardServer/templates/characters.jinja2
Normal file
@@ -0,0 +1,112 @@
|
||||
{% extends "base.jinja2" %}
|
||||
|
||||
{% block title %}Characters - DarkflameServer{% endblock %}
|
||||
|
||||
{% block css %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="characters-container">
|
||||
<div class="container-fluid">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>Characters</h2>
|
||||
<p class="text-muted">View and manage player characters - <span class="character-count">0</span> total</p>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table id="charactersTable" class="table table-dark table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Account</th>
|
||||
<th>Last Login</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Data populated by DataTables -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="/js/realtime.js"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Initialize DataTable with server-side processing
|
||||
const table = $('#charactersTable').DataTable({
|
||||
processing: true,
|
||||
serverSide: true,
|
||||
pageLength: 25,
|
||||
lengthMenu: [10, 25, 50, 100],
|
||||
ajax: {
|
||||
url: '/api/tables/characters',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: function(d) {
|
||||
return JSON.stringify(d);
|
||||
}
|
||||
},
|
||||
columns: [
|
||||
{ data: 'id' },
|
||||
{ data: 'name' },
|
||||
{ data: 'account_name' },
|
||||
{
|
||||
data: 'last_login',
|
||||
render: function(data) {
|
||||
if (data === 0) return 'Never';
|
||||
const date = new Date(data * 1000);
|
||||
return date.toLocaleString();
|
||||
}
|
||||
},
|
||||
{
|
||||
data: 'id',
|
||||
render: function(data) {
|
||||
return '<div class="account-actions">' +
|
||||
'<button class="btn btn-sm btn-info" onclick="viewCharacter(' + data + ')" title="View">👁️</button>' +
|
||||
'<button class="btn btn-sm btn-warning" onclick="editCharacter(' + data + ')" title="Edit">✏️</button>' +
|
||||
'</div>';
|
||||
},
|
||||
orderable: false,
|
||||
searchable: false
|
||||
}
|
||||
],
|
||||
order: [[0, 'asc']],
|
||||
stateSave: false
|
||||
});
|
||||
|
||||
// Register table for real-time updates
|
||||
realtimeManager.RegisterTable('character', table);
|
||||
|
||||
// Update character count when DataTable loads
|
||||
table.on('draw.dt', function() {
|
||||
const info = table.page.info();
|
||||
const countEl = document.querySelector('.character-count');
|
||||
if (countEl && info) {
|
||||
countEl.textContent = info.recordsTotal;
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize WebSocket realtime updates
|
||||
const waitForWS = setInterval(() => {
|
||||
if (typeof realtimeManager !== 'undefined' && wsConnectionReady) {
|
||||
clearInterval(waitForWS);
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
|
||||
function viewCharacter(id) {
|
||||
alert('View character: ' + id);
|
||||
// TODO: Implement character view modal
|
||||
}
|
||||
|
||||
function editCharacter(id) {
|
||||
alert('Edit character: ' + id);
|
||||
// TODO: Implement character edit modal
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
30
dDashboardServer/templates/header.jinja2
Normal file
30
dDashboardServer/templates/header.jinja2
Normal file
@@ -0,0 +1,30 @@
|
||||
{# Navigation #}
|
||||
<nav class="navbar navbar-dark bg-dark flex-column">
|
||||
<div class="p-3">
|
||||
<a class="navbar-brand fw-bold" href="/">🎮 DarkflameServer</a>
|
||||
</div>
|
||||
|
||||
<ul class="navbar-nav flex-column w-100 flex-grow-1 p-3">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/">Home</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/accounts">Accounts</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/characters">Characters</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/play_keys">Play Keys</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/properties">Properties</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/bug_reports">Bug Reports</a>
|
||||
</li>
|
||||
<li class="nav-item mt-auto">
|
||||
<a class="nav-link" href="#" id="logoutBtn">Logout</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
35
dDashboardServer/templates/index.jinja2
Normal file
35
dDashboardServer/templates/index.jinja2
Normal file
@@ -0,0 +1,35 @@
|
||||
{% extends "base.jinja2" %}
|
||||
|
||||
{% block title %}Dashboard - DarkflameServer{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-5 px-4">
|
||||
<div class="dashboard-header mb-4">
|
||||
<div>
|
||||
<h1>Welcome to DarkflameServer Dashboard</h1>
|
||||
<p class="text-muted">Real-time server monitoring and administration</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
{% include "server_status.jinja2" %}
|
||||
{% include "statistics.jinja2" %}
|
||||
</div>
|
||||
|
||||
{% include "world_instances.jinja2" %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// Initialize dashboard when dashboard.js is loaded
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Wait a bit for dashboard.js to load
|
||||
setTimeout(() => {
|
||||
if (typeof checkAuthentication === 'function') {
|
||||
checkAuthentication();
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
65
dDashboardServer/templates/login.jinja2
Normal file
65
dDashboardServer/templates/login.jinja2
Normal file
@@ -0,0 +1,65 @@
|
||||
{% extends "base.jinja2" %}
|
||||
|
||||
{% block title %}Dashboard Login - DarkflameServer{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
<link rel="stylesheet" href="/css/login.css">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="login-container">
|
||||
<div class="login-wrapper">
|
||||
<div class="login-left">
|
||||
<div class="login-branding">
|
||||
<div class="logo">⚙️</div>
|
||||
<h1>DarkflameServer</h1>
|
||||
<p>Administration Dashboard</p>
|
||||
</div>
|
||||
<div class="login-info">
|
||||
<h3>Welcome Back</h3>
|
||||
<p>Manage your DarkflameServer infrastructure with ease. Secure access to server administration tools.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="login-right">
|
||||
<div class="login-form-wrapper">
|
||||
<h2>Sign In</h2>
|
||||
|
||||
<div id="alert" class="alert-box" role="alert" style="display: none;"></div>
|
||||
|
||||
<form id="loginForm" class="login-form">
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" id="username" name="username" required autofocus placeholder="Enter your username">
|
||||
<div class="form-focus"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password" required maxlength="40" placeholder="Enter your password">
|
||||
<div class="form-focus"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-checkbox">
|
||||
<input type="checkbox" id="rememberMe" name="rememberMe">
|
||||
<label for="rememberMe">Remember me for 30 days</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-login" id="loginBtn">
|
||||
<span id="loading" class="spinner" style="display: none;"></span>
|
||||
<span class="btn-text">Sign In</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="login-footer">
|
||||
<p>Contact your administrator if you don't have access</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="/js/login.js"></script>
|
||||
{% endblock %}
|
||||
117
dDashboardServer/templates/play_keys.jinja2
Normal file
117
dDashboardServer/templates/play_keys.jinja2
Normal file
@@ -0,0 +1,117 @@
|
||||
{% extends "base.jinja2" %}
|
||||
|
||||
{% block title %}Play Keys - DarkflameServer{% endblock %}
|
||||
|
||||
{% block css %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="play-keys-container">
|
||||
<div class="container-fluid">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>Play Keys <span class="play_key-count">0</span></h2>
|
||||
<p class="text-muted">View and manage play keys</p>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table id="playKeysTable" class="table table-dark table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Key String</th>
|
||||
<th>Uses Remaining</th>
|
||||
<th>Created</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Data populated by DataTables -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="/js/realtime.js"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Initialize DataTable with server-side processing
|
||||
const table = $('#playKeysTable').DataTable({
|
||||
processing: true,
|
||||
serverSide: true,
|
||||
pageLength: 25,
|
||||
lengthMenu: [10, 25, 50, 100],
|
||||
ajax: {
|
||||
url: '/api/tables/play_keys',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: function(d) {
|
||||
return JSON.stringify(d);
|
||||
}
|
||||
},
|
||||
columns: [
|
||||
{ data: 'id' },
|
||||
{ data: 'key_string' },
|
||||
{ data: 'key_uses' },
|
||||
{
|
||||
data: 'created_at',
|
||||
render: function(data) {
|
||||
return data ? new Date(data).toLocaleString() : '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
data: 'active',
|
||||
render: function(data) {
|
||||
return data ? '<span class="badge badge-active">Active</span>' : '<span class="badge badge-inactive">Inactive</span>';
|
||||
}
|
||||
},
|
||||
{
|
||||
data: 'id',
|
||||
render: function(data) {
|
||||
return '<div class="account-actions">' +
|
||||
'<button class="btn btn-sm btn-info" onclick="viewKey(' + data + ')" title="View">👁️</button>' +
|
||||
'<button class="btn btn-sm btn-warning" onclick="editKey(' + data + ')" title="Edit">✏️</button>' +
|
||||
'</div>';
|
||||
},
|
||||
orderable: false,
|
||||
searchable: false
|
||||
}
|
||||
],
|
||||
order: [[0, 'asc']],
|
||||
stateSave: false
|
||||
});
|
||||
|
||||
// Register table for real-time updates
|
||||
realtimeManager.RegisterTable('play_key', table);
|
||||
|
||||
// Update play key count on table draw
|
||||
table.on('draw.dt', function() {
|
||||
const info = table.page.info();
|
||||
const countEl = document.querySelector('.play_key-count');
|
||||
if (countEl && info) {
|
||||
countEl.textContent = info.recordsTotal;
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize real-time listener when WebSocket is ready
|
||||
const waitForWS = setInterval(() => {
|
||||
if (typeof realtimeManager !== 'undefined' && wsConnectionReady) {
|
||||
clearInterval(waitForWS);
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
|
||||
function viewKey(id) {
|
||||
alert('View key: ' + id);
|
||||
// TODO: Implement key view modal
|
||||
}
|
||||
|
||||
function editKey(id) {
|
||||
alert('Edit key: ' + id);
|
||||
// TODO: Implement key edit modal
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
114
dDashboardServer/templates/properties.jinja2
Normal file
114
dDashboardServer/templates/properties.jinja2
Normal file
@@ -0,0 +1,114 @@
|
||||
{% extends "base.jinja2" %}
|
||||
|
||||
{% block title %}Properties - DarkflameServer{% endblock %}
|
||||
|
||||
{% block css %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="properties-container">
|
||||
<div class="container-fluid">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>Properties <span class="property-count">0</span></h2>
|
||||
<p class="text-muted">View and manage player properties</p>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table id="propertiesTable" class="table table-dark table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Owner ID</th>
|
||||
<th>Moderation Status</th>
|
||||
<th>Reputation</th>
|
||||
<th>Zone</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Data populated by DataTables -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="/js/realtime.js"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Initialize DataTable with server-side processing
|
||||
const table = $('#propertiesTable').DataTable({
|
||||
processing: true,
|
||||
serverSide: true,
|
||||
pageLength: 25,
|
||||
lengthMenu: [10, 25, 50, 100],
|
||||
ajax: {
|
||||
url: '/api/tables/properties',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: function(d) {
|
||||
return JSON.stringify(d);
|
||||
}
|
||||
},
|
||||
columns: [
|
||||
{ data: 'id' },
|
||||
{ data: 'name' },
|
||||
{ data: 'owner_id' },
|
||||
{
|
||||
data: 'mod_approved',
|
||||
render: function(data) {
|
||||
return data ? '<span class="badge badge-approved">Approved</span>' : '<span class="badge badge-pending">Pending</span>';
|
||||
}
|
||||
},
|
||||
{ data: 'reputation' },
|
||||
{ data: 'zone_id' },
|
||||
{
|
||||
data: 'id',
|
||||
render: function(data) {
|
||||
return '<div class="account-actions">' +
|
||||
'<button class="btn btn-sm btn-info" onclick="viewProperty(' + data + ')" title="View">👁️</button>' +
|
||||
'<button class="btn btn-sm btn-warning" onclick="editProperty(' + data + ')" title="Edit">✏️</button>' +
|
||||
'</div>';
|
||||
},
|
||||
orderable: false,
|
||||
searchable: false
|
||||
}
|
||||
],
|
||||
order: [[0, 'asc']],
|
||||
stateSave: false
|
||||
});
|
||||
|
||||
// Register table for real-time updates
|
||||
realtimeManager.RegisterTable('property', table);
|
||||
|
||||
// Update property count on table draw
|
||||
table.on('draw.dt', function() {
|
||||
const info = table.page.info();
|
||||
const countEl = document.querySelector('.property-count');
|
||||
if (countEl && info) {
|
||||
countEl.textContent = info.recordsTotal;
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize real-time listener when WebSocket is ready
|
||||
const waitForWS = setInterval(() => {
|
||||
if (typeof realtimeManager !== 'undefined' && wsConnectionReady) {
|
||||
clearInterval(waitForWS);
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
|
||||
function viewProperty(id) {
|
||||
alert('View property: ' + id);
|
||||
// TODO: Implement property view modal
|
||||
}
|
||||
|
||||
function editProperty(id) {
|
||||
alert('Edit property: ' + id);
|
||||
// TODO: Implement property edit modal
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
29
dDashboardServer/templates/server_status.jinja2
Normal file
29
dDashboardServer/templates/server_status.jinja2
Normal file
@@ -0,0 +1,29 @@
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Server Status</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<span>Auth Server</span>
|
||||
{% if auth.online %}
|
||||
<span class="badge badge-active" id="auth-status">Online</span>
|
||||
{% else %}
|
||||
<span class="badge badge-banned" id="auth-status">Offline</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<span>Chat Server</span>
|
||||
{% if chat.online %}
|
||||
<span class="badge badge-active" id="chat-status">Online</span>
|
||||
{% else %}
|
||||
<span class="badge badge-banned" id="chat-status">Offline</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span>Active Worlds</span>
|
||||
<span class="badge badge-primary" id="world-count">{{ length(worlds) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
21
dDashboardServer/templates/statistics.jinja2
Normal file
21
dDashboardServer/templates/statistics.jinja2
Normal file
@@ -0,0 +1,21 @@
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Statistics</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<span>Online Players</span>
|
||||
<span class="badge badge-primary" id="online-players">{{ stats.onlinePlayers }}</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<span>Total Accounts</span>
|
||||
<span class="badge badge-primary" id="total-accounts">{{ stats.totalAccounts }}</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span>Total Characters</span>
|
||||
<span class="badge badge-primary" id="total-characters">{{ stats.totalCharacters }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
37
dDashboardServer/templates/world_instances.jinja2
Normal file
37
dDashboardServer/templates/world_instances.jinja2
Normal file
@@ -0,0 +1,37 @@
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">
|
||||
<h2>Active World Instances</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="world-list">
|
||||
{% if length(worlds) == 0 %}
|
||||
<p class="text-muted text-center mb-0">No active world instances</p>
|
||||
{% else %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-dark table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Zone</th>
|
||||
<th>Instance</th>
|
||||
<th>Clone</th>
|
||||
<th>Players</th>
|
||||
<th>Type</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for world in worlds %}
|
||||
<tr>
|
||||
<td>{{ world.mapID }}</td>
|
||||
<td>{{ world.instanceID }}</td>
|
||||
<td>{{ world.cloneID }}</td>
|
||||
<td><span class="badge badge-primary">{{ world.players }}</span></td>
|
||||
<td>{% if world.isPrivate %}<span class="badge badge-warning">Private</span>{% else %}<span class="badge badge-success">Public</span>{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include "json.hpp"
|
||||
|
||||
enum class eGameMasterLevel : uint8_t;
|
||||
|
||||
@@ -39,6 +40,30 @@ public:
|
||||
virtual void UpdateAccountGmLevel(const uint32_t accountId, const eGameMasterLevel gmLevel) = 0;
|
||||
|
||||
virtual uint32_t GetAccountCount() = 0;
|
||||
|
||||
// Login attempt tracking methods
|
||||
// Record a failed login attempt
|
||||
virtual void RecordFailedAttempt(const uint32_t accountId) = 0;
|
||||
|
||||
// Clear failed login attempts and update last login time
|
||||
virtual void ClearFailedAttempts(const uint32_t accountId) = 0;
|
||||
|
||||
// Set account lockout
|
||||
virtual void SetLockout(const uint32_t accountId, const int64_t lockoutUntil) = 0;
|
||||
|
||||
// Check if account is locked out
|
||||
virtual bool IsLockedOut(const uint32_t accountId) = 0;
|
||||
|
||||
// Get failed attempt count
|
||||
virtual uint8_t GetFailedAttempts(const uint32_t accountId) = 0;
|
||||
|
||||
// Get paginated list of accounts with optional search/filtering for DataTables
|
||||
// Returns a JSON object with the account data and metadata
|
||||
virtual nlohmann::json GetAccountsTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) = 0;
|
||||
|
||||
// Get a single account by ID
|
||||
// Returns a JSON object with the account details
|
||||
virtual nlohmann::json GetAccountById(uint32_t accountId) = 0;
|
||||
};
|
||||
|
||||
#endif //!__IACCOUNTS__H__
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#define __IBUGREPORTS__H__
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
class IBugReports {
|
||||
@@ -16,5 +17,9 @@ public:
|
||||
|
||||
// Add a new bug report to the database.
|
||||
virtual void InsertNewBugReport(const Info& info) = 0;
|
||||
|
||||
// Get paginated list of bug reports with optional search/filtering for DataTables
|
||||
// Returns a JSON-formatted string with the bug report data and metadata
|
||||
virtual std::string GetBugReportsTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) = 0;
|
||||
};
|
||||
#endif //!__IBUGREPORTS__H__
|
||||
|
||||
@@ -33,6 +33,9 @@ public:
|
||||
// Get the character ids for the given account.
|
||||
virtual std::vector<LWOOBJID> GetAccountCharacterIds(const LWOOBJID accountId) = 0;
|
||||
|
||||
// Get the total number of characters in the database.
|
||||
virtual uint32_t GetCharacterCount() = 0;
|
||||
|
||||
// Insert a new character into the database.
|
||||
virtual void InsertNewCharacter(const ICharInfo::Info info) = 0;
|
||||
|
||||
|
||||
@@ -15,6 +15,10 @@ public:
|
||||
|
||||
// Insert the character xml for the given character id.
|
||||
virtual void InsertCharacterXml(const LWOOBJID characterId, const std::string_view lxfml) = 0;
|
||||
|
||||
// Get paginated list of characters with optional search/filtering for DataTables
|
||||
// Returns a JSON-formatted string with the character data and metadata
|
||||
virtual std::string GetCharactersTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) = 0;
|
||||
};
|
||||
|
||||
#endif //!__ICHARXML__H__
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
class IPlayKeys {
|
||||
public:
|
||||
@@ -10,6 +12,10 @@ public:
|
||||
// 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;
|
||||
|
||||
// Get paginated list of play keys with optional search/filtering for DataTables
|
||||
// Returns a JSON-formatted string with the play key data and metadata
|
||||
virtual std::string GetPlayKeysTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) = 0;
|
||||
};
|
||||
|
||||
#endif //!__IPLAYKEYS__H__
|
||||
|
||||
@@ -65,5 +65,9 @@ public:
|
||||
|
||||
// Insert a new property into the database.
|
||||
virtual void InsertNewProperty(const IProperty::Info& info, const uint32_t templateId, const LWOZONEID& zoneId) = 0;
|
||||
|
||||
// Get paginated list of properties with optional search/filtering for DataTables
|
||||
// Returns a JSON-formatted string with the property data and metadata
|
||||
virtual std::string GetPropertiesTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) = 0;
|
||||
};
|
||||
#endif //!__IPROPERTY__H__
|
||||
|
||||
@@ -74,6 +74,7 @@ public:
|
||||
std::optional<IAccounts::Info> GetAccountInfo(const std::string_view username) override;
|
||||
void InsertNewCharacter(const ICharInfo::Info info) override;
|
||||
void InsertCharacterXml(const LWOOBJID accountId, const std::string_view lxfml) override;
|
||||
std::string GetCharactersTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) 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;
|
||||
@@ -93,6 +94,7 @@ public:
|
||||
void RemoveModel(const LWOOBJID& modelId) override;
|
||||
void UpdatePerformanceCost(const LWOZONEID& zoneId, const float performanceCost) override;
|
||||
void InsertNewBugReport(const IBugReports::Info& info) override;
|
||||
std::string GetBugReportsTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) override;
|
||||
void InsertCheatDetection(const IPlayerCheatDetections::Info& info) override;
|
||||
void InsertNewMail(const MailInfo& mail) override;
|
||||
void InsertNewUgcModel(
|
||||
@@ -117,6 +119,7 @@ public:
|
||||
void InsertDefaultPersistentId() override;
|
||||
std::optional<uint32_t> GetDonationTotal(const uint32_t activityId) override;
|
||||
std::optional<bool> IsPlaykeyActive(const int32_t playkeyId) override;
|
||||
std::string GetPlayKeysTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) override;
|
||||
std::vector<IUgc::Model> GetUgcModels(const LWOOBJID& propertyId) override;
|
||||
void AddIgnore(const LWOOBJID playerId, const LWOOBJID ignoredPlayerId) override;
|
||||
void RemoveIgnore(const LWOOBJID playerId, const LWOOBJID ignoredPlayerId) override;
|
||||
@@ -140,10 +143,19 @@ public:
|
||||
void InsertUgcBuild(const std::string& modules, const LWOOBJID bigId, const std::optional<LWOOBJID> characterId) override;
|
||||
void DeleteUgcBuild(const LWOOBJID bigId) override;
|
||||
uint32_t GetAccountCount() override;
|
||||
uint32_t GetCharacterCount() override;
|
||||
void RecordFailedAttempt(const uint32_t accountId) override;
|
||||
void ClearFailedAttempts(const uint32_t accountId) override;
|
||||
void SetLockout(const uint32_t accountId, const int64_t lockoutUntil) override;
|
||||
bool IsLockedOut(const uint32_t accountId) override;
|
||||
uint8_t GetFailedAttempts(const uint32_t accountId) override;
|
||||
nlohmann::json GetAccountsTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) override;
|
||||
nlohmann::json GetAccountById(uint32_t accountId) override;
|
||||
bool IsNameInUse(const std::string_view name) 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;
|
||||
std::string GetPropertiesTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) override;
|
||||
sql::PreparedStatement* CreatePreppedStmt(const std::string& query);
|
||||
private:
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "MySQLDatabase.h"
|
||||
|
||||
#include "eGameMasterLevel.h"
|
||||
#include "json.hpp"
|
||||
|
||||
std::optional<IAccounts::Info> MySQLDatabase::GetAccountInfo(const std::string_view username) {
|
||||
auto result = ExecuteSelect("SELECT id, password, banned, locked, play_key_id, gm_level, mute_expire FROM accounts WHERE name = ? LIMIT 1;", username);
|
||||
@@ -45,3 +46,142 @@ uint32_t MySQLDatabase::GetAccountCount() {
|
||||
auto res = ExecuteSelect("SELECT COUNT(*) as count FROM accounts;");
|
||||
return res->next() ? res->getUInt("count") : 0;
|
||||
}
|
||||
|
||||
void MySQLDatabase::RecordFailedAttempt(const uint32_t accountId) {
|
||||
ExecuteUpdate("UPDATE accounts SET failed_attempts = failed_attempts + 1 WHERE id = ?;", accountId);
|
||||
}
|
||||
|
||||
void MySQLDatabase::ClearFailedAttempts(const uint32_t accountId) {
|
||||
ExecuteUpdate("UPDATE accounts SET failed_attempts = 0, lockout_time = NULL, last_login = NOW() WHERE id = ?;", accountId);
|
||||
}
|
||||
|
||||
void MySQLDatabase::SetLockout(const uint32_t accountId, const int64_t lockoutUntil) {
|
||||
ExecuteUpdate("UPDATE accounts SET lockout_time = FROM_UNIXTIME(?) WHERE id = ?;", lockoutUntil, accountId);
|
||||
}
|
||||
|
||||
bool MySQLDatabase::IsLockedOut(const uint32_t accountId) {
|
||||
auto result = ExecuteSelect("SELECT lockout_time FROM accounts WHERE id = ?;", accountId);
|
||||
if (!result->next()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If lockout_time is set and in the future, account is locked
|
||||
const char* lockoutTime = result->getString("lockout_time").c_str();
|
||||
if (lockoutTime == nullptr || strlen(lockoutTime) == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Simplified check - if lockout_time exists and is not null, it's locked
|
||||
return true;
|
||||
}
|
||||
|
||||
uint8_t MySQLDatabase::GetFailedAttempts(const uint32_t accountId) {
|
||||
auto result = ExecuteSelect("SELECT failed_attempts FROM accounts WHERE id = ?;", accountId);
|
||||
if (!result->next()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return result->getUInt("failed_attempts");
|
||||
}
|
||||
|
||||
nlohmann::json MySQLDatabase::GetAccountsTable(uint32_t start, uint32_t length, const std::string_view search, uint32_t orderColumn, bool orderAsc) {
|
||||
// Build base query
|
||||
std::string baseQuery = "SELECT id, name, banned, locked, gm_level, mute_expire, created_at FROM accounts";
|
||||
std::string whereClause;
|
||||
std::string orderClause;
|
||||
|
||||
// Add search filter if provided
|
||||
if (!search.empty()) {
|
||||
whereClause = " WHERE name LIKE CONCAT('%', ?, '%')";
|
||||
}
|
||||
|
||||
// Map column indices to database columns
|
||||
std::string orderColumnName = "id";
|
||||
switch (orderColumn) {
|
||||
case 0: orderColumnName = "id"; break;
|
||||
case 1: orderColumnName = "name"; break;
|
||||
case 2: orderColumnName = "banned"; break;
|
||||
case 3: orderColumnName = "locked"; break;
|
||||
case 4: orderColumnName = "gm_level"; break;
|
||||
case 5: orderColumnName = "mute_expire"; break;
|
||||
case 6: orderColumnName = "created_at"; break;
|
||||
default: orderColumnName = "id";
|
||||
}
|
||||
|
||||
orderClause = " ORDER BY " + orderColumnName + (orderAsc ? " ASC" : " DESC");
|
||||
|
||||
// Build the main query
|
||||
std::string mainQuery = baseQuery + whereClause + orderClause + " LIMIT ?, ?;";
|
||||
|
||||
// Get total count
|
||||
std::string totalCountQuery = "SELECT COUNT(*) as count FROM accounts;";
|
||||
auto totalCountResult = ExecuteSelect(totalCountQuery);
|
||||
uint32_t totalRecords = totalCountResult->next() ? totalCountResult->getUInt("count") : 0;
|
||||
|
||||
// Get filtered count
|
||||
uint32_t filteredRecords = totalRecords;
|
||||
if (!search.empty()) {
|
||||
std::string filteredCountQuery = "SELECT COUNT(*) as count FROM accounts WHERE name LIKE CONCAT('%', ?, '%');";
|
||||
auto filteredCountResult = ExecuteSelect(filteredCountQuery, search);
|
||||
filteredRecords = filteredCountResult->next() ? filteredCountResult->getUInt("count") : 0;
|
||||
}
|
||||
|
||||
// Execute main query
|
||||
std::unique_ptr<sql::ResultSet> result;
|
||||
if (!search.empty()) {
|
||||
result = ExecuteSelect(mainQuery, search, start, length);
|
||||
} else {
|
||||
result = ExecuteSelect(mainQuery, start, length);
|
||||
}
|
||||
|
||||
// Build response JSON
|
||||
nlohmann::json accountsArray = nlohmann::json::array();
|
||||
|
||||
while (result->next()) {
|
||||
nlohmann::json account = {
|
||||
{"id", result->getUInt("id")},
|
||||
{"name", result->getString("name")},
|
||||
{"banned", result->getBoolean("banned")},
|
||||
{"locked", result->getBoolean("locked")},
|
||||
{"gm_level", result->getInt("gm_level")},
|
||||
{"mute_expire", result->getUInt64("mute_expire")},
|
||||
{"created_at", result->getString("created_at")}
|
||||
};
|
||||
accountsArray.push_back(account);
|
||||
}
|
||||
|
||||
nlohmann::json response = {
|
||||
{"draw", 1},
|
||||
{"recordsTotal", totalRecords},
|
||||
{"recordsFiltered", filteredRecords},
|
||||
{"data", accountsArray}
|
||||
};
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
nlohmann::json MySQLDatabase::GetAccountById(uint32_t accountId) {
|
||||
try {
|
||||
const std::string query = "SELECT id, name, banned, locked, gm_level, mute_expire, created_at FROM accounts WHERE id = ?;";
|
||||
auto result = ExecuteSelect(query, accountId);
|
||||
|
||||
if (!result->next()) {
|
||||
return nlohmann::json{{"error", "Account not found"}};
|
||||
}
|
||||
|
||||
nlohmann::json account = {
|
||||
{"id", result->getUInt("id")},
|
||||
{"name", result->getString("name")},
|
||||
{"banned", result->getBoolean("banned")},
|
||||
{"locked", result->getBoolean("locked")},
|
||||
{"gm_level", result->getInt("gm_level")},
|
||||
{"mute_expire", result->getUInt64("mute_expire")},
|
||||
{"created_at", result->getString("created_at")}
|
||||
};
|
||||
|
||||
return account;
|
||||
} catch (const sql::SQLException& e) {
|
||||
LOG_DEBUG("SQL Error: %s", e.what());
|
||||
return nlohmann::json{{"error", "Database error"}};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,3 +4,77 @@ void MySQLDatabase::InsertNewBugReport(const IBugReports::Info& info) {
|
||||
ExecuteInsert("INSERT INTO `bug_reports`(body, client_version, other_player_id, selection, reporter_id) VALUES (?, ?, ?, ?, ?)",
|
||||
info.body, info.clientVersion, info.otherPlayer, info.selection, info.characterId);
|
||||
}
|
||||
|
||||
#include "json.hpp"
|
||||
|
||||
std::string MySQLDatabase::GetBugReportsTable(uint32_t start, uint32_t length, const std::string_view search, uint32_t orderColumn, bool orderAsc) {
|
||||
// Build base query
|
||||
std::string baseQuery = "SELECT id, body, client_version, other_player_id, selection, submitted FROM bug_reports";
|
||||
std::string whereClause;
|
||||
std::string orderClause;
|
||||
|
||||
// Add search filter if provided
|
||||
if (!search.empty()) {
|
||||
whereClause = " WHERE body LIKE CONCAT('%', ?, '%') OR other_player_id LIKE CONCAT('%', ?, '%')";
|
||||
}
|
||||
|
||||
// Map column indices to database columns
|
||||
std::string orderColumnName = "id";
|
||||
switch (orderColumn) {
|
||||
case 0: orderColumnName = "id"; break;
|
||||
case 1: orderColumnName = "other_player_id"; break;
|
||||
case 2: orderColumnName = "client_version"; break;
|
||||
case 3: orderColumnName = "submitted"; break;
|
||||
default: orderColumnName = "id";
|
||||
}
|
||||
|
||||
orderClause = " ORDER BY " + orderColumnName + (orderAsc ? " ASC" : " DESC");
|
||||
|
||||
// Build the main query
|
||||
std::string mainQuery = baseQuery + whereClause + orderClause + " LIMIT ?, ?;";
|
||||
|
||||
// Get total count
|
||||
std::string totalCountQuery = "SELECT COUNT(*) as count FROM bug_reports;";
|
||||
auto totalCountResult = ExecuteSelect(totalCountQuery);
|
||||
uint32_t totalRecords = totalCountResult->next() ? totalCountResult->getUInt("count") : 0;
|
||||
|
||||
// Get filtered count
|
||||
uint32_t filteredRecords = totalRecords;
|
||||
if (!search.empty()) {
|
||||
std::string filteredCountQuery = "SELECT COUNT(*) as count FROM bug_reports WHERE body LIKE CONCAT('%', ?, '%') OR other_player_id LIKE CONCAT('%', ?, '%');";
|
||||
auto filteredCountResult = ExecuteSelect(filteredCountQuery, search, search);
|
||||
filteredRecords = filteredCountResult->next() ? filteredCountResult->getUInt("count") : 0;
|
||||
}
|
||||
|
||||
// Execute main query
|
||||
std::unique_ptr<sql::ResultSet> result;
|
||||
if (!search.empty()) {
|
||||
result = ExecuteSelect(mainQuery, search, search, start, length);
|
||||
} else {
|
||||
result = ExecuteSelect(mainQuery, start, length);
|
||||
}
|
||||
|
||||
// Build response JSON
|
||||
nlohmann::json reportsArray = nlohmann::json::array();
|
||||
|
||||
while (result->next()) {
|
||||
nlohmann::json report = {
|
||||
{"id", result->getUInt("id")},
|
||||
{"other_player_id", result->getString("other_player_id")},
|
||||
{"client_version", result->getString("client_version")},
|
||||
{"selection", result->getString("selection")},
|
||||
{"submitted", result->getString("submitted")},
|
||||
{"body", result->getString("body")}
|
||||
};
|
||||
reportsArray.push_back(report);
|
||||
}
|
||||
|
||||
nlohmann::json response = {
|
||||
{"draw", 0},
|
||||
{"recordsTotal", totalRecords},
|
||||
{"recordsFiltered", filteredRecords},
|
||||
{"data", reportsArray}
|
||||
};
|
||||
|
||||
return response.dump();
|
||||
}
|
||||
|
||||
@@ -52,6 +52,11 @@ std::vector<LWOOBJID> MySQLDatabase::GetAccountCharacterIds(const LWOOBJID accou
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
uint32_t MySQLDatabase::GetCharacterCount() {
|
||||
auto res = ExecuteSelect("SELECT COUNT(*) as count FROM charinfo;");
|
||||
return res->next() ? res->getUInt("count") : 0;
|
||||
}
|
||||
|
||||
void MySQLDatabase::InsertNewCharacter(const ICharInfo::Info info) {
|
||||
ExecuteInsert(
|
||||
"INSERT INTO `charinfo`(`id`, `account_id`, `name`, `pending_name`, `needs_rename`, `last_login`) VALUES (?,?,?,?,?,?)",
|
||||
|
||||
@@ -17,3 +17,75 @@ void MySQLDatabase::UpdateCharacterXml(const LWOOBJID charId, const std::string_
|
||||
void MySQLDatabase::InsertCharacterXml(const LWOOBJID characterId, const std::string_view lxfml) {
|
||||
ExecuteInsert("INSERT INTO `charxml` (`id`, `xml_data`) VALUES (?,?)", characterId, lxfml);
|
||||
}
|
||||
|
||||
#include "json.hpp"
|
||||
|
||||
std::string MySQLDatabase::GetCharactersTable(uint32_t start, uint32_t length, const std::string_view search, uint32_t orderColumn, bool orderAsc) {
|
||||
// Build base query
|
||||
std::string baseQuery = "SELECT c.id, c.name, c.account_id, c.last_login, a.name as account_name FROM charinfo c JOIN accounts a ON c.account_id = a.id";
|
||||
std::string whereClause;
|
||||
std::string orderClause;
|
||||
|
||||
// Add search filter if provided
|
||||
if (!search.empty()) {
|
||||
whereClause = " WHERE c.name LIKE CONCAT('%', ?, '%')";
|
||||
}
|
||||
|
||||
// Map column indices to database columns
|
||||
std::string orderColumnName = "c.id";
|
||||
switch (orderColumn) {
|
||||
case 0: orderColumnName = "c.id"; break;
|
||||
case 1: orderColumnName = "c.name"; break;
|
||||
case 2: orderColumnName = "a.name"; break;
|
||||
case 3: orderColumnName = "c.last_login"; break;
|
||||
default: orderColumnName = "c.id";
|
||||
}
|
||||
|
||||
orderClause = " ORDER BY " + orderColumnName + (orderAsc ? " ASC" : " DESC");
|
||||
|
||||
// Build the main query
|
||||
std::string mainQuery = baseQuery + whereClause + orderClause + " LIMIT ?, ?;";
|
||||
|
||||
// Get total count
|
||||
std::string totalCountQuery = "SELECT COUNT(*) as count FROM charinfo;";
|
||||
auto totalCountResult = ExecuteSelect(totalCountQuery);
|
||||
uint32_t totalRecords = totalCountResult->next() ? totalCountResult->getUInt("count") : 0;
|
||||
|
||||
// Get filtered count
|
||||
uint32_t filteredRecords = totalRecords;
|
||||
if (!search.empty()) {
|
||||
std::string filteredCountQuery = "SELECT COUNT(*) as count FROM charinfo WHERE name LIKE CONCAT('%', ?, '%');";
|
||||
auto filteredCountResult = ExecuteSelect(filteredCountQuery, search);
|
||||
filteredRecords = filteredCountResult->next() ? filteredCountResult->getUInt("count") : 0;
|
||||
}
|
||||
|
||||
// Execute main query
|
||||
std::unique_ptr<sql::ResultSet> result;
|
||||
if (!search.empty()) {
|
||||
result = ExecuteSelect(mainQuery, search, start, length);
|
||||
} else {
|
||||
result = ExecuteSelect(mainQuery, start, length);
|
||||
}
|
||||
|
||||
// Build response JSON
|
||||
nlohmann::json charactersArray = nlohmann::json::array();
|
||||
|
||||
while (result->next()) {
|
||||
nlohmann::json character = {
|
||||
{"id", result->getUInt64("id")},
|
||||
{"name", result->getString("name")},
|
||||
{"account_name", result->getString("account_name")},
|
||||
{"last_login", result->getUInt64("last_login")}
|
||||
};
|
||||
charactersArray.push_back(character);
|
||||
}
|
||||
|
||||
nlohmann::json response = {
|
||||
{"draw", 0},
|
||||
{"recordsTotal", totalRecords},
|
||||
{"recordsFiltered", filteredRecords},
|
||||
{"data", charactersArray}
|
||||
};
|
||||
|
||||
return response.dump();
|
||||
}
|
||||
|
||||
@@ -9,3 +9,77 @@ std::optional<bool> MySQLDatabase::IsPlaykeyActive(const int32_t playkeyId) {
|
||||
|
||||
return keyCheckRes->getBoolean("active");
|
||||
}
|
||||
|
||||
#include "json.hpp"
|
||||
|
||||
std::string MySQLDatabase::GetPlayKeysTable(uint32_t start, uint32_t length, const std::string_view search, uint32_t orderColumn, bool orderAsc) {
|
||||
// Build base query
|
||||
std::string baseQuery = "SELECT id, key_string, key_uses, created_at, active FROM play_keys";
|
||||
std::string whereClause;
|
||||
std::string orderClause;
|
||||
|
||||
// Add search filter if provided
|
||||
if (!search.empty()) {
|
||||
whereClause = " WHERE key_string LIKE CONCAT('%', ?, '%')";
|
||||
}
|
||||
|
||||
// Map column indices to database columns
|
||||
std::string orderColumnName = "id";
|
||||
switch (orderColumn) {
|
||||
case 0: orderColumnName = "id"; break;
|
||||
case 1: orderColumnName = "key_string"; break;
|
||||
case 2: orderColumnName = "key_uses"; break;
|
||||
case 3: orderColumnName = "created_at"; break;
|
||||
case 4: orderColumnName = "active"; break;
|
||||
default: orderColumnName = "id";
|
||||
}
|
||||
|
||||
orderClause = " ORDER BY " + orderColumnName + (orderAsc ? " ASC" : " DESC");
|
||||
|
||||
// Build the main query
|
||||
std::string mainQuery = baseQuery + whereClause + orderClause + " LIMIT ?, ?;";
|
||||
|
||||
// Get total count
|
||||
std::string totalCountQuery = "SELECT COUNT(*) as count FROM play_keys;";
|
||||
auto totalCountResult = ExecuteSelect(totalCountQuery);
|
||||
uint32_t totalRecords = totalCountResult->next() ? totalCountResult->getUInt("count") : 0;
|
||||
|
||||
// Get filtered count
|
||||
uint32_t filteredRecords = totalRecords;
|
||||
if (!search.empty()) {
|
||||
std::string filteredCountQuery = "SELECT COUNT(*) as count FROM play_keys WHERE key_string LIKE CONCAT('%', ?, '%');";
|
||||
auto filteredCountResult = ExecuteSelect(filteredCountQuery, search);
|
||||
filteredRecords = filteredCountResult->next() ? filteredCountResult->getUInt("count") : 0;
|
||||
}
|
||||
|
||||
// Execute main query
|
||||
std::unique_ptr<sql::ResultSet> result;
|
||||
if (!search.empty()) {
|
||||
result = ExecuteSelect(mainQuery, search, start, length);
|
||||
} else {
|
||||
result = ExecuteSelect(mainQuery, start, length);
|
||||
}
|
||||
|
||||
// Build response JSON
|
||||
nlohmann::json keysArray = nlohmann::json::array();
|
||||
|
||||
while (result->next()) {
|
||||
nlohmann::json key = {
|
||||
{"id", result->getUInt("id")},
|
||||
{"key_string", result->getString("key_string")},
|
||||
{"key_uses", result->getUInt("key_uses")},
|
||||
{"created_at", result->getString("created_at")},
|
||||
{"active", result->getBoolean("active")}
|
||||
};
|
||||
keysArray.push_back(key);
|
||||
}
|
||||
|
||||
nlohmann::json response = {
|
||||
{"draw", 0},
|
||||
{"recordsTotal", totalRecords},
|
||||
{"recordsFiltered", filteredRecords},
|
||||
{"data", keysArray}
|
||||
};
|
||||
|
||||
return response.dump();
|
||||
}
|
||||
|
||||
@@ -195,3 +195,79 @@ std::optional<IProperty::Info> MySQLDatabase::GetPropertyInfo(const LWOOBJID id)
|
||||
|
||||
return ReadPropertyInfo(propertyEntry);
|
||||
}
|
||||
|
||||
#include "json.hpp"
|
||||
|
||||
std::string MySQLDatabase::GetPropertiesTable(uint32_t start, uint32_t length, const std::string_view search, uint32_t orderColumn, bool orderAsc) {
|
||||
// Build base query
|
||||
std::string baseQuery = "SELECT id, owner_id, name, mod_approved, reputation, zone_id FROM properties";
|
||||
std::string whereClause;
|
||||
std::string orderClause;
|
||||
|
||||
// Add search filter if provided
|
||||
if (!search.empty()) {
|
||||
whereClause = " WHERE name LIKE CONCAT('%', ?, '%')";
|
||||
}
|
||||
|
||||
// Map column indices to database columns
|
||||
std::string orderColumnName = "id";
|
||||
switch (orderColumn) {
|
||||
case 0: orderColumnName = "id"; break;
|
||||
case 1: orderColumnName = "name"; break;
|
||||
case 2: orderColumnName = "owner_id"; break;
|
||||
case 3: orderColumnName = "mod_approved"; break;
|
||||
case 4: orderColumnName = "reputation"; break;
|
||||
case 5: orderColumnName = "zone_id"; break;
|
||||
default: orderColumnName = "id";
|
||||
}
|
||||
|
||||
orderClause = " ORDER BY " + orderColumnName + (orderAsc ? " ASC" : " DESC");
|
||||
|
||||
// Build the main query
|
||||
std::string mainQuery = baseQuery + whereClause + orderClause + " LIMIT ?, ?;";
|
||||
|
||||
// Get total count
|
||||
std::string totalCountQuery = "SELECT COUNT(*) as count FROM properties;";
|
||||
auto totalCountResult = ExecuteSelect(totalCountQuery);
|
||||
uint32_t totalRecords = totalCountResult->next() ? totalCountResult->getUInt("count") : 0;
|
||||
|
||||
// Get filtered count
|
||||
uint32_t filteredRecords = totalRecords;
|
||||
if (!search.empty()) {
|
||||
std::string filteredCountQuery = "SELECT COUNT(*) as count FROM properties WHERE name LIKE CONCAT('%', ?, '%');";
|
||||
auto filteredCountResult = ExecuteSelect(filteredCountQuery, search);
|
||||
filteredRecords = filteredCountResult->next() ? filteredCountResult->getUInt("count") : 0;
|
||||
}
|
||||
|
||||
// Execute main query
|
||||
std::unique_ptr<sql::ResultSet> result;
|
||||
if (!search.empty()) {
|
||||
result = ExecuteSelect(mainQuery, search, start, length);
|
||||
} else {
|
||||
result = ExecuteSelect(mainQuery, start, length);
|
||||
}
|
||||
|
||||
// Build response JSON
|
||||
nlohmann::json propertiesArray = nlohmann::json::array();
|
||||
|
||||
while (result->next()) {
|
||||
nlohmann::json property = {
|
||||
{"id", result->getUInt64("id")},
|
||||
{"owner_id", result->getUInt64("owner_id")},
|
||||
{"name", result->getString("name")},
|
||||
{"mod_approved", result->getBoolean("mod_approved")},
|
||||
{"reputation", result->getUInt64("reputation")},
|
||||
{"zone_id", result->getUInt("zone_id")}
|
||||
};
|
||||
propertiesArray.push_back(property);
|
||||
}
|
||||
|
||||
nlohmann::json response = {
|
||||
{"draw", 0},
|
||||
{"recordsTotal", totalRecords},
|
||||
{"recordsFiltered", filteredRecords},
|
||||
{"data", propertiesArray}
|
||||
};
|
||||
|
||||
return response.dump();
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ public:
|
||||
std::optional<IAccounts::Info> GetAccountInfo(const std::string_view username) override;
|
||||
void InsertNewCharacter(const ICharInfo::Info info) override;
|
||||
void InsertCharacterXml(const LWOOBJID accountId, const std::string_view lxfml) override;
|
||||
std::string GetCharactersTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) 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;
|
||||
@@ -77,6 +78,7 @@ public:
|
||||
void RemoveModel(const LWOOBJID& modelId) override;
|
||||
void UpdatePerformanceCost(const LWOZONEID& zoneId, const float performanceCost) override;
|
||||
void InsertNewBugReport(const IBugReports::Info& info) override;
|
||||
std::string GetBugReportsTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) override;
|
||||
void InsertCheatDetection(const IPlayerCheatDetections::Info& info) override;
|
||||
void InsertNewMail(const MailInfo& mail) override;
|
||||
void InsertNewUgcModel(
|
||||
@@ -101,6 +103,7 @@ public:
|
||||
void InsertDefaultPersistentId() override;
|
||||
std::optional<uint32_t> GetDonationTotal(const uint32_t activityId) override;
|
||||
std::optional<bool> IsPlaykeyActive(const int32_t playkeyId) override;
|
||||
std::string GetPlayKeysTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) override;
|
||||
std::vector<IUgc::Model> GetUgcModels(const LWOOBJID& propertyId) override;
|
||||
void AddIgnore(const LWOOBJID playerId, const LWOOBJID ignoredPlayerId) override;
|
||||
void RemoveIgnore(const LWOOBJID playerId, const LWOOBJID ignoredPlayerId) override;
|
||||
@@ -124,10 +127,19 @@ public:
|
||||
void InsertUgcBuild(const std::string& modules, const LWOOBJID bigId, const std::optional<LWOOBJID> characterId) override;
|
||||
void DeleteUgcBuild(const LWOOBJID bigId) override;
|
||||
uint32_t GetAccountCount() override;
|
||||
uint32_t GetCharacterCount() override;
|
||||
void RecordFailedAttempt(const uint32_t accountId) override;
|
||||
void ClearFailedAttempts(const uint32_t accountId) override;
|
||||
void SetLockout(const uint32_t accountId, const int64_t lockoutUntil) override;
|
||||
bool IsLockedOut(const uint32_t accountId) override;
|
||||
uint8_t GetFailedAttempts(const uint32_t accountId) override;
|
||||
nlohmann::json GetAccountsTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) override;
|
||||
nlohmann::json GetAccountById(uint32_t accountId) override;
|
||||
bool IsNameInUse(const std::string_view name) 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;
|
||||
std::string GetPropertiesTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) override;
|
||||
private:
|
||||
CppSQLite3Statement CreatePreppedStmt(const std::string& query);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "eGameMasterLevel.h"
|
||||
#include "Database.h"
|
||||
#include "json.hpp"
|
||||
|
||||
std::optional<IAccounts::Info> SQLiteDatabase::GetAccountInfo(const std::string_view username) {
|
||||
auto [_, result] = ExecuteSelect("SELECT * FROM accounts WHERE name = ? LIMIT 1", username);
|
||||
@@ -48,3 +49,137 @@ uint32_t SQLiteDatabase::GetAccountCount() {
|
||||
|
||||
return res.getIntField("count");
|
||||
}
|
||||
void SQLiteDatabase::RecordFailedAttempt(const uint32_t accountId) {
|
||||
ExecuteUpdate("UPDATE accounts SET failed_attempts = failed_attempts + 1 WHERE id = ?;", accountId);
|
||||
}
|
||||
|
||||
void SQLiteDatabase::ClearFailedAttempts(const uint32_t accountId) {
|
||||
ExecuteUpdate("UPDATE accounts SET failed_attempts = 0, lockout_time = NULL, last_login = CURRENT_TIMESTAMP WHERE id = ?;", accountId);
|
||||
}
|
||||
|
||||
void SQLiteDatabase::SetLockout(const uint32_t accountId, const int64_t lockoutUntil) {
|
||||
ExecuteUpdate("UPDATE accounts SET lockout_time = datetime(?, 'unixepoch') WHERE id = ?;", lockoutUntil, accountId);
|
||||
}
|
||||
|
||||
bool SQLiteDatabase::IsLockedOut(const uint32_t accountId) {
|
||||
auto [_, result] = ExecuteSelect("SELECT lockout_time FROM accounts WHERE id = ?;", accountId);
|
||||
if (result.eof()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const char* lockoutTime = result.getStringField("lockout_time");
|
||||
if (lockoutTime == nullptr || strlen(lockoutTime) == 0 || strcmp(lockoutTime, "0") == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If lockout_time is set and in the future, account is locked
|
||||
// For now, simplified check - if lockout_time exists, it's locked
|
||||
return true;
|
||||
}
|
||||
|
||||
uint8_t SQLiteDatabase::GetFailedAttempts(const uint32_t accountId) {
|
||||
auto [_, result] = ExecuteSelect("SELECT failed_attempts FROM accounts WHERE id = ?;", accountId);
|
||||
if (result.eof()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return result.getIntField("failed_attempts");
|
||||
}
|
||||
|
||||
nlohmann::json SQLiteDatabase::GetAccountsTable(uint32_t start, uint32_t length, const std::string_view search, uint32_t orderColumn, bool orderAsc) {
|
||||
// Build base query
|
||||
std::string baseQuery = "SELECT id, name, banned, locked, gm_level, mute_expire, created_at FROM accounts";
|
||||
std::string whereClause;
|
||||
std::string orderClause;
|
||||
|
||||
// Add search filter if provided
|
||||
if (!search.empty()) {
|
||||
whereClause = " WHERE name LIKE '%' || ? || '%'";
|
||||
}
|
||||
|
||||
// Map column indices to database columns
|
||||
std::string orderColumnName = "id";
|
||||
switch (orderColumn) {
|
||||
case 0: orderColumnName = "id"; break;
|
||||
case 1: orderColumnName = "name"; break;
|
||||
case 2: orderColumnName = "banned"; break;
|
||||
case 3: orderColumnName = "locked"; break;
|
||||
case 4: orderColumnName = "gm_level"; break;
|
||||
case 5: orderColumnName = "mute_expire"; break;
|
||||
case 6: orderColumnName = "created_at"; break;
|
||||
default: orderColumnName = "id";
|
||||
}
|
||||
|
||||
orderClause = " ORDER BY " + orderColumnName + (orderAsc ? " ASC" : " DESC");
|
||||
|
||||
// Build the main query
|
||||
std::string mainQuery = baseQuery + whereClause + orderClause + " LIMIT ? OFFSET ?;";
|
||||
|
||||
// Get total count
|
||||
std::string totalCountQuery = "SELECT COUNT(*) as count FROM accounts;";
|
||||
auto [_, totalCountResult] = ExecuteSelect(totalCountQuery);
|
||||
uint32_t totalRecords = totalCountResult.eof() ? 0 : totalCountResult.getIntField("count");
|
||||
|
||||
// Get filtered count
|
||||
uint32_t filteredRecords = totalRecords;
|
||||
if (!search.empty()) {
|
||||
std::string filteredCountQuery = "SELECT COUNT(*) as count FROM accounts WHERE name LIKE '%' || ? || '%';";
|
||||
auto [__, filteredCountResult] = ExecuteSelect(filteredCountQuery, search);
|
||||
filteredRecords = filteredCountResult.eof() ? 0 : filteredCountResult.getIntField("count");
|
||||
}
|
||||
|
||||
// Execute main query
|
||||
auto [stmt, result] = !search.empty() ?
|
||||
ExecuteSelect(mainQuery, search, length, start) :
|
||||
ExecuteSelect(mainQuery, length, start);
|
||||
|
||||
// Build response JSON
|
||||
nlohmann::json accountsArray = nlohmann::json::array();
|
||||
|
||||
while (!result.eof()) {
|
||||
nlohmann::json account = {
|
||||
{"id", result.getIntField("id")},
|
||||
{"name", result.getStringField("name")},
|
||||
{"banned", result.getIntField("banned")},
|
||||
{"locked", result.getIntField("locked")},
|
||||
{"gm_level", result.getIntField("gm_level")},
|
||||
{"mute_expire", result.getInt64Field("mute_expire")},
|
||||
{"created_at", result.getStringField("created_at")}
|
||||
};
|
||||
accountsArray.push_back(account);
|
||||
result.nextRow();
|
||||
}
|
||||
|
||||
nlohmann::json response = {
|
||||
{"draw", 1},
|
||||
{"recordsTotal", totalRecords},
|
||||
{"recordsFiltered", filteredRecords},
|
||||
{"data", accountsArray}
|
||||
};
|
||||
|
||||
return response;
|
||||
}
|
||||
nlohmann::json SQLiteDatabase::GetAccountById(uint32_t accountId) {
|
||||
try {
|
||||
auto [_, result] = ExecuteSelect("SELECT * FROM accounts WHERE id = ? LIMIT 1;", accountId);
|
||||
|
||||
if (result.eof()) {
|
||||
return nlohmann::json{{"error", "Account not found"}};
|
||||
}
|
||||
|
||||
nlohmann::json account = {
|
||||
{"id", result.getIntField("id")},
|
||||
{"name", result.getStringField("name")},
|
||||
{"banned", result.getIntField("banned")},
|
||||
{"locked", result.getIntField("locked")},
|
||||
{"gm_level", result.getIntField("gm_level")},
|
||||
{"mute_expire", result.getInt64Field("mute_expire")},
|
||||
{"created_at", result.getStringField("created_at")}
|
||||
};
|
||||
|
||||
return account;
|
||||
} catch (const CppSQLite3Exception& e) {
|
||||
LOG_DEBUG("SQLite Error: %s", e.errorMessage());
|
||||
return nlohmann::json{{"error", "Database error"}};
|
||||
}
|
||||
}
|
||||
@@ -4,3 +4,75 @@ void SQLiteDatabase::InsertNewBugReport(const IBugReports::Info& info) {
|
||||
ExecuteInsert("INSERT INTO `bug_reports`(body, client_version, other_player_id, selection, reporter_id) VALUES (?, ?, ?, ?, ?)",
|
||||
info.body, info.clientVersion, info.otherPlayer, info.selection, info.characterId);
|
||||
}
|
||||
|
||||
#include "json.hpp"
|
||||
|
||||
std::string SQLiteDatabase::GetBugReportsTable(uint32_t start, uint32_t length, const std::string_view search, uint32_t orderColumn, bool orderAsc) {
|
||||
// Build base query
|
||||
std::string baseQuery = "SELECT id, body, client_version, other_player_id, selection, submitted FROM bug_reports";
|
||||
std::string whereClause;
|
||||
std::string orderClause;
|
||||
|
||||
// Add search filter if provided
|
||||
if (!search.empty()) {
|
||||
whereClause = " WHERE body LIKE '%' || ? || '%' OR other_player_id LIKE '%' || ? || '%'";
|
||||
}
|
||||
|
||||
// Map column indices to database columns
|
||||
std::string orderColumnName = "id";
|
||||
switch (orderColumn) {
|
||||
case 0: orderColumnName = "id"; break;
|
||||
case 1: orderColumnName = "other_player_id"; break;
|
||||
case 2: orderColumnName = "client_version"; break;
|
||||
case 3: orderColumnName = "submitted"; break;
|
||||
default: orderColumnName = "id";
|
||||
}
|
||||
|
||||
orderClause = " ORDER BY " + orderColumnName + (orderAsc ? " ASC" : " DESC");
|
||||
|
||||
// Build the main query
|
||||
std::string mainQuery = baseQuery + whereClause + orderClause + " LIMIT ? OFFSET ?;";
|
||||
|
||||
// Get total count
|
||||
std::string totalCountQuery = "SELECT COUNT(*) as count FROM bug_reports;";
|
||||
auto [__, totalCountResult] = ExecuteSelect(totalCountQuery);
|
||||
uint32_t totalRecords = totalCountResult.eof() ? 0 : totalCountResult.getIntField("count");
|
||||
|
||||
// Get filtered count
|
||||
uint32_t filteredRecords = totalRecords;
|
||||
if (!search.empty()) {
|
||||
std::string filteredCountQuery = "SELECT COUNT(*) as count FROM bug_reports WHERE body LIKE '%' || ? || '%' OR other_player_id LIKE '%' || ? || '%';";
|
||||
auto [___, filteredCountResult] = ExecuteSelect(filteredCountQuery, search, search);
|
||||
filteredRecords = filteredCountResult.eof() ? 0 : filteredCountResult.getIntField("count");
|
||||
}
|
||||
|
||||
// Execute main query
|
||||
auto [stmt, result] = !search.empty() ?
|
||||
ExecuteSelect(mainQuery, search, search, length, start) :
|
||||
ExecuteSelect(mainQuery, length, start);
|
||||
|
||||
// Build response JSON
|
||||
nlohmann::json reportsArray = nlohmann::json::array();
|
||||
|
||||
while (!result.eof()) {
|
||||
nlohmann::json report = {
|
||||
{"id", result.getIntField("id")},
|
||||
{"other_player_id", result.getStringField("other_player_id")},
|
||||
{"client_version", result.getStringField("client_version")},
|
||||
{"selection", result.getStringField("selection")},
|
||||
{"submitted", result.getStringField("submitted")},
|
||||
{"body", result.getStringField("body")}
|
||||
};
|
||||
reportsArray.push_back(report);
|
||||
result.nextRow();
|
||||
}
|
||||
|
||||
nlohmann::json response = {
|
||||
{"draw", 0},
|
||||
{"recordsTotal", totalRecords},
|
||||
{"recordsFiltered", filteredRecords},
|
||||
{"data", reportsArray}
|
||||
};
|
||||
|
||||
return response.dump();
|
||||
}
|
||||
|
||||
@@ -55,6 +55,13 @@ std::vector<LWOOBJID> SQLiteDatabase::GetAccountCharacterIds(const LWOOBJID acco
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
uint32_t SQLiteDatabase::GetCharacterCount() {
|
||||
auto [_, res] = ExecuteSelect("SELECT COUNT(*) as count FROM charinfo;");
|
||||
if (res.eof()) return 0;
|
||||
|
||||
return res.getIntField("count");
|
||||
}
|
||||
|
||||
void SQLiteDatabase::InsertNewCharacter(const ICharInfo::Info info) {
|
||||
ExecuteInsert(
|
||||
"INSERT INTO `charinfo`(`id`, `account_id`, `name`, `pending_name`, `needs_rename`, `last_login`, `prop_clone_id`) VALUES (?,?,?,?,?,?,(SELECT IFNULL(MAX(`prop_clone_id`), 0) + 1 FROM `charinfo`))",
|
||||
|
||||
@@ -17,3 +17,73 @@ void SQLiteDatabase::UpdateCharacterXml(const LWOOBJID charId, const std::string
|
||||
void SQLiteDatabase::InsertCharacterXml(const LWOOBJID characterId, const std::string_view lxfml) {
|
||||
ExecuteInsert("INSERT INTO `charxml` (`id`, `xml_data`) VALUES (?,?)", characterId, lxfml);
|
||||
}
|
||||
|
||||
#include "json.hpp"
|
||||
|
||||
std::string SQLiteDatabase::GetCharactersTable(uint32_t start, uint32_t length, const std::string_view search, uint32_t orderColumn, bool orderAsc) {
|
||||
// Build base query
|
||||
std::string baseQuery = "SELECT c.id, c.name, c.account_id, c.last_login, a.name as account_name FROM charinfo c JOIN accounts a ON c.account_id = a.id";
|
||||
std::string whereClause;
|
||||
std::string orderClause;
|
||||
|
||||
// Add search filter if provided
|
||||
if (!search.empty()) {
|
||||
whereClause = " WHERE c.name LIKE '%' || ? || '%'";
|
||||
}
|
||||
|
||||
// Map column indices to database columns
|
||||
std::string orderColumnName = "c.id";
|
||||
switch (orderColumn) {
|
||||
case 0: orderColumnName = "c.id"; break;
|
||||
case 1: orderColumnName = "c.name"; break;
|
||||
case 2: orderColumnName = "a.name"; break;
|
||||
case 3: orderColumnName = "c.last_login"; break;
|
||||
default: orderColumnName = "c.id";
|
||||
}
|
||||
|
||||
orderClause = " ORDER BY " + orderColumnName + (orderAsc ? " ASC" : " DESC");
|
||||
|
||||
// Build the main query
|
||||
std::string mainQuery = baseQuery + whereClause + orderClause + " LIMIT ? OFFSET ?;";
|
||||
|
||||
// Get total count
|
||||
std::string totalCountQuery = "SELECT COUNT(*) as count FROM charinfo;";
|
||||
auto [__, totalCountResult] = ExecuteSelect(totalCountQuery);
|
||||
uint32_t totalRecords = totalCountResult.eof() ? 0 : totalCountResult.getIntField("count");
|
||||
|
||||
// Get filtered count
|
||||
uint32_t filteredRecords = totalRecords;
|
||||
if (!search.empty()) {
|
||||
std::string filteredCountQuery = "SELECT COUNT(*) as count FROM charinfo WHERE name LIKE '%' || ? || '%';";
|
||||
auto [___, filteredCountResult] = ExecuteSelect(filteredCountQuery, search);
|
||||
filteredRecords = filteredCountResult.eof() ? 0 : filteredCountResult.getIntField("count");
|
||||
}
|
||||
|
||||
// Execute main query
|
||||
auto [stmt, result] = !search.empty() ?
|
||||
ExecuteSelect(mainQuery, search, length, start) :
|
||||
ExecuteSelect(mainQuery, length, start);
|
||||
|
||||
// Build response JSON
|
||||
nlohmann::json charactersArray = nlohmann::json::array();
|
||||
|
||||
while (!result.eof()) {
|
||||
nlohmann::json character = {
|
||||
{"id", result.getInt64Field("id")},
|
||||
{"name", result.getStringField("name")},
|
||||
{"account_name", result.getStringField("account_name")},
|
||||
{"last_login", result.getInt64Field("last_login")}
|
||||
};
|
||||
charactersArray.push_back(character);
|
||||
result.nextRow();
|
||||
}
|
||||
|
||||
nlohmann::json response = {
|
||||
{"draw", 0},
|
||||
{"recordsTotal", totalRecords},
|
||||
{"recordsFiltered", filteredRecords},
|
||||
{"data", charactersArray}
|
||||
};
|
||||
|
||||
return response.dump();
|
||||
}
|
||||
|
||||
@@ -9,3 +9,75 @@ std::optional<bool> SQLiteDatabase::IsPlaykeyActive(const int32_t playkeyId) {
|
||||
|
||||
return keyCheckRes.getIntField("active");
|
||||
}
|
||||
|
||||
#include "json.hpp"
|
||||
|
||||
std::string SQLiteDatabase::GetPlayKeysTable(uint32_t start, uint32_t length, const std::string_view search, uint32_t orderColumn, bool orderAsc) {
|
||||
// Build base query
|
||||
std::string baseQuery = "SELECT id, key_string, key_uses, created_at, active FROM play_keys";
|
||||
std::string whereClause;
|
||||
std::string orderClause;
|
||||
|
||||
// Add search filter if provided
|
||||
if (!search.empty()) {
|
||||
whereClause = " WHERE key_string LIKE '%' || ? || '%'";
|
||||
}
|
||||
|
||||
// Map column indices to database columns
|
||||
std::string orderColumnName = "id";
|
||||
switch (orderColumn) {
|
||||
case 0: orderColumnName = "id"; break;
|
||||
case 1: orderColumnName = "key_string"; break;
|
||||
case 2: orderColumnName = "key_uses"; break;
|
||||
case 3: orderColumnName = "created_at"; break;
|
||||
case 4: orderColumnName = "active"; break;
|
||||
default: orderColumnName = "id";
|
||||
}
|
||||
|
||||
orderClause = " ORDER BY " + orderColumnName + (orderAsc ? " ASC" : " DESC");
|
||||
|
||||
// Build the main query
|
||||
std::string mainQuery = baseQuery + whereClause + orderClause + " LIMIT ? OFFSET ?;";
|
||||
|
||||
// Get total count
|
||||
std::string totalCountQuery = "SELECT COUNT(*) as count FROM play_keys;";
|
||||
auto [__, totalCountResult] = ExecuteSelect(totalCountQuery);
|
||||
uint32_t totalRecords = totalCountResult.eof() ? 0 : totalCountResult.getIntField("count");
|
||||
|
||||
// Get filtered count
|
||||
uint32_t filteredRecords = totalRecords;
|
||||
if (!search.empty()) {
|
||||
std::string filteredCountQuery = "SELECT COUNT(*) as count FROM play_keys WHERE key_string LIKE '%' || ? || '%';";
|
||||
auto [___, filteredCountResult] = ExecuteSelect(filteredCountQuery, search);
|
||||
filteredRecords = filteredCountResult.eof() ? 0 : filteredCountResult.getIntField("count");
|
||||
}
|
||||
|
||||
// Execute main query
|
||||
auto [stmt, result] = !search.empty() ?
|
||||
ExecuteSelect(mainQuery, search, length, start) :
|
||||
ExecuteSelect(mainQuery, length, start);
|
||||
|
||||
// Build response JSON
|
||||
nlohmann::json keysArray = nlohmann::json::array();
|
||||
|
||||
while (!result.eof()) {
|
||||
nlohmann::json key = {
|
||||
{"id", result.getIntField("id")},
|
||||
{"key_string", result.getStringField("key_string")},
|
||||
{"key_uses", result.getIntField("key_uses")},
|
||||
{"created_at", result.getStringField("created_at")},
|
||||
{"active", result.getIntField("active")}
|
||||
};
|
||||
keysArray.push_back(key);
|
||||
result.nextRow();
|
||||
}
|
||||
|
||||
nlohmann::json response = {
|
||||
{"draw", 0},
|
||||
{"recordsTotal", totalRecords},
|
||||
{"recordsFiltered", filteredRecords},
|
||||
{"data", keysArray}
|
||||
};
|
||||
|
||||
return response.dump();
|
||||
}
|
||||
|
||||
@@ -197,3 +197,77 @@ std::optional<IProperty::Info> SQLiteDatabase::GetPropertyInfo(const LWOOBJID id
|
||||
|
||||
return ReadPropertyInfo(propertyEntry);
|
||||
}
|
||||
|
||||
#include "json.hpp"
|
||||
|
||||
std::string SQLiteDatabase::GetPropertiesTable(uint32_t start, uint32_t length, const std::string_view search, uint32_t orderColumn, bool orderAsc) {
|
||||
// Build base query
|
||||
std::string baseQuery = "SELECT id, owner_id, name, mod_approved, reputation, zone_id FROM properties";
|
||||
std::string whereClause;
|
||||
std::string orderClause;
|
||||
|
||||
// Add search filter if provided
|
||||
if (!search.empty()) {
|
||||
whereClause = " WHERE name LIKE '%' || ? || '%'";
|
||||
}
|
||||
|
||||
// Map column indices to database columns
|
||||
std::string orderColumnName = "id";
|
||||
switch (orderColumn) {
|
||||
case 0: orderColumnName = "id"; break;
|
||||
case 1: orderColumnName = "name"; break;
|
||||
case 2: orderColumnName = "owner_id"; break;
|
||||
case 3: orderColumnName = "mod_approved"; break;
|
||||
case 4: orderColumnName = "reputation"; break;
|
||||
case 5: orderColumnName = "zone_id"; break;
|
||||
default: orderColumnName = "id";
|
||||
}
|
||||
|
||||
orderClause = " ORDER BY " + orderColumnName + (orderAsc ? " ASC" : " DESC");
|
||||
|
||||
// Build the main query
|
||||
std::string mainQuery = baseQuery + whereClause + orderClause + " LIMIT ? OFFSET ?;";
|
||||
|
||||
// Get total count
|
||||
std::string totalCountQuery = "SELECT COUNT(*) as count FROM properties;";
|
||||
auto [__, totalCountResult] = ExecuteSelect(totalCountQuery);
|
||||
uint32_t totalRecords = totalCountResult.eof() ? 0 : totalCountResult.getIntField("count");
|
||||
|
||||
// Get filtered count
|
||||
uint32_t filteredRecords = totalRecords;
|
||||
if (!search.empty()) {
|
||||
std::string filteredCountQuery = "SELECT COUNT(*) as count FROM properties WHERE name LIKE '%' || ? || '%';";
|
||||
auto [___, filteredCountResult] = ExecuteSelect(filteredCountQuery, search);
|
||||
filteredRecords = filteredCountResult.eof() ? 0 : filteredCountResult.getIntField("count");
|
||||
}
|
||||
|
||||
// Execute main query
|
||||
auto [stmt, result] = !search.empty() ?
|
||||
ExecuteSelect(mainQuery, search, length, start) :
|
||||
ExecuteSelect(mainQuery, length, start);
|
||||
|
||||
// Build response JSON
|
||||
nlohmann::json propertiesArray = nlohmann::json::array();
|
||||
|
||||
while (!result.eof()) {
|
||||
nlohmann::json property = {
|
||||
{"id", result.getInt64Field("id")},
|
||||
{"owner_id", result.getInt64Field("owner_id")},
|
||||
{"name", result.getStringField("name")},
|
||||
{"mod_approved", result.getIntField("mod_approved")},
|
||||
{"reputation", result.getInt64Field("reputation")},
|
||||
{"zone_id", result.getIntField("zone_id")}
|
||||
};
|
||||
propertiesArray.push_back(property);
|
||||
result.nextRow();
|
||||
}
|
||||
|
||||
nlohmann::json response = {
|
||||
{"draw", 0},
|
||||
{"recordsTotal", totalRecords},
|
||||
{"recordsFiltered", filteredRecords},
|
||||
{"data", propertiesArray}
|
||||
};
|
||||
|
||||
return response.dump();
|
||||
}
|
||||
|
||||
@@ -103,6 +103,18 @@ class TestSQLDatabase : public GameDatabase {
|
||||
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 { return 0; };
|
||||
uint32_t GetCharacterCount() override { return 0; };
|
||||
void RecordFailedAttempt(const uint32_t accountId) override {};
|
||||
void ClearFailedAttempts(const uint32_t accountId) override {};
|
||||
void SetLockout(const uint32_t accountId, const int64_t lockoutUntil) override {};
|
||||
bool IsLockedOut(const uint32_t accountId) override { return false; };
|
||||
uint8_t GetFailedAttempts(const uint32_t accountId) override { return 0; };
|
||||
nlohmann::json GetAccountsTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) override { return nlohmann::json::object(); };
|
||||
nlohmann::json GetAccountById(uint32_t accountId) override { return nlohmann::json::object(); };
|
||||
std::string GetCharactersTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) override { return "{}"; };
|
||||
std::string GetPlayKeysTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) override { return "{}"; };
|
||||
std::string GetPropertiesTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) override { return "{}"; };
|
||||
std::string GetBugReportsTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) override { return "{}"; };
|
||||
|
||||
bool IsNameInUse(const std::string_view name) override { return false; };
|
||||
std::optional<IPropertyContents::Model> GetModel(const LWOOBJID modelID) override { return {}; }
|
||||
|
||||
@@ -1019,6 +1019,7 @@ void Entity::WriteBaseReplicaData(RakNet::BitStream& outBitStream, eReplicaPacke
|
||||
const bool hasParent = m_ParentEntity != nullptr || m_SpawnerID != 0;
|
||||
outBitStream.Write(hasParent);
|
||||
if (hasParent) {
|
||||
// 触るな!
|
||||
if (m_ParentEntity != nullptr) outBitStream.Write(GeneralUtils::SetBit(m_ParentEntity->GetObjectID(), static_cast<uint32_t>(eObjectBits::CLIENT)));
|
||||
else if (m_Spawner != nullptr && m_Spawner->m_Info.isNetwork) outBitStream.Write(m_SpawnerID);
|
||||
else outBitStream.Write(GeneralUtils::SetBit(m_SpawnerID, static_cast<uint32_t>(eObjectBits::CLIENT)));
|
||||
@@ -2235,11 +2236,16 @@ bool Entity::MsgRequestServerObjectInfo(GameMessages::RequestServerObjectInfo& r
|
||||
|
||||
const auto& objTableInfo = table->GetByID(GetLOT());
|
||||
|
||||
objectInfo.PushDebug<AMFStringValue>("Name") = objTableInfo.name;
|
||||
objectInfo.PushDebug<AMFIntValue>("Template ID(LOT)") = GetLOT();
|
||||
objectInfo.PushDebug<AMFStringValue>("Object ID") = std::to_string(GetObjectID());
|
||||
objectInfo.PushDebug<AMFStringValue>("Spawner's Object ID") = std::to_string(GetSpawnerID());
|
||||
objectInfo.PushDebug<AMFStringValue>("Owner override") = std::to_string(m_OwnerOverride);
|
||||
objectInfo.PushDebug<AMFStringValue>("Name", "name") = objTableInfo.name;
|
||||
objectInfo.PushDebug<AMFIntValue>("Template ID(LOT)", "LOT") = GetLOT();
|
||||
objectInfo.PushDebug<AMFStringValue>("Object ID", "LWOOBJID") = std::to_string(GetObjectID());
|
||||
objectInfo.PushDebug<AMFStringValue>("Spawner's Object ID", "LWOOBJID") = std::to_string(GetSpawnerID());
|
||||
objectInfo.PushDebug<AMFStringValue>("Owner override", "LWOOBJID") = std::to_string(m_OwnerOverride);
|
||||
auto& children = objectInfo.PushDebug("Child Objects");
|
||||
int i = 1;
|
||||
for (const auto* child : m_ChildEntities) {
|
||||
if (child) children.PushDebug<AMFStringValue>("Child " + std::to_string(i++), "LWOOBJID") = std::to_string(child->GetObjectID());
|
||||
}
|
||||
|
||||
auto& componentDetails = objectInfo.PushDebug("Component Information");
|
||||
for (const auto [id, component] : m_Components) {
|
||||
|
||||
@@ -521,6 +521,10 @@ void BaseCombatAIComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsI
|
||||
|
||||
void BaseCombatAIComponent::SetAiState(AiState newState) {
|
||||
if (newState == this->m_State) return;
|
||||
GameMessages::NotifyCombatAIStateChange stateMsg;
|
||||
stateMsg.prevState = this->m_State;
|
||||
stateMsg.newState = newState;
|
||||
m_Parent->HandleMsg(stateMsg);
|
||||
this->m_State = newState;
|
||||
m_DirtyStateOrTarget = true;
|
||||
Game::entityManager->SerializeEntity(m_Parent);
|
||||
@@ -860,7 +864,7 @@ bool BaseCombatAIComponent::MsgGetObjectReportInfo(GameMessages::GetObjectReport
|
||||
auto& cmptType = reportInfo.info->PushDebug("Base Combat AI");
|
||||
cmptType.PushDebug<AMFIntValue>("Component ID") = GetComponentID();
|
||||
auto& targetInfo = cmptType.PushDebug("Current Target Info");
|
||||
targetInfo.PushDebug<AMFStringValue>("Current Target ID") = std::to_string(m_Target);
|
||||
targetInfo.PushDebug<AMFStringValue>("Current Target ID", "LWOOBJID") = std::to_string(m_Target);
|
||||
// if (m_Target != LWOOBJID_EMPTY) {
|
||||
// LWOGameMessages::ObjGetName nameMsg(m_CurrentTarget);
|
||||
// SEND_GAMEOBJ_MSG(nameMsg);
|
||||
@@ -901,10 +905,7 @@ bool BaseCombatAIComponent::MsgGetObjectReportInfo(GameMessages::GetObjectReport
|
||||
//}
|
||||
//cmptType.PushDebug("Current Combat Role") = curState;
|
||||
|
||||
auto& tetherPoint = cmptType.PushDebug("Tether Point");
|
||||
tetherPoint.PushDebug<AMFDoubleValue>("X") = m_StartPosition.x;
|
||||
tetherPoint.PushDebug<AMFDoubleValue>("Y") = m_StartPosition.y;
|
||||
tetherPoint.PushDebug<AMFDoubleValue>("Z") = m_StartPosition.z;
|
||||
cmptType.PushDebug("Tether Point").PushDebug(m_StartPosition);
|
||||
cmptType.PushDebug<AMFDoubleValue>("Hard Tether Radius") = m_HardTetherRadius;
|
||||
cmptType.PushDebug<AMFDoubleValue>("Soft Tether Radius") = m_SoftTetherRadius;
|
||||
cmptType.PushDebug<AMFDoubleValue>("Aggro Radius") = m_AggroRadius;
|
||||
|
||||
@@ -361,17 +361,11 @@ void ControllablePhysicsComponent::SetStunImmunity(
|
||||
|
||||
bool ControllablePhysicsComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) {
|
||||
PhysicsComponent::OnGetObjectReportInfo(reportInfo);
|
||||
auto& info = reportInfo.subCategory->PushDebug("Controllable Info");
|
||||
auto& info = reportInfo.subCategory->PushDebug("Controllable Physics");
|
||||
|
||||
auto& vel = info.PushDebug("Velocity");
|
||||
vel.PushDebug<AMFDoubleValue>("x") = m_Velocity.x;
|
||||
vel.PushDebug<AMFDoubleValue>("y") = m_Velocity.y;
|
||||
vel.PushDebug<AMFDoubleValue>("z") = m_Velocity.z;
|
||||
info.PushDebug("Velocity").PushDebug(m_Velocity);
|
||||
|
||||
auto& angularVelocity = info.PushDebug("Angular Velocity");
|
||||
angularVelocity.PushDebug<AMFDoubleValue>("x") = m_AngularVelocity.x;
|
||||
angularVelocity.PushDebug<AMFDoubleValue>("y") = m_AngularVelocity.y;
|
||||
angularVelocity.PushDebug<AMFDoubleValue>("z") = m_AngularVelocity.z;
|
||||
info.PushDebug("Angular Velocity").PushDebug(m_AngularVelocity);
|
||||
|
||||
info.PushDebug<AMFBoolValue>("Is On Ground") = m_IsOnGround;
|
||||
info.PushDebug<AMFBoolValue>("Is On Rail") = m_IsOnRail;
|
||||
@@ -403,12 +397,13 @@ bool ControllablePhysicsComponent::OnGetObjectReportInfo(GameMessages::GetObject
|
||||
info.PushDebug<AMFBoolValue>("Is In Bubble") = m_IsInBubble;
|
||||
info.PushDebug<AMFStringValue>("Bubble Type") = StringifiedEnum::ToString(m_BubbleType).data();
|
||||
info.PushDebug<AMFBoolValue>("Special Anims") = m_SpecialAnims;
|
||||
info.PushDebug<AMFIntValue>("Immune To Stun Attack Count") = m_ImmuneToStunAttackCount;
|
||||
info.PushDebug<AMFIntValue>("Immune To Stun Equip Count") = m_ImmuneToStunEquipCount;
|
||||
info.PushDebug<AMFIntValue>("Immune To Stun Interact Count") = m_ImmuneToStunInteractCount;
|
||||
info.PushDebug<AMFIntValue>("Immune To Stun Jump Count") = m_ImmuneToStunJumpCount;
|
||||
info.PushDebug<AMFIntValue>("Immune To Stun Move Count") = m_ImmuneToStunMoveCount;
|
||||
info.PushDebug<AMFIntValue>("Immune To Stun Turn Count") = m_ImmuneToStunTurnCount;
|
||||
info.PushDebug<AMFIntValue>("Immune To Stun UseItem Count") = m_ImmuneToStunUseItemCount;
|
||||
auto& immunity = info.PushDebug("Immunity Effects");
|
||||
immunity.PushDebug<AMFBoolValue>("Immune to Stun Move") = m_ImmuneToStunMoveCount != 0;
|
||||
immunity.PushDebug<AMFBoolValue>("Immune to Stun Turn") = m_ImmuneToStunTurnCount != 0;
|
||||
immunity.PushDebug<AMFBoolValue>("Immune to Stun Attack") = m_ImmuneToStunAttackCount != 0;
|
||||
immunity.PushDebug<AMFBoolValue>("Immune to Stun Use Item") = m_ImmuneToStunUseItemCount != 0;
|
||||
immunity.PushDebug<AMFBoolValue>("Immune to Stun Equip") = m_ImmuneToStunEquipCount != 0;
|
||||
immunity.PushDebug<AMFBoolValue>("Immune to Stun Interact") = m_ImmuneToStunInteractCount != 0;
|
||||
immunity.PushDebug<AMFBoolValue>("Immune to Stun Jump") = m_ImmuneToStunJumpCount != 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -328,7 +328,7 @@ private:
|
||||
/**
|
||||
* The effect that plays while using the jetpack
|
||||
*/
|
||||
int32_t m_JetpackEffectID;
|
||||
int32_t m_JetpackEffectID{};
|
||||
|
||||
/**
|
||||
* The current speed multiplier, allowing an entity to run faster
|
||||
|
||||
@@ -1146,7 +1146,7 @@ bool DestroyableComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportIn
|
||||
destroyableInfo.PushDebug<AMFDoubleValue>("Explode Factor") = m_ExplodeFactor;
|
||||
destroyableInfo.PushDebug<AMFBoolValue>("Has Threats") = m_HasThreats;
|
||||
|
||||
destroyableInfo.PushDebug<AMFStringValue>("Killer ID") = std::to_string(m_KillerID);
|
||||
destroyableInfo.PushDebug<AMFStringValue>("Killer ID", "LWOOBJID") = std::to_string(m_KillerID);
|
||||
|
||||
// "Scripts"; idk what to do about scripts yet
|
||||
auto& immuneCounts = destroyableInfo.PushDebug("Immune Counts");
|
||||
|
||||
@@ -108,7 +108,7 @@ bool GhostComponent::OnGetGMInvis(GameMessages::GetGMInvis& gmInvisMsg) {
|
||||
bool GhostComponent::MsgGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportMsg) {
|
||||
auto& cmptType = reportMsg.info->PushDebug("Ghost");
|
||||
cmptType.PushDebug<AMFIntValue>("Component ID") = GetComponentID();
|
||||
cmptType.PushDebug<AMFBoolValue>("Is GM Invis") = false;
|
||||
cmptType.PushDebug<AMFBoolValue>("Is GM Invis") = m_IsGMInvisible;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -110,15 +110,9 @@ bool HavokVehiclePhysicsComponent::OnGetObjectReportInfo(GameMessages::GetObject
|
||||
|
||||
auto& info = reportInfo.subCategory->PushDebug("Havok Vehicle Physics Info");
|
||||
|
||||
auto& velocity = info.PushDebug("Velocity");
|
||||
velocity.PushDebug<AMFDoubleValue>("x") = m_Velocity.x;
|
||||
velocity.PushDebug<AMFDoubleValue>("y") = m_Velocity.y;
|
||||
velocity.PushDebug<AMFDoubleValue>("z") = m_Velocity.z;
|
||||
auto& velocity = info.PushDebug("Velocity").PushDebug(m_Velocity);
|
||||
|
||||
auto& angularVelocity = info.PushDebug("Angular Velocity");
|
||||
angularVelocity.PushDebug<AMFDoubleValue>("x") = m_AngularVelocity.x;
|
||||
angularVelocity.PushDebug<AMFDoubleValue>("y") = m_AngularVelocity.y;
|
||||
angularVelocity.PushDebug<AMFDoubleValue>("z") = m_AngularVelocity.z;
|
||||
auto& angularVelocity = info.PushDebug("Angular Velocity").PushDebug(m_AngularVelocity);
|
||||
|
||||
info.PushDebug<AMFBoolValue>("Is On Ground") = m_IsOnGround;
|
||||
info.PushDebug<AMFBoolValue>("Is On Rail") = m_IsOnRail;
|
||||
|
||||
@@ -723,10 +723,6 @@ void InventoryComponent::Serialize(RakNet::BitStream& outBitStream, const bool b
|
||||
for (const auto& pair : m_Equipped) {
|
||||
const auto item = pair.second;
|
||||
|
||||
if (bIsInitialUpdate) {
|
||||
AddItemSkills(item.lot);
|
||||
}
|
||||
|
||||
outBitStream.Write(item.id);
|
||||
outBitStream.Write(item.lot);
|
||||
|
||||
@@ -1151,14 +1147,12 @@ LOT InventoryComponent::GetConsumable() const {
|
||||
void InventoryComponent::AddItemSkills(const LOT lot) {
|
||||
const auto info = Inventory::FindItemComponent(lot);
|
||||
|
||||
const auto slot = FindBehaviorSlot(static_cast<eItemType>(info.itemType));
|
||||
const auto slot = FindBehaviorSlot(info.equipLocation);
|
||||
|
||||
if (slot == BehaviorSlot::Invalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto index = m_Skills.find(slot);
|
||||
|
||||
const auto skill = FindSkill(lot);
|
||||
|
||||
SetSkill(slot, skill);
|
||||
@@ -1186,7 +1180,7 @@ void InventoryComponent::FixInvisibleItems() {
|
||||
void InventoryComponent::RemoveItemSkills(const LOT lot) {
|
||||
const auto info = Inventory::FindItemComponent(lot);
|
||||
|
||||
const auto slot = FindBehaviorSlot(static_cast<eItemType>(info.itemType));
|
||||
const auto slot = FindBehaviorSlot(info.equipLocation);
|
||||
|
||||
if (slot == BehaviorSlot::Invalid) {
|
||||
return;
|
||||
@@ -1198,15 +1192,31 @@ void InventoryComponent::RemoveItemSkills(const LOT lot) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto old = index->second;
|
||||
const auto skillId = FindSkill(lot);
|
||||
|
||||
GameMessages::SendRemoveSkill(m_Parent, old);
|
||||
// Only act on this slot if it still holds the skill from this item.
|
||||
// Another item may have overwritten the slot since this one was equipped.
|
||||
if (index->second != skillId) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_Skills.erase(slot);
|
||||
|
||||
// Find another slot that still holds this skillID (if any).
|
||||
const auto surviving = std::ranges::find_if(m_Skills, [skillId](const auto& pair) {
|
||||
return pair.second == skillId;
|
||||
});
|
||||
|
||||
// The client stores one acquiredSkillsInfo entry per skillID, tagged with the slotID
|
||||
// it was originally added with. Always send RemoveSkill to clear that entry, then
|
||||
// re-add with the surviving slot so the client shows it in the correct place.
|
||||
GameMessages::SendRemoveSkill(m_Parent, skillId);
|
||||
if (surviving != m_Skills.end()) {
|
||||
GameMessages::SendAddSkill(m_Parent, skillId, surviving->first);
|
||||
}
|
||||
|
||||
if (slot == BehaviorSlot::Primary) {
|
||||
m_Skills.insert_or_assign(BehaviorSlot::Primary, 1);
|
||||
|
||||
GameMessages::SendAddSkill(m_Parent, 1, BehaviorSlot::Primary);
|
||||
}
|
||||
}
|
||||
@@ -1298,23 +1308,17 @@ void InventoryComponent::RemoveDatabasePet(LWOOBJID id) {
|
||||
m_Pets.erase(id);
|
||||
}
|
||||
|
||||
BehaviorSlot InventoryComponent::FindBehaviorSlot(const eItemType type) {
|
||||
switch (type) {
|
||||
case eItemType::HAT:
|
||||
return BehaviorSlot::Head;
|
||||
case eItemType::NECK:
|
||||
return BehaviorSlot::Neck;
|
||||
case eItemType::LEFT_HAND:
|
||||
return BehaviorSlot::Offhand;
|
||||
case eItemType::RIGHT_HAND:
|
||||
return BehaviorSlot::Primary;
|
||||
case eItemType::CONSUMABLE:
|
||||
return BehaviorSlot::Consumable;
|
||||
default:
|
||||
return BehaviorSlot::Invalid;
|
||||
}
|
||||
BehaviorSlot InventoryComponent::FindBehaviorSlot(const std::string& equipLocation) {
|
||||
// Skill slot is determined by equipLocation, not itemType.
|
||||
// Mapping confirmed against live captures and client data (issue #1339).
|
||||
if (equipLocation == "special_r") return BehaviorSlot::Primary;
|
||||
if (equipLocation == "hair") return BehaviorSlot::Head;
|
||||
if (equipLocation == "special_l") return BehaviorSlot::Offhand;
|
||||
if (equipLocation == "clavicle") return BehaviorSlot::Neck;
|
||||
return BehaviorSlot::Invalid;
|
||||
}
|
||||
|
||||
|
||||
bool InventoryComponent::IsTransferInventory(eInventoryType type, bool includeVault) {
|
||||
return type == VENDOR_BUYBACK || (includeVault && (type == VAULT_ITEMS || type == VAULT_MODELS)) || type == TEMP_ITEMS || type == TEMP_MODELS || type == MODELS_IN_BBB;
|
||||
}
|
||||
@@ -1655,10 +1659,28 @@ bool InventoryComponent::SetSkill(BehaviorSlot slot, uint32_t skillId) {
|
||||
const auto index = m_Skills.find(slot);
|
||||
if (index != m_Skills.end()) {
|
||||
const auto old = index->second;
|
||||
GameMessages::SendRemoveSkill(m_Parent, old);
|
||||
// Only remove the old skill from the client if no other slot still holds it.
|
||||
// The client's acquiredSkillsInfo is keyed by skillID (one entry per skill),
|
||||
// so RemoveSkill clears it globally — sending it while another slot still uses
|
||||
// the same skillID would break that slot on the client.
|
||||
const auto usedElsewhere = std::ranges::any_of(m_Skills, [&](const auto& pair) {
|
||||
return pair.first != slot && pair.second == old;
|
||||
});
|
||||
if (!usedElsewhere) {
|
||||
GameMessages::SendRemoveSkill(m_Parent, old);
|
||||
}
|
||||
}
|
||||
|
||||
// Only send AddSkill if the client doesn't already know about this skillID.
|
||||
// The client early-exits on duplicate AddSkill (same skillID already in
|
||||
// acquiredSkillsInfo) without updating the slot — so only send when it's new.
|
||||
const auto alreadyKnown = std::ranges::any_of(m_Skills, [&](const auto& pair) {
|
||||
return pair.first != slot && pair.second == skillId;
|
||||
});
|
||||
if (!alreadyKnown) {
|
||||
GameMessages::SendAddSkill(m_Parent, skillId, slot);
|
||||
}
|
||||
|
||||
GameMessages::SendAddSkill(m_Parent, skillId, slot);
|
||||
m_Skills.insert_or_assign(slot, skillId);
|
||||
return true;
|
||||
}
|
||||
@@ -1838,9 +1860,9 @@ bool InventoryComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo
|
||||
std::stringstream ss;
|
||||
ss << "%[Objects_" << item->GetLot() << "_name] Slot " << item->GetSlot();
|
||||
auto& slot = curInv.PushDebug(ss.str());
|
||||
slot.PushDebug<AMFStringValue>("Object ID") = std::to_string(item->GetId());
|
||||
slot.PushDebug<AMFIntValue>("LOT") = item->GetLot();
|
||||
if (item->GetSubKey() != LWOOBJID_EMPTY) slot.PushDebug<AMFStringValue>("Subkey") = std::to_string(item->GetSubKey());
|
||||
slot.PushDebug<AMFStringValue>("Object ID", "LWOOBJID") = std::to_string(item->GetId());
|
||||
slot.PushDebug<AMFIntValue>("LOT", "LOT") = item->GetLot();
|
||||
if (item->GetSubKey() != LWOOBJID_EMPTY) slot.PushDebug<AMFStringValue>("Subkey", "LWOOBJID") = std::to_string(item->GetSubKey());
|
||||
slot.PushDebug<AMFIntValue>("Count") = item->GetCount();
|
||||
slot.PushDebug<AMFIntValue>("Slot") = item->GetSlot();
|
||||
slot.PushDebug<AMFBoolValue>("Bind on pickup") = item->GetInfo().isBOP;
|
||||
@@ -1859,7 +1881,8 @@ bool InventoryComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo
|
||||
ss << "%[Objects_" << info.lot << "_name]";
|
||||
auto& equipSlot = equipped.PushDebug(ss.str());
|
||||
equipSlot.PushDebug<AMFStringValue>("Location") = location;
|
||||
equipSlot.PushDebug<AMFStringValue>("Object ID") = std::to_string(info.id);
|
||||
equipSlot.PushDebug<AMFStringValue>("Object ID", "LWOOBJID") = std::to_string(info.id);
|
||||
equipSlot.PushDebug<AMFIntValue>("LOT", "LOT") = info.lot;
|
||||
equipSlot.PushDebug<AMFIntValue>("Slot") = info.slot;
|
||||
equipSlot.PushDebug<AMFIntValue>("Count") = info.count;
|
||||
auto& extra = equipSlot.PushDebug("Extra Info");
|
||||
|
||||
@@ -367,11 +367,10 @@ public:
|
||||
void RemoveDatabasePet(LWOOBJID id);
|
||||
|
||||
/**
|
||||
* Returns the current behavior slot active for the passed item type
|
||||
* @param type the item type to find the behavior slot for
|
||||
* @return the current behavior slot active for the passed item type
|
||||
* Returns the behavior slot for the given equipLocation string.
|
||||
* This is the authoritative mapping used for skill slot assignment.
|
||||
*/
|
||||
static BehaviorSlot FindBehaviorSlot(eItemType type);
|
||||
static BehaviorSlot FindBehaviorSlot(const std::string& equipLocation);
|
||||
|
||||
/**
|
||||
* Checks if the inventory type is a temp inventory
|
||||
@@ -403,6 +402,8 @@ public:
|
||||
|
||||
std::map<BehaviorSlot, uint32_t> GetSkills() { return m_Skills; };
|
||||
|
||||
void ClearSkills() { m_Skills.clear(); };
|
||||
|
||||
bool SetSkill(int slot, uint32_t skillId);
|
||||
bool SetSkill(BehaviorSlot slot, uint32_t skillId);
|
||||
|
||||
|
||||
@@ -652,7 +652,7 @@ void PushMissions(const std::map<uint32_t, Mission*>& missions, AMFArrayValue& V
|
||||
}
|
||||
|
||||
bool MissionComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) {
|
||||
auto& missionInfo = reportInfo.info->PushDebug("Mission (Laggy)");
|
||||
auto& missionInfo = reportInfo.info->PushDebug("Mission (Laggy)", "", 1);
|
||||
missionInfo.PushDebug<AMFIntValue>("Component ID") = GetComponentID();
|
||||
// Sort the missions so they are easier to parse and present to the end user
|
||||
std::map<uint32_t, Mission*> achievements;
|
||||
|
||||
@@ -355,8 +355,8 @@ bool ModelComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& re
|
||||
|
||||
cmptInfo.PushDebug<AMFStringValue>("Name") = "Objects_" + std::to_string(m_Parent->GetLOT()) + "_name";
|
||||
cmptInfo.PushDebug<AMFBoolValue>("Has Unique Name") = false;
|
||||
cmptInfo.PushDebug<AMFStringValue>("UGID (from item)") = std::to_string(m_userModelID);
|
||||
cmptInfo.PushDebug<AMFStringValue>("UGID") = std::to_string(m_userModelID);
|
||||
cmptInfo.PushDebug<AMFStringValue>("UGID (from item)", "LWOOBJID") = std::to_string(m_userModelID);
|
||||
cmptInfo.PushDebug<AMFStringValue>("UGID", "LWOOBJID") = std::to_string(m_userModelID);
|
||||
cmptInfo.PushDebug<AMFStringValue>("Description") = "";
|
||||
cmptInfo.PushDebug<AMFIntValue>("Behavior Count") = m_Behaviors.size();
|
||||
|
||||
|
||||
@@ -535,20 +535,14 @@ bool MovementAIComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInf
|
||||
movementInfo.PushDebug<AMFDoubleValue>("Pulling To Point") = m_PullingToPoint;
|
||||
movementInfo.PushDebug<AMFBoolValue>("At Final Waypoint") = m_AtFinalWaypoint;
|
||||
|
||||
auto& pullPointInfo = movementInfo.PushDebug("Pull Point");
|
||||
pullPointInfo.PushDebug<AMFDoubleValue>("X") = m_PullPoint.x;
|
||||
pullPointInfo.PushDebug<AMFDoubleValue>("Y") = m_PullPoint.y;
|
||||
pullPointInfo.PushDebug<AMFDoubleValue>("Z") = m_PullPoint.z;
|
||||
auto& pullPointInfo = movementInfo.PushDebug("Pull Point").PushDebug(m_PullPoint);
|
||||
|
||||
// movementInfo.PushDebug<AMFDoubleValue>("Delay") = m_Delay;
|
||||
|
||||
auto& waypoints = movementInfo.PushDebug("Interpolated Waypoints");
|
||||
int i = 0;
|
||||
for (const auto& point : m_InterpolatedWaypoints) {
|
||||
auto& waypoint = waypoints.PushDebug("Waypoint " + std::to_string(++i));
|
||||
waypoint.PushDebug<AMFDoubleValue>("X") = point.x;
|
||||
waypoint.PushDebug<AMFDoubleValue>("Y") = point.y;
|
||||
waypoint.PushDebug<AMFDoubleValue>("Z") = point.z;
|
||||
waypoints.PushDebug("Waypoint " + std::to_string(++i)).PushDebug(point);
|
||||
}
|
||||
|
||||
i = 0;
|
||||
@@ -556,10 +550,7 @@ bool MovementAIComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInf
|
||||
auto pathCopy = m_CurrentPath; // Copy to avoid modifying the original stack
|
||||
while (!pathCopy.empty()) {
|
||||
const auto& waypoint = pathCopy.top();
|
||||
auto& pathWaypoint = currentPath.PushDebug("Waypoint " + std::to_string(++i));
|
||||
pathWaypoint.PushDebug<AMFDoubleValue>("X") = waypoint.position.x;
|
||||
pathWaypoint.PushDebug<AMFDoubleValue>("Y") = waypoint.position.y;
|
||||
pathWaypoint.PushDebug<AMFDoubleValue>("Z") = waypoint.position.z;
|
||||
currentPath.PushDebug("Waypoint " + std::to_string(++i)).PushDebug(waypoint.position);
|
||||
|
||||
pathCopy.pop();
|
||||
}
|
||||
|
||||
@@ -238,10 +238,7 @@ bool PhantomPhysicsComponent::OnGetObjectReportInfo(GameMessages::GetObjectRepor
|
||||
info.PushDebug<AMFIntValue>("Effect Type") = static_cast<int>(m_EffectType);
|
||||
info.PushDebug<AMFDoubleValue>("Directional Multiplier") = m_DirectionalMultiplier;
|
||||
info.PushDebug<AMFBoolValue>("Is Directional") = m_IsDirectional;
|
||||
auto& direction = info.PushDebug("Direction");
|
||||
direction.PushDebug<AMFDoubleValue>("x") = m_Direction.x;
|
||||
direction.PushDebug<AMFDoubleValue>("y") = m_Direction.y;
|
||||
direction.PushDebug<AMFDoubleValue>("z") = m_Direction.z;
|
||||
auto& direction = info.PushDebug("Direction").PushDebug(m_Direction);
|
||||
|
||||
if (m_MinMax) {
|
||||
auto& minMaxInfo = info.PushDebug("Min Max Info");
|
||||
@@ -252,15 +249,8 @@ bool PhantomPhysicsComponent::OnGetObjectReportInfo(GameMessages::GetObjectRepor
|
||||
if (m_IsRespawnVolume) {
|
||||
auto& respawnInfo = info.PushDebug("Respawn Info");
|
||||
respawnInfo.PushDebug<AMFBoolValue>("Is Respawn Volume") = m_IsRespawnVolume;
|
||||
auto& respawnPos = respawnInfo.PushDebug("Respawn Position");
|
||||
respawnPos.PushDebug<AMFDoubleValue>("x") = m_RespawnPos.x;
|
||||
respawnPos.PushDebug<AMFDoubleValue>("y") = m_RespawnPos.y;
|
||||
respawnPos.PushDebug<AMFDoubleValue>("z") = m_RespawnPos.z;
|
||||
auto& respawnRot = respawnInfo.PushDebug("Respawn Rotation");
|
||||
respawnRot.PushDebug<AMFDoubleValue>("w") = m_RespawnRot.w;
|
||||
respawnRot.PushDebug<AMFDoubleValue>("x") = m_RespawnRot.x;
|
||||
respawnRot.PushDebug<AMFDoubleValue>("y") = m_RespawnRot.y;
|
||||
respawnRot.PushDebug<AMFDoubleValue>("z") = m_RespawnRot.z;
|
||||
respawnInfo.PushDebug("Respawn Position").PushDebug(m_RespawnPos);
|
||||
respawnInfo.PushDebug("Respawn Rotation").PushDebug(m_RespawnRot);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -249,18 +249,11 @@ bool PhysicsComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo&
|
||||
auto& info = reportInfo.info->PushDebug("Physics");
|
||||
reportInfo.subCategory = &info;
|
||||
|
||||
auto& pos = info.PushDebug("Position");
|
||||
pos.PushDebug<AMFDoubleValue>("x") = m_Position.x;
|
||||
pos.PushDebug<AMFDoubleValue>("y") = m_Position.y;
|
||||
pos.PushDebug<AMFDoubleValue>("z") = m_Position.z;
|
||||
auto& pos = info.PushDebug("Position").PushDebug(m_Position);
|
||||
|
||||
auto& rot = info.PushDebug("Rotation");
|
||||
rot.PushDebug<AMFDoubleValue>("w") = m_Rotation.w;
|
||||
rot.PushDebug<AMFDoubleValue>("x") = m_Rotation.x;
|
||||
rot.PushDebug<AMFDoubleValue>("y") = m_Rotation.y;
|
||||
rot.PushDebug<AMFDoubleValue>("z") = m_Rotation.z;
|
||||
auto& rot = info.PushDebug("Rotation").PushDebug(m_Rotation);
|
||||
|
||||
info.PushDebug<AMFIntValue>("CollisionGroup") = m_CollisionGroup;
|
||||
info.PushDebug<AMFIntValue>("Collision Group") = m_CollisionGroup;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -80,8 +80,9 @@ bool ProximityMonitorComponent::OnGetObjectReportInfo(GameMessages::GetObjectRep
|
||||
proxAmf.PushDebug("Position").PushDebug(entity->GetPosition());
|
||||
proxAmf.PushDebug("Rotation").PushDebug(entity->GetRotation());
|
||||
auto& collidingAmf = proxAmf.PushDebug("Colliding Objects");
|
||||
int i = 1;
|
||||
for (const auto& colliding : entity->GetCurrentlyCollidingObjects()) {
|
||||
collidingAmf.PushDebug(std::to_string(colliding));
|
||||
collidingAmf.PushDebug<AMFStringValue>(std::to_string(i++), "LWOOBJID") = std::to_string(colliding);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -576,25 +576,25 @@ bool QuickBuildComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInf
|
||||
auto& quickbuild = reportInfo.info->PushDebug("Quick Build");
|
||||
quickbuild.PushDebug<AMFStringValue>("State") = StringifiedEnum::ToString(m_State).data();
|
||||
quickbuild.PushDebug<AMFDoubleValue>("Timer") = m_Timer;
|
||||
quickbuild.PushDebug<AMFDoubleValue>("TimerIncomplete") = m_TimerIncomplete;
|
||||
quickbuild.PushDebug("ActivatorPosition").PushDebug(m_ActivatorPosition);
|
||||
quickbuild.PushDebug<AMFStringValue>("ActivatorId") = std::to_string(m_ActivatorId);
|
||||
quickbuild.PushDebug<AMFBoolValue>("ShowResetEffect") = m_ShowResetEffect;
|
||||
quickbuild.PushDebug<AMFDoubleValue>("Timer Incomplete") = m_TimerIncomplete;
|
||||
quickbuild.PushDebug("Activator Position").PushDebug(m_ActivatorPosition);
|
||||
quickbuild.PushDebug<AMFStringValue>("Activator ID", "LWOOBJID") = std::to_string(m_ActivatorId);
|
||||
quickbuild.PushDebug<AMFBoolValue>("Show Reset Effect") = m_ShowResetEffect;
|
||||
quickbuild.PushDebug<AMFDoubleValue>("Taken") = m_Taken;
|
||||
quickbuild.PushDebug<AMFDoubleValue>("ResetTime") = m_ResetTime;
|
||||
quickbuild.PushDebug<AMFDoubleValue>("CompleteTime") = m_CompleteTime;
|
||||
quickbuild.PushDebug<AMFIntValue>("TakeImagination") = m_TakeImagination;
|
||||
quickbuild.PushDebug<AMFDoubleValue>("Reset Time") = m_ResetTime;
|
||||
quickbuild.PushDebug<AMFDoubleValue>("Complete Time") = m_CompleteTime;
|
||||
quickbuild.PushDebug<AMFIntValue>("Take Imagination") = m_TakeImagination;
|
||||
quickbuild.PushDebug<AMFBoolValue>("Interruptible") = m_Interruptible;
|
||||
quickbuild.PushDebug<AMFBoolValue>("SelfActivator") = m_SelfActivator;
|
||||
auto& modules = quickbuild.PushDebug("CustomModules");
|
||||
quickbuild.PushDebug<AMFBoolValue>("Self Activator") = m_SelfActivator;
|
||||
auto& modules = quickbuild.PushDebug("Custom Modules");
|
||||
for (const auto cmodule : m_CustomModules) modules.PushDebug<AMFIntValue>("Module") = cmodule;
|
||||
quickbuild.PushDebug<AMFIntValue>("ActivityId") = m_ActivityId;
|
||||
quickbuild.PushDebug<AMFIntValue>("PostImaginationCost") = m_PostImaginationCost;
|
||||
quickbuild.PushDebug<AMFDoubleValue>("TimeBeforeSmash") = m_TimeBeforeSmash;
|
||||
quickbuild.PushDebug<AMFDoubleValue>("TimeBeforeDrain") = m_TimeBeforeDrain;
|
||||
quickbuild.PushDebug<AMFIntValue>("DrainedImagination") = m_DrainedImagination;
|
||||
quickbuild.PushDebug<AMFBoolValue>("RepositionPlayer") = m_RepositionPlayer;
|
||||
quickbuild.PushDebug<AMFDoubleValue>("SoftTimer") = m_SoftTimer;
|
||||
quickbuild.PushDebug<AMFStringValue>("Builder") = std::to_string(m_Builder);
|
||||
quickbuild.PushDebug<AMFIntValue>("Activity Id") = m_ActivityId;
|
||||
quickbuild.PushDebug<AMFIntValue>("Post Imagination Cost") = m_PostImaginationCost;
|
||||
quickbuild.PushDebug<AMFDoubleValue>("Time Before Smash") = m_TimeBeforeSmash;
|
||||
quickbuild.PushDebug<AMFDoubleValue>("Time Before Drain") = m_TimeBeforeDrain;
|
||||
quickbuild.PushDebug<AMFIntValue>("Drained Imagination") = m_DrainedImagination;
|
||||
quickbuild.PushDebug<AMFBoolValue>("Reposition Player") = m_RepositionPlayer;
|
||||
quickbuild.PushDebug<AMFDoubleValue>("Soft Timer") = m_SoftTimer;
|
||||
quickbuild.PushDebug<AMFStringValue>("Builder", "LWOOBJID") = std::to_string(m_Builder);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ ScriptComponent::ScriptComponent(Entity* parent, const int32_t componentID, cons
|
||||
m_ScriptName = scriptName;
|
||||
|
||||
SetScript(scriptName);
|
||||
RegisterMsg(&ScriptComponent::OnGetObjectReportInfo);
|
||||
Component::RegisterMsg(&ScriptComponent::OnGetObjectReportInfo);
|
||||
}
|
||||
|
||||
void ScriptComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) {
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
|
||||
#include "CppScripts.h"
|
||||
#include "Component.h"
|
||||
#include "GameMessages.h"
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include "eReplicaComponentType.h"
|
||||
|
||||
@@ -42,9 +45,22 @@ public:
|
||||
* @param scriptName the name of the script to find
|
||||
*/
|
||||
void SetScript(const std::string& scriptName);
|
||||
|
||||
|
||||
bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo);
|
||||
|
||||
// Registers a message from a script to be listened for on the parent object
|
||||
template<typename ScriptClass, typename DerivedMsgType>
|
||||
void RegisterMsg(ScriptClass* scriptThis, bool (ScriptClass::*scriptHandler)(Entity&, DerivedMsgType&)) {
|
||||
static_assert(std::is_base_of_v<GameMessages::GameMsg, DerivedMsgType>, "DerivedMsgType must derive from GameMessages::GameMsg base class.");
|
||||
const auto boundMsg = std::bind(scriptHandler, scriptThis, std::placeholders::_1, std::placeholders::_2);
|
||||
auto* const parent = m_Parent;
|
||||
const auto castWrapper = [parent, boundMsg](GameMessages::GameMsg& msg) {
|
||||
return boundMsg(*parent, static_cast<DerivedMsgType&>(msg));
|
||||
};
|
||||
DerivedMsgType msg;
|
||||
m_Parent->RegisterMsg(msg.msgId, castWrapper);
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
/**
|
||||
|
||||
@@ -79,14 +79,8 @@ void SimplePhysicsComponent::SetPhysicsMotionState(uint32_t value) {
|
||||
bool SimplePhysicsComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) {
|
||||
PhysicsComponent::OnGetObjectReportInfo(reportInfo);
|
||||
auto& info = reportInfo.subCategory->PushDebug("Simple Physics Info");
|
||||
auto& velocity = info.PushDebug("Velocity");
|
||||
velocity.PushDebug<AMFDoubleValue>("x") = m_Velocity.x;
|
||||
velocity.PushDebug<AMFDoubleValue>("y") = m_Velocity.y;
|
||||
velocity.PushDebug<AMFDoubleValue>("z") = m_Velocity.z;
|
||||
auto& angularVelocity = info.PushDebug("Angular Velocity");
|
||||
angularVelocity.PushDebug<AMFDoubleValue>("x") = m_AngularVelocity.x;
|
||||
angularVelocity.PushDebug<AMFDoubleValue>("y") = m_AngularVelocity.y;
|
||||
angularVelocity.PushDebug<AMFDoubleValue>("z") = m_AngularVelocity.z;
|
||||
info.PushDebug("Velocity").PushDebug(m_Velocity);
|
||||
info.PushDebug("Angular Velocity").PushDebug(m_AngularVelocity);
|
||||
info.PushDebug<AMFIntValue>("Physics Motion State") = m_PhysicsMotionState;
|
||||
info.PushDebug<AMFStringValue>("Climbable Type") = StringifiedEnum::ToString(m_ClimbableType).data();
|
||||
return true;
|
||||
|
||||
@@ -159,6 +159,10 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System
|
||||
|
||||
InventoryComponent* inv = entity->GetComponent<InventoryComponent>();
|
||||
if (inv) {
|
||||
// Clear server-side skill state so AddItemSkills sends fresh AddSkill
|
||||
// packets to the now-ready client. Skills sent during entity construction
|
||||
// (Serialize) arrive before LWOSkillComponent is initialized and are dropped.
|
||||
inv->ClearSkills();
|
||||
auto items = inv->GetEquippedItems();
|
||||
for (auto pair : items) {
|
||||
const auto item = pair.second;
|
||||
|
||||
@@ -45,6 +45,7 @@ enum class BehaviorSlot : int32_t;
|
||||
enum class eVendorTransactionResult : uint32_t;
|
||||
enum class eReponseMoveItemBetweenInventoryTypeCode : int32_t;
|
||||
enum class eMissionState : int;
|
||||
enum class AiState : uint32_t;
|
||||
|
||||
enum class eCameraTargetCyclingMode : int32_t {
|
||||
ALLOW_CYCLE_TEAMMATES,
|
||||
@@ -980,5 +981,12 @@ namespace GameMessages {
|
||||
LWOOBJID objectID{};
|
||||
LOT lot{};
|
||||
};
|
||||
|
||||
struct NotifyCombatAIStateChange : public GameMsg {
|
||||
NotifyCombatAIStateChange() : GameMsg(MessageType::Game::NOTIFY_COMBAT_AI_STATE_CHANGE) {}
|
||||
|
||||
AiState newState{};
|
||||
AiState prevState{};
|
||||
};
|
||||
};
|
||||
#endif // GAMEMESSAGES_H
|
||||
|
||||
@@ -68,6 +68,7 @@ void HandlePacket(Packet* packet);
|
||||
std::map<uint32_t, std::string> activeSessions;
|
||||
SystemAddress authServerMasterPeerSysAddr;
|
||||
SystemAddress chatServerMasterPeerSysAddr;
|
||||
SystemAddress dashboardServerMasterPeerSysAddr;
|
||||
|
||||
int GenerateBCryptPassword(const std::string& password, const int workFactor, char salt[BCRYPT_HASHSIZE], char hash[BCRYPT_HASHSIZE]) {
|
||||
int32_t bcryptState = ::bcrypt_gensalt(workFactor, salt);
|
||||
@@ -375,6 +376,11 @@ int main(int argc, char** argv) {
|
||||
StartAuthServer();
|
||||
}
|
||||
|
||||
// Start web dashboard if enabled
|
||||
if (Game::config->GetValue("enable_dashboard") == "1") {
|
||||
StartDashboardServer();
|
||||
}
|
||||
|
||||
auto t = std::chrono::high_resolution_clock::now();
|
||||
Packet* packet = nullptr;
|
||||
constexpr uint32_t logFlushTime = 15 * masterFramerate;
|
||||
@@ -499,6 +505,11 @@ void HandlePacket(Packet* packet) {
|
||||
authServerMasterPeerSysAddr = UNASSIGNED_SYSTEM_ADDRESS;
|
||||
StartAuthServer();
|
||||
}
|
||||
|
||||
if (packet->systemAddress == dashboardServerMasterPeerSysAddr) {
|
||||
dashboardServerMasterPeerSysAddr = UNASSIGNED_SYSTEM_ADDRESS;
|
||||
StartDashboardServer();
|
||||
}
|
||||
}
|
||||
|
||||
if (packet->data[0] == ID_CONNECTION_LOST) {
|
||||
@@ -520,6 +531,11 @@ void HandlePacket(Packet* packet) {
|
||||
authServerMasterPeerSysAddr = UNASSIGNED_SYSTEM_ADDRESS;
|
||||
StartAuthServer();
|
||||
}
|
||||
|
||||
if (packet->systemAddress == dashboardServerMasterPeerSysAddr) {
|
||||
dashboardServerMasterPeerSysAddr = UNASSIGNED_SYSTEM_ADDRESS;
|
||||
StartDashboardServer();
|
||||
}
|
||||
}
|
||||
|
||||
if (packet->length < 4) return;
|
||||
@@ -601,6 +617,9 @@ void HandlePacket(Packet* packet) {
|
||||
case ServiceType::AUTH:
|
||||
authServerMasterPeerSysAddr = packet->systemAddress;
|
||||
break;
|
||||
case ServiceType::DASHBOARD:
|
||||
dashboardServerMasterPeerSysAddr = packet->systemAddress;
|
||||
break;
|
||||
default:
|
||||
// We just ignore any other server type
|
||||
break;
|
||||
@@ -866,11 +885,11 @@ int ShutdownSequence(int32_t signal) {
|
||||
instance->SetIsShuttingDown(true);
|
||||
}
|
||||
|
||||
LOG("Attempting to shutdown instances, max 60 seconds...");
|
||||
LOG("Attempting to shutdown instances, max 10 seconds...");
|
||||
|
||||
auto t = std::chrono::high_resolution_clock::now();
|
||||
uint32_t framesSinceShutdownStart = 0;
|
||||
constexpr uint32_t maxShutdownTime = 60 * mediumFramerate;
|
||||
constexpr uint32_t maxShutdownTime = 10 * mediumFramerate;
|
||||
bool allInstancesShutdown = false;
|
||||
Packet* packet = nullptr;
|
||||
while (true) {
|
||||
@@ -893,7 +912,10 @@ int ShutdownSequence(int32_t signal) {
|
||||
}
|
||||
}
|
||||
|
||||
if (allInstancesShutdown && authServerMasterPeerSysAddr == UNASSIGNED_SYSTEM_ADDRESS && chatServerMasterPeerSysAddr == UNASSIGNED_SYSTEM_ADDRESS) {
|
||||
if (allInstancesShutdown && \
|
||||
authServerMasterPeerSysAddr == UNASSIGNED_SYSTEM_ADDRESS && \
|
||||
chatServerMasterPeerSysAddr == UNASSIGNED_SYSTEM_ADDRESS && \
|
||||
dashboardServerMasterPeerSysAddr == UNASSIGNED_SYSTEM_ADDRESS) {
|
||||
LOG("Finished shutting down MasterServer!");
|
||||
break;
|
||||
}
|
||||
@@ -905,6 +927,26 @@ int ShutdownSequence(int32_t signal) {
|
||||
|
||||
if (framesSinceShutdownStart == maxShutdownTime) {
|
||||
LOG("Finished shutting down by timeout!");
|
||||
// log what we were waiting on: worlds, chat, auth, dashboard, etc
|
||||
if (authServerMasterPeerSysAddr != UNASSIGNED_SYSTEM_ADDRESS) {
|
||||
LOG("Auth server did not shutdown in time");
|
||||
}
|
||||
if (chatServerMasterPeerSysAddr != UNASSIGNED_SYSTEM_ADDRESS) {
|
||||
LOG("Chat server did not shutdown in time");
|
||||
}
|
||||
if (dashboardServerMasterPeerSysAddr != UNASSIGNED_SYSTEM_ADDRESS) {
|
||||
LOG("Dashboard server did not shutdown in time");
|
||||
}
|
||||
for (const auto& instance : Game::im->GetInstances()) {
|
||||
if (instance == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!instance->GetShutdownComplete()) {
|
||||
LOG("Instance zone %i clone %i instance %i port %i did not shutdown in time", instance->GetMapID(), instance->GetCloneID(), instance->GetInstanceID(), instance->GetPort());
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,6 +107,42 @@ uint32_t StartAuthServer() {
|
||||
return auth_pid;
|
||||
}
|
||||
|
||||
uint32_t StartDashboardServer() {
|
||||
if (Game::ShouldShutdown()) {
|
||||
LOG("Currently shutting down. DashboardServer will not be restarted.");
|
||||
return 0;
|
||||
}
|
||||
auto web_path = BinaryPathFinder::GetBinaryDir() / "DashboardServer";
|
||||
#ifdef _WIN32
|
||||
web_path.replace_extension(".exe");
|
||||
auto web_startup = startup;
|
||||
auto web_info = PROCESS_INFORMATION{};
|
||||
if (!CreateProcessW(web_path.wstring().data(), web_path.wstring().data(),
|
||||
nullptr, nullptr, false, 0, nullptr, nullptr,
|
||||
&web_startup, &web_info))
|
||||
{
|
||||
LOG("Failed to launch DashboardServer");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// get pid and close unused handles
|
||||
auto web_pid = web_info.dwProcessId;
|
||||
CloseHandle(web_info.hProcess);
|
||||
CloseHandle(web_info.hThread);
|
||||
#else // *nix systems
|
||||
const auto web_pid = fork();
|
||||
if (web_pid < 0) {
|
||||
LOG("Failed to launch DashboardServer");
|
||||
return 0;
|
||||
} else if (web_pid == 0) {
|
||||
// We are the child process
|
||||
execl(web_path.string().c_str(), web_path.string().c_str(), nullptr);
|
||||
}
|
||||
#endif
|
||||
LOG("DashboardServer PID is %d", web_pid);
|
||||
return web_pid;
|
||||
}
|
||||
|
||||
uint32_t StartWorldServer(LWOMAPID mapID, uint16_t port, LWOINSTANCEID lastInstanceID, int maxPlayers, LWOCLONEID cloneID) {
|
||||
auto world_path = BinaryPathFinder::GetBinaryDir() / "WorldServer";
|
||||
#ifdef _WIN32
|
||||
|
||||
@@ -3,4 +3,5 @@
|
||||
|
||||
uint32_t StartAuthServer();
|
||||
uint32_t StartChatServer();
|
||||
uint32_t StartDashboardServer();
|
||||
uint32_t StartWorldServer(LWOMAPID mapID, uint16_t port, LWOINSTANCEID lastInstanceID, int maxPlayers, LWOCLONEID cloneID);
|
||||
|
||||
@@ -94,6 +94,7 @@ void MasterPackets::HandleServerInfo(Packet* packet) {
|
||||
}
|
||||
|
||||
void MasterPackets::SendServerInfo(dServer* server, Packet* packet) {
|
||||
LOG("SendServerInfo called for server type %i", static_cast<int>(server->GetServerType()));
|
||||
RakNet::BitStream bitStream;
|
||||
BitStreamUtils::WriteHeader(bitStream, ServiceType::MASTER, MessageType::Master::SERVER_INFO);
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@ Packet* dServer::ReceiveFromMaster() {
|
||||
break;
|
||||
}
|
||||
case ID_CONNECTION_REQUEST_ACCEPTED: {
|
||||
LOG("Established connection to master, zone (%i), instance (%i)", this->GetZoneID(), this->GetInstanceID());
|
||||
LOG("Established connection to master: ServiceType (%s), Zone (%i), Instance (%i)", StringifiedEnum::ToString(this->GetServerType()).data(), this->GetZoneID(), this->GetInstanceID());
|
||||
mMasterConnectionActive = true;
|
||||
mMasterSystemAddress = packet->systemAddress;
|
||||
MasterPackets::SendServerInfo(this, packet);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
set(DSCRIPTS_SOURCES_02_SERVER_ENEMY_FV
|
||||
"DragonRonin.cpp"
|
||||
"FvMaelstromCavalry.cpp"
|
||||
"FvMaelstromDragon.cpp"
|
||||
PARENT_SCOPE)
|
||||
|
||||
6
dScripts/02_server/Enemy/FV/DragonRonin.cpp
Normal file
6
dScripts/02_server/Enemy/FV/DragonRonin.cpp
Normal file
@@ -0,0 +1,6 @@
|
||||
#include "DragonRonin.h"
|
||||
|
||||
void DragonRonin::OnStartup(Entity* self) {
|
||||
self->SetVar<float>(u"suicideTimer", 40.0f);
|
||||
CountdownDestroyAI::OnStartup(self);
|
||||
}
|
||||
7
dScripts/02_server/Enemy/FV/DragonRonin.h
Normal file
7
dScripts/02_server/Enemy/FV/DragonRonin.h
Normal file
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
#include "CountdownDestroyAI.h"
|
||||
|
||||
class DragonRonin : public CountdownDestroyAI {
|
||||
public:
|
||||
void OnStartup(Entity* self) override;
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
set(DSCRIPTS_SOURCES_02_SERVER_ENEMY_GENERAL
|
||||
"BaseEnemyMech.cpp"
|
||||
"BaseEnemyApe.cpp"
|
||||
"CountdownDestroyAI.cpp"
|
||||
"GfApeSmashingQB.cpp"
|
||||
"TreasureChestDragonServer.cpp"
|
||||
"EnemyNjBuff.cpp"
|
||||
|
||||
50
dScripts/02_server/Enemy/General/CountdownDestroyAI.cpp
Normal file
50
dScripts/02_server/Enemy/General/CountdownDestroyAI.cpp
Normal file
@@ -0,0 +1,50 @@
|
||||
#include "CountdownDestroyAI.h"
|
||||
|
||||
#include "BaseCombatAIComponent.h"
|
||||
#include "ScriptComponent.h"
|
||||
|
||||
void CountdownDestroyAI::OnStartup(Entity* self) {
|
||||
CountdownStartup(*self);
|
||||
auto* scriptComp = self->GetComponent<ScriptComponent>();
|
||||
if (scriptComp) scriptComp->RegisterMsg(this, &CountdownDestroyAI::OnNotifyCombatAIStateChange);
|
||||
}
|
||||
|
||||
void CountdownDestroyAI::CountdownStartup(Entity& self) {
|
||||
auto suicideTimer = self.GetVar<float>(u"suicideTimer");
|
||||
if (suicideTimer == 0.0f) suicideTimer = 60;
|
||||
self.AddTimer("Dead", suicideTimer);
|
||||
}
|
||||
|
||||
void CountdownDestroyAI::OnHit(Entity* self, Entity* attacker) {
|
||||
if (!self->GetVar<bool>(u"ShouldBeDead")) return;
|
||||
self->CancelTimer("IsBeingAttacked");
|
||||
self->AddTimer("Dead", 5.0f);
|
||||
}
|
||||
|
||||
void CountdownDestroyAI::OnTimerDone(Entity* self, std::string timerName) {
|
||||
if (timerName == "Dead") {
|
||||
self->SetVar<bool>(u"ShouldBeDead", true);
|
||||
if (self->GetVar<bool>(u"Busy")) {
|
||||
self->AddTimer("IsBeingAttacked", 5.0f);
|
||||
} else {
|
||||
self->Smash();
|
||||
}
|
||||
} else if (timerName == "IsBeingAttacked") {
|
||||
self->Smash();
|
||||
}
|
||||
}
|
||||
|
||||
bool CountdownDestroyAI::OnNotifyCombatAIStateChange(Entity& self, GameMessages::NotifyCombatAIStateChange& notifyMsg) {
|
||||
const auto curState = notifyMsg.newState;
|
||||
if (curState == AiState::dead) return true;
|
||||
|
||||
if (curState == AiState::aggro || curState == AiState::tether) {
|
||||
self.SetVar(u"Busy", true);
|
||||
} else {
|
||||
self.SetVar(u"Busy", false);
|
||||
if (self.GetVar<bool>(u"ShouldBeDead")) {
|
||||
self.Smash();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user