mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-03-04 23:59:50 +00:00
WIP working state
This commit is contained in:
@@ -1,17 +1,10 @@
|
||||
set(DDASHBOARDSERVER_SOURCES
|
||||
"DashboardServer.cpp"
|
||||
"MasterPacketHandler.cpp"
|
||||
"routes/APIRoutes.cpp"
|
||||
"routes/StaticRoutes.cpp"
|
||||
"routes/DashboardRoutes.cpp"
|
||||
"routes/WSRoutes.cpp"
|
||||
"routes/AuthRoutes.cpp"
|
||||
"auth/JWTUtils.cpp"
|
||||
"auth/DashboardAuthService.cpp"
|
||||
"auth/AuthMiddleware.cpp"
|
||||
"auth/RequireAuthMiddleware.cpp"
|
||||
)
|
||||
|
||||
add_subdirectory(routes)
|
||||
add_subdirectory(auth)
|
||||
|
||||
add_executable(DashboardServer ${DDASHBOARDSERVER_SOURCES})
|
||||
|
||||
target_include_directories(DashboardServer PRIVATE
|
||||
@@ -29,11 +22,12 @@ target_include_directories(DashboardServer PRIVATE
|
||||
"${PROJECT_SOURCE_DIR}/dServer"
|
||||
"${PROJECT_SOURCE_DIR}/thirdparty"
|
||||
"${PROJECT_SOURCE_DIR}/thirdparty/nlohmann"
|
||||
"${PROJECT_SOURCE_DIR}/dDashboardServer"
|
||||
"${PROJECT_SOURCE_DIR}/dDashboardServer/auth"
|
||||
"${PROJECT_SOURCE_DIR}/dDashboardServer/routes"
|
||||
)
|
||||
|
||||
target_link_libraries(DashboardServer ${COMMON_LIBRARIES} dWeb dServer bcrypt OpenSSL::Crypto)
|
||||
target_link_libraries(DashboardServer ${COMMON_LIBRARIES} dWeb dServer bcrypt OpenSSL::Crypto DashboardRoutes DashboardAuth)
|
||||
|
||||
|
||||
# Copy static files and templates to build directory (always copy)
|
||||
|
||||
@@ -20,14 +20,13 @@
|
||||
#include "Diagnostics.h"
|
||||
#include "Web.h"
|
||||
#include "Server.h"
|
||||
#include "MasterPacketHandler.h"
|
||||
|
||||
#include "routes/ServerState.h"
|
||||
#include "routes/APIRoutes.h"
|
||||
#include "routes/StaticRoutes.h"
|
||||
#include "routes/DashboardRoutes.h"
|
||||
#include "routes/WSRoutes.h"
|
||||
#include "routes/AuthRoutes.h"
|
||||
#include "ServerState.h"
|
||||
#include "APIRoutes.h"
|
||||
#include "StaticRoutes.h"
|
||||
#include "DashboardRoutes.h"
|
||||
#include "WSRoutes.h"
|
||||
#include "AuthRoutes.h"
|
||||
#include "AuthMiddleware.h"
|
||||
|
||||
namespace Game {
|
||||
@@ -157,12 +156,22 @@ int main(int argc, char** argv) {
|
||||
const auto elapsedSinceBroadcast = std::chrono::duration_cast<std::chrono::milliseconds>(currentTime - lastBroadcast).count();
|
||||
|
||||
if (elapsed >= 1000.0f / 60.0f) {
|
||||
// Handle master server packets
|
||||
Packet* packet = g_Server->ReceiveFromMaster();
|
||||
if (packet) {
|
||||
MasterPacketHandler::HandleMasterPacket(packet);
|
||||
g_Server->DeallocateMasterPacket(packet);
|
||||
}
|
||||
// // Handle master server packets
|
||||
// Packet* packet = g_Server->ReceiveFromMaster();
|
||||
// if (packet) {
|
||||
// RakNet::BitStream bitStream(packet->data, packet->length, false);
|
||||
// PacketHandler::HandlePacket(bitStream, packet->systemAddress);
|
||||
// g_Server->DeallocateMasterPacket(packet);
|
||||
// }
|
||||
|
||||
// // Handle RakNet protocol packets from connected servers
|
||||
// packet = g_Server->Receive();
|
||||
// while (packet) {
|
||||
// RakNet::BitStream bitStream(packet->data, packet->length, false);
|
||||
// PacketHandler::HandlePacket(bitStream, packet->systemAddress);
|
||||
// g_Server->DeallocatePacket(packet);
|
||||
// packet = g_Server->Receive();
|
||||
// }
|
||||
|
||||
// Handle web requests
|
||||
Game::web.ReceiveRequests();
|
||||
@@ -190,3 +199,5 @@ int main(int argc, char** argv) {
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,209 +0,0 @@
|
||||
#include "MasterPacketHandler.h"
|
||||
|
||||
#include "BitStreamUtils.h"
|
||||
#include "dServer.h"
|
||||
#include "Game.h"
|
||||
#include "Logger.h"
|
||||
#include "RakNetTypes.h"
|
||||
#include "routes/ServerState.h"
|
||||
#include <chrono>
|
||||
#include <mutex>
|
||||
|
||||
namespace MasterPacketHandler {
|
||||
namespace {
|
||||
std::map<MessageType::Master, std::function<std::unique_ptr<MasterPacket>()>> g_Handlers = {
|
||||
{MessageType::Master::SERVER_INFO, []() {
|
||||
return std::make_unique<ServerInfo>();
|
||||
}},
|
||||
{MessageType::Master::PLAYER_ADDED, []() {
|
||||
return std::make_unique<PlayerAdded>();
|
||||
}},
|
||||
{MessageType::Master::PLAYER_REMOVED, []() {
|
||||
return std::make_unique<PlayerRemoved>();
|
||||
}},
|
||||
{MessageType::Master::SHUTDOWN_RESPONSE, []() {
|
||||
return std::make_unique<ShutdownResponse>();
|
||||
}},
|
||||
{MessageType::Master::SHUTDOWN, []() {
|
||||
return std::make_unique<Shutdown>();
|
||||
}},
|
||||
};
|
||||
}
|
||||
|
||||
bool ServerInfo::Deserialize(RakNet::BitStream& bitStream) {
|
||||
VALIDATE_READ(bitStream.Read(port));
|
||||
VALIDATE_READ(bitStream.Read(zoneID));
|
||||
VALIDATE_READ(bitStream.Read(instanceID));
|
||||
VALIDATE_READ(bitStream.Read(serverType));
|
||||
LUString ipStr{};
|
||||
VALIDATE_READ(bitStream.Read(ipStr));
|
||||
ip = ipStr.string;
|
||||
return true;
|
||||
}
|
||||
|
||||
void ServerInfo::Handle() {
|
||||
std::lock_guard<std::mutex> lock(ServerState::g_StatusMutex);
|
||||
|
||||
LOG("MasterPacketHandler: Processing SERVER_INFO for service type %i, zone %u, instance %u, port %u", serverType, zoneID, instanceID, port);
|
||||
|
||||
switch (serverType) {
|
||||
case ServiceType::AUTH:
|
||||
ServerState::g_AuthStatus.online = true;
|
||||
ServerState::g_AuthStatus.lastSeen = std::chrono::steady_clock::now();
|
||||
LOG("Updated Auth server status: online");
|
||||
break;
|
||||
case ServiceType::CHAT:
|
||||
ServerState::g_ChatStatus.online = true;
|
||||
ServerState::g_ChatStatus.lastSeen = std::chrono::steady_clock::now();
|
||||
LOG("Updated Chat server status: online");
|
||||
break;
|
||||
case ServiceType::WORLD: {
|
||||
// Update or add world instance
|
||||
bool found = false;
|
||||
for (auto& world : ServerState::g_WorldInstances) {
|
||||
if (world.mapID == zoneID && world.instanceID == instanceID) {
|
||||
world.ip = ip;
|
||||
world.port = port;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
WorldInstanceInfo info{};
|
||||
info.mapID = zoneID;
|
||||
info.instanceID = instanceID;
|
||||
info.cloneID = 0;
|
||||
info.players = 0;
|
||||
info.ip = ip;
|
||||
info.port = port;
|
||||
info.isPrivate = false;
|
||||
ServerState::g_WorldInstances.push_back(info);
|
||||
LOG("Added world instance: map %u instance %u", zoneID, instanceID);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool PlayerAdded::Deserialize(RakNet::BitStream& bitStream) {
|
||||
VALIDATE_READ(bitStream.Read(zoneID));
|
||||
VALIDATE_READ(bitStream.Read(instanceID));
|
||||
return true;
|
||||
}
|
||||
|
||||
void PlayerAdded::Handle() {
|
||||
std::lock_guard<std::mutex> lock(ServerState::g_StatusMutex);
|
||||
for (auto& world : ServerState::g_WorldInstances) {
|
||||
if (world.mapID == zoneID && world.instanceID == instanceID) {
|
||||
world.players++;
|
||||
LOG_DEBUG("Player added to map %u instance %u, now %u players", zoneID, instanceID, world.players);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool PlayerRemoved::Deserialize(RakNet::BitStream& bitStream) {
|
||||
VALIDATE_READ(bitStream.Read(zoneID));
|
||||
VALIDATE_READ(bitStream.Read(instanceID));
|
||||
return true;
|
||||
}
|
||||
|
||||
void PlayerRemoved::Handle() {
|
||||
std::lock_guard<std::mutex> lock(ServerState::g_StatusMutex);
|
||||
for (auto& world : ServerState::g_WorldInstances) {
|
||||
if (world.mapID == zoneID && world.instanceID == instanceID) {
|
||||
if (world.players > 0) world.players--;
|
||||
LOG_DEBUG("Player removed from map %u instance %u, now %u players", zoneID, instanceID, world.players);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool ShutdownResponse::Deserialize(RakNet::BitStream& bitStream) {
|
||||
VALIDATE_READ(bitStream.Read(zoneID));
|
||||
VALIDATE_READ(bitStream.Read(instanceID));
|
||||
VALIDATE_READ(bitStream.Read(serverType));
|
||||
return true;
|
||||
}
|
||||
|
||||
void ShutdownResponse::Handle() {
|
||||
std::lock_guard<std::mutex> lock(ServerState::g_StatusMutex);
|
||||
|
||||
switch (serverType) {
|
||||
case ServiceType::AUTH:
|
||||
ServerState::g_AuthStatus.online = false;
|
||||
LOG_DEBUG("Auth server shutdown");
|
||||
break;
|
||||
case ServiceType::CHAT:
|
||||
ServerState::g_ChatStatus.online = false;
|
||||
LOG_DEBUG("Chat server shutdown");
|
||||
break;
|
||||
case ServiceType::WORLD:
|
||||
for (auto it = ServerState::g_WorldInstances.begin(); it != ServerState::g_WorldInstances.end(); ++it) {
|
||||
if (it->mapID == zoneID && it->instanceID == instanceID) {
|
||||
ServerState::g_WorldInstances.erase(it);
|
||||
LOG_DEBUG("Removed shutdown instance: map %u instance %u", zoneID, instanceID);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool Shutdown::Deserialize(RakNet::BitStream& bitStream) {
|
||||
// SHUTDOWN message has no additional data
|
||||
return true;
|
||||
}
|
||||
|
||||
void Shutdown::Handle() {
|
||||
LOG("Received SHUTDOWN command from Master");
|
||||
Game::lastSignal = -1; // Trigger shutdown
|
||||
}
|
||||
|
||||
void HandleMasterPacket(Packet* packet) {
|
||||
if (!packet) return;
|
||||
|
||||
switch (packet->data[0]) {
|
||||
case ID_DISCONNECTION_NOTIFICATION:
|
||||
case ID_CONNECTION_LOST:
|
||||
LOG("Lost connection to Master Server");
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(ServerState::g_StatusMutex);
|
||||
ServerState::g_AuthStatus.online = false;
|
||||
ServerState::g_ChatStatus.online = false;
|
||||
ServerState::g_WorldInstances.clear();
|
||||
}
|
||||
break;
|
||||
case ID_CONNECTION_REQUEST_ACCEPTED:
|
||||
LOG("Connected to Master Server");
|
||||
break;
|
||||
case ID_USER_PACKET_ENUM: {
|
||||
RakNet::BitStream inStream(packet->data, packet->length, false);
|
||||
uint64_t header{};
|
||||
inStream.Read(header);
|
||||
|
||||
const auto packetType = static_cast<MessageType::Master>(header);
|
||||
LOG_DEBUG("Received Master packet type: %i", packetType);
|
||||
|
||||
auto it = g_Handlers.find(packetType);
|
||||
if (it != g_Handlers.end()) {
|
||||
auto handler = it->second();
|
||||
if (!handler->Deserialize(inStream)) {
|
||||
LOG_DEBUG("Error deserializing Master packet type %i", packetType);
|
||||
return;
|
||||
}
|
||||
handler->Handle();
|
||||
} else {
|
||||
LOG_DEBUG("Unhandled Master packet type: %i", packetType);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
|
||||
#include "dCommonVars.h"
|
||||
#include "MessageType/Master.h"
|
||||
#include "BitStream.h"
|
||||
|
||||
struct Packet;
|
||||
|
||||
namespace MasterPacketHandler {
|
||||
// Base class for all master packet handlers
|
||||
class MasterPacket {
|
||||
public:
|
||||
virtual ~MasterPacket() = default;
|
||||
virtual bool Deserialize(RakNet::BitStream& bitStream) = 0;
|
||||
virtual void Handle() = 0;
|
||||
};
|
||||
|
||||
// SERVER_INFO packet handler
|
||||
class ServerInfo : public MasterPacket {
|
||||
public:
|
||||
bool Deserialize(RakNet::BitStream& bitStream) override;
|
||||
void Handle() override;
|
||||
|
||||
private:
|
||||
uint32_t port{0};
|
||||
uint32_t zoneID{0};
|
||||
uint32_t instanceID{0};
|
||||
ServiceType serverType{};
|
||||
std::string ip{};
|
||||
};
|
||||
|
||||
// PLAYER_ADDED packet handler
|
||||
class PlayerAdded : public MasterPacket {
|
||||
public:
|
||||
bool Deserialize(RakNet::BitStream& bitStream) override;
|
||||
void Handle() override;
|
||||
|
||||
private:
|
||||
LWOMAPID zoneID{};
|
||||
LWOINSTANCEID instanceID{};
|
||||
};
|
||||
|
||||
// PLAYER_REMOVED packet handler
|
||||
class PlayerRemoved : public MasterPacket {
|
||||
public:
|
||||
bool Deserialize(RakNet::BitStream& bitStream) override;
|
||||
void Handle() override;
|
||||
|
||||
private:
|
||||
LWOMAPID zoneID{};
|
||||
LWOINSTANCEID instanceID{};
|
||||
};
|
||||
|
||||
// SHUTDOWN_RESPONSE packet handler
|
||||
class ShutdownResponse : public MasterPacket {
|
||||
public:
|
||||
bool Deserialize(RakNet::BitStream& bitStream) override;
|
||||
void Handle() override;
|
||||
|
||||
private:
|
||||
uint32_t zoneID{};
|
||||
uint32_t instanceID{};
|
||||
ServiceType serverType{};
|
||||
};
|
||||
|
||||
// SHUTDOWN packet handler
|
||||
class Shutdown : public MasterPacket {
|
||||
public:
|
||||
bool Deserialize(RakNet::BitStream& bitStream) override;
|
||||
void Handle() override;
|
||||
};
|
||||
|
||||
// Main handler function
|
||||
void HandleMasterPacket(Packet* packet);
|
||||
}
|
||||
28
dDashboardServer/auth/CMakeLists.txt
Normal file
28
dDashboardServer/auth/CMakeLists.txt
Normal file
@@ -0,0 +1,28 @@
|
||||
set(DASHBOARDAUTH_SOURCES
|
||||
"JWTUtils.cpp"
|
||||
"DashboardAuthService.cpp"
|
||||
"AuthMiddleware.cpp"
|
||||
"RequireAuthMiddleware.cpp"
|
||||
)
|
||||
|
||||
add_library(DashboardAuth STATIC ${DASHBOARDAUTH_SOURCES})
|
||||
|
||||
target_include_directories(DashboardAuth PRIVATE
|
||||
"${PROJECT_SOURCE_DIR}/dCommon"
|
||||
"${PROJECT_SOURCE_DIR}/dCommon/dClient"
|
||||
"${PROJECT_SOURCE_DIR}/dCommon/dEnums"
|
||||
"${PROJECT_SOURCE_DIR}/dDatabase"
|
||||
"${PROJECT_SOURCE_DIR}/dDatabase/CDClientDatabase"
|
||||
"${PROJECT_SOURCE_DIR}/dDatabase/CDClientDatabase/CDClientTables"
|
||||
"${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase"
|
||||
"${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase/ITables"
|
||||
"${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase/MySQL"
|
||||
"${PROJECT_SOURCE_DIR}/dNet"
|
||||
"${PROJECT_SOURCE_DIR}/dWeb"
|
||||
"${PROJECT_SOURCE_DIR}/dServer"
|
||||
"${PROJECT_SOURCE_DIR}/thirdparty"
|
||||
"${PROJECT_SOURCE_DIR}/thirdparty/nlohmann"
|
||||
"${PROJECT_SOURCE_DIR}/dDashboardServer/auth"
|
||||
)
|
||||
|
||||
target_link_libraries(DashboardAuth PRIVATE ${COMMON_LIBRARIES} dWeb dServer bcrypt OpenSSL::Crypto)
|
||||
@@ -17,30 +17,7 @@ void RegisterAPIRoutes() {
|
||||
.method = eHTTPMethod::GET,
|
||||
.middleware = { std::make_shared<RequireAuthMiddleware>(0) },
|
||||
.handle = [](HTTPReply& reply, const HTTPContext& context) {
|
||||
std::lock_guard<std::mutex> lock(ServerState::g_StatusMutex);
|
||||
|
||||
nlohmann::json response = {
|
||||
{"auth", {
|
||||
{"online", ServerState::g_AuthStatus.online},
|
||||
{"players", ServerState::g_AuthStatus.players},
|
||||
{"version", ServerState::g_AuthStatus.version}
|
||||
}},
|
||||
{"chat", {
|
||||
{"online", ServerState::g_ChatStatus.online},
|
||||
{"players", ServerState::g_ChatStatus.players}
|
||||
}},
|
||||
{"worlds", nlohmann::json::array()}
|
||||
};
|
||||
|
||||
for (const auto& world : ServerState::g_WorldInstances) {
|
||||
response["worlds"].push_back({
|
||||
{"mapID", world.mapID},
|
||||
{"instanceID", world.instanceID},
|
||||
{"cloneID", world.cloneID},
|
||||
{"players", world.players},
|
||||
{"isPrivate", world.isPrivate}
|
||||
});
|
||||
}
|
||||
nlohmann::json response = ServerState::GetServerStateJson();
|
||||
|
||||
reply.status = eHTTPStatusCode::OK;
|
||||
reply.message = response.dump();
|
||||
@@ -92,10 +69,375 @@ void RegisterAPIRoutes() {
|
||||
.method = eHTTPMethod::GET,
|
||||
.middleware = { std::make_shared<RequireAuthMiddleware>(0) },
|
||||
.handle = [](HTTPReply& reply, const HTTPContext& context) {
|
||||
nlohmann::json response = {{"count", 0}, {"note", "Not yet implemented"}};
|
||||
reply.status = eHTTPStatusCode::OK;
|
||||
reply.message = response.dump();
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
try {
|
||||
const uint32_t count = Database::Get()->GetCharacterCount();
|
||||
nlohmann::json response = {{"count", count}};
|
||||
reply.status = eHTTPStatusCode::OK;
|
||||
reply.message = response.dump();
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
} catch (std::exception& ex) {
|
||||
LOG("Error in /api/characters/count: %s", ex.what());
|
||||
reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR;
|
||||
reply.message = "{\"error\":\"Database error\"}";
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/tables/accounts - Get accounts table data (DataTables.js format)
|
||||
Game::web.RegisterHTTPRoute({
|
||||
.path = "/api/tables/accounts",
|
||||
.method = eHTTPMethod::POST,
|
||||
.middleware = { std::make_shared<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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
@@ -25,35 +25,16 @@ void RegisterDashboardRoutes() {
|
||||
env.set_lstrip_blocks(true);
|
||||
|
||||
// Prepare data for template
|
||||
nlohmann::json data;
|
||||
// Get username from auth context
|
||||
data["username"] = context.authenticatedUser;
|
||||
data["gmLevel"] = context.gmLevel;
|
||||
nlohmann::json data = context.GetUserDataJson();
|
||||
|
||||
// Server status (placeholder data - will be updated with real data from master)
|
||||
data["auth"]["online"] = ServerState::g_AuthStatus.online;
|
||||
data["auth"]["players"] = ServerState::g_AuthStatus.players;
|
||||
data["chat"]["online"] = ServerState::g_ChatStatus.online;
|
||||
data["chat"]["players"] = ServerState::g_ChatStatus.players;
|
||||
|
||||
// World instances
|
||||
std::lock_guard<std::mutex> lock(ServerState::g_StatusMutex);
|
||||
data["worlds"] = nlohmann::json::array();
|
||||
for (const auto& world : ServerState::g_WorldInstances) {
|
||||
data["worlds"].push_back({
|
||||
{"mapID", world.mapID},
|
||||
{"instanceID", world.instanceID},
|
||||
{"cloneID", world.cloneID},
|
||||
{"players", world.players},
|
||||
{"isPrivate", world.isPrivate}
|
||||
});
|
||||
}
|
||||
// Server status - merge with server state
|
||||
nlohmann::json serverState = ServerState::GetServerStateJson();
|
||||
data.merge_patch(serverState);
|
||||
|
||||
// Statistics
|
||||
const uint32_t accountCount = Database::Get()->GetAccountCount();
|
||||
data["stats"]["onlinePlayers"] = 0; // TODO: Get from server communication
|
||||
data["stats"]["totalAccounts"] = accountCount;
|
||||
data["stats"]["totalCharacters"] = 0; // TODO: Add GetCharacterCount to database interface
|
||||
data["stats"]["totalAccounts"] = Database::Get()->GetAccountCount();
|
||||
data["stats"]["totalCharacters"] = Database::Get()->GetCharacterCount();
|
||||
|
||||
// Render template
|
||||
const std::string html = env.render_file("index.jinja2", data);
|
||||
@@ -82,9 +63,8 @@ void RegisterDashboardRoutes() {
|
||||
env.set_trim_blocks(true);
|
||||
env.set_lstrip_blocks(true);
|
||||
|
||||
// Render template with empty username
|
||||
nlohmann::json data;
|
||||
data["username"] = "";
|
||||
// Render template with empty user data (not authenticated)
|
||||
nlohmann::json data = context.GetUserDataJson();
|
||||
const std::string html = env.render_file("login.jinja2", data);
|
||||
|
||||
reply.status = eHTTPStatusCode::OK;
|
||||
@@ -98,4 +78,214 @@ void RegisterDashboardRoutes() {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// GET /accounts/:id - View single account
|
||||
Game::web.RegisterHTTPRoute({
|
||||
.path = "/accounts/:id",
|
||||
.method = eHTTPMethod::GET,
|
||||
.middleware = { std::make_shared<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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "json.hpp"
|
||||
|
||||
class HTTPContext;
|
||||
|
||||
void RegisterDashboardRoutes();
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <cstdint>
|
||||
#include "json.hpp"
|
||||
|
||||
struct ServerStatus {
|
||||
bool online{false};
|
||||
@@ -27,5 +27,26 @@ namespace ServerState {
|
||||
extern ServerStatus g_AuthStatus;
|
||||
extern ServerStatus g_ChatStatus;
|
||||
extern std::vector<WorldInstanceInfo> g_WorldInstances;
|
||||
extern std::mutex g_StatusMutex;
|
||||
|
||||
// Helper function to get all server state as JSON
|
||||
inline nlohmann::json GetServerStateJson() {
|
||||
nlohmann::json data;
|
||||
data["auth"]["online"] = g_AuthStatus.online;
|
||||
data["auth"]["players"] = g_AuthStatus.players;
|
||||
data["chat"]["online"] = g_ChatStatus.online;
|
||||
data["chat"]["players"] = g_ChatStatus.players;
|
||||
|
||||
data["worlds"] = nlohmann::json::array();
|
||||
for (const auto& world : g_WorldInstances) {
|
||||
data["worlds"].push_back({
|
||||
{"mapID", world.mapID},
|
||||
{"instanceID", world.instanceID},
|
||||
{"cloneID", world.cloneID},
|
||||
{"players", world.players},
|
||||
{"isPrivate", world.isPrivate}
|
||||
});
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,8 +65,4 @@ void RegisterStaticRoutes() {
|
||||
// Serve JavaScript files
|
||||
ServeStaticFile("/js/dashboard.js", "dDashboardServer/static/js/dashboard.js");
|
||||
ServeStaticFile("/js/login.js", "dDashboardServer/static/js/login.js");
|
||||
|
||||
// Also serve from /static/ paths for backwards compatibility
|
||||
ServeStaticFile("/static/css/dashboard.css", "dDashboardServer/static/css/dashboard.css");
|
||||
ServeStaticFile("/static/js/dashboard.js", "dDashboardServer/static/js/dashboard.js");
|
||||
}
|
||||
|
||||
@@ -18,37 +18,14 @@ void RegisterWSRoutes() {
|
||||
}
|
||||
|
||||
void BroadcastDashboardUpdate() {
|
||||
std::lock_guard<std::mutex> lock(ServerState::g_StatusMutex);
|
||||
|
||||
nlohmann::json data = {
|
||||
{"auth", {
|
||||
{"online", ServerState::g_AuthStatus.online},
|
||||
{"players", ServerState::g_AuthStatus.players},
|
||||
{"version", ServerState::g_AuthStatus.version}
|
||||
}},
|
||||
{"chat", {
|
||||
{"online", ServerState::g_ChatStatus.online},
|
||||
{"players", ServerState::g_ChatStatus.players}
|
||||
}},
|
||||
{"worlds", nlohmann::json::array()}
|
||||
};
|
||||
|
||||
for (const auto& world : ServerState::g_WorldInstances) {
|
||||
data["worlds"].push_back({
|
||||
{"mapID", world.mapID},
|
||||
{"instanceID", world.instanceID},
|
||||
{"cloneID", world.cloneID},
|
||||
{"players", world.players},
|
||||
{"isPrivate", world.isPrivate}
|
||||
});
|
||||
}
|
||||
// Get server state data (auth, chat, worlds) - mutex is acquired internally
|
||||
nlohmann::json data = ServerState::GetServerStateJson();
|
||||
|
||||
// Add statistics
|
||||
try {
|
||||
const uint32_t accountCount = Database::Get()->GetAccountCount();
|
||||
data["stats"]["onlinePlayers"] = 0; // TODO: Get from server communication
|
||||
data["stats"]["totalAccounts"] = accountCount;
|
||||
data["stats"]["totalCharacters"] = 0; // TODO: Add GetCharacterCount to database interface
|
||||
data["stats"]["totalAccounts"] = Database::Get()->GetAccountCount();
|
||||
data["stats"]["totalCharacters"] = Database::Get()->GetCharacterCount();
|
||||
} catch (const std::exception& ex) {
|
||||
LOG_DEBUG("Error getting stats: %s", ex.what());
|
||||
}
|
||||
|
||||
@@ -2,11 +2,15 @@
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background-color: #f8f9fa;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body.d-flex.bg-dark.text-white {
|
||||
background-color: #0d0d0d;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Sidebar adjustments */
|
||||
.navbar.flex-column {
|
||||
box-shadow: 0.125rem 0 0.25rem rgba(0, 0, 0, 0.075);
|
||||
@@ -91,21 +95,6 @@ main {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
padding: 25px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
font-size: 1.5em;
|
||||
border-bottom: 2px solid #667eea;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.stat {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -175,3 +164,332 @@ main {
|
||||
padding: 20px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* Dark theme for data tables and containers */
|
||||
|
||||
/* Container margin for sidebar layout */
|
||||
.account-view-container,
|
||||
.accounts-container,
|
||||
.characters-container,
|
||||
.play-keys-container,
|
||||
.properties-container,
|
||||
.bug-reports-container {
|
||||
margin-left: 280px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* Table card styling */
|
||||
.table-card {
|
||||
background-color: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.table-header {
|
||||
background-color: #222;
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid #333;
|
||||
border-radius: 0.5rem 0.5rem 0 0;
|
||||
}
|
||||
|
||||
.table-header h2 {
|
||||
margin: 0;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.table-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
/* Bootstrap card overrides for dark theme */
|
||||
.card {
|
||||
background-color: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background-color: #222;
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid #333;
|
||||
border-radius: 0.5rem 0.5rem 0 0;
|
||||
}
|
||||
|
||||
.card-header h2 {
|
||||
margin: 0;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
/* Table styling */
|
||||
.table-dark {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.table-dark thead {
|
||||
background-color: #2a2a2a;
|
||||
}
|
||||
|
||||
.table-dark thead th {
|
||||
border-bottom: 2px solid #444;
|
||||
color: #aaa;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.875rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.table-dark tbody td {
|
||||
padding: 0.875rem 1rem;
|
||||
border-bottom: 1px solid #333;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.table-dark tbody tr:hover {
|
||||
background-color: #252525;
|
||||
}
|
||||
|
||||
/* DataTables customization */
|
||||
.dataTables_wrapper {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.dataTables_wrapper .dataTables_filter input {
|
||||
background-color: #2a2a2a;
|
||||
color: #fff;
|
||||
border: 1px solid #444;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.4rem 0.6rem;
|
||||
}
|
||||
|
||||
.dataTables_wrapper .dataTables_filter input::placeholder {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.dataTables_wrapper .dataTables_info {
|
||||
color: #aaa;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.dataTables_wrapper .dataTables_paginate .paginate_button {
|
||||
background: #2a2a2a;
|
||||
color: #fff;
|
||||
border: 1px solid #444;
|
||||
margin: 0 2px;
|
||||
padding: 0.4rem 0.8rem;
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.dataTables_wrapper .dataTables_paginate .paginate_button:hover {
|
||||
background: #3a3a3a;
|
||||
border: 1px solid #555;
|
||||
}
|
||||
|
||||
.dataTables_wrapper .dataTables_paginate .paginate_button.current {
|
||||
background: #0d6efd;
|
||||
border: 1px solid #0d6efd;
|
||||
}
|
||||
|
||||
.dataTables_wrapper .dataTables_paginate .paginate_button.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.dataTables_wrapper .dataTables_length select {
|
||||
background-color: #2a2a2a;
|
||||
color: #fff;
|
||||
border: 1px solid #444;
|
||||
padding: 0.4rem 0.6rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
/* Detail grid layout */
|
||||
.detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
background-color: #0a0a0a;
|
||||
padding: 1rem;
|
||||
border-radius: 0.25rem;
|
||||
border-left: 3px solid #0d6efd;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
color: #999;
|
||||
font-size: 0.875rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Badge styling */
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 0.35rem 0.6rem;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.badge-active {
|
||||
background-color: #28a745;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.badge-inactive {
|
||||
background-color: #6c757d;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.badge-banned {
|
||||
background-color: #dc3545;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.badge-locked {
|
||||
background-color: #ffc107;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.badge-gm {
|
||||
background-color: #17a2b8;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.badge-approved {
|
||||
background-color: #28a745;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.badge-pending {
|
||||
background-color: #ffc107;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/* Button styling */
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.25rem;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #0d6efd;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #0b5ed7;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: #6c757d;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: #5c636a;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: #dc3545;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background-color: #c82333;
|
||||
}
|
||||
|
||||
.btn-info {
|
||||
background-color: #17a2b8;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-info:hover {
|
||||
background-color: #138496;
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background-color: #ffc107;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.btn-warning:hover {
|
||||
background-color: #e0a800;
|
||||
}
|
||||
|
||||
/* Action buttons */
|
||||
.account-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Utility classes */
|
||||
.text-muted {
|
||||
color: #999 !important;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
color: #0d6efd;
|
||||
text-decoration: none;
|
||||
margin-bottom: 1rem;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.back-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.report-preview {
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.search-section {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
background-color: #2a2a2a;
|
||||
border: 1px solid #444;
|
||||
color: #fff;
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: #888;
|
||||
}
|
||||
137
dDashboardServer/templates/account-view.jinja2
Normal file
137
dDashboardServer/templates/account-view.jinja2
Normal file
@@ -0,0 +1,137 @@
|
||||
{% 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 }} - {{ account.name }}</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">{{ account.name }}</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="badge badge-gm">GM {{ account.gm_level }}</span>
|
||||
{% else %}
|
||||
<span class="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="badge badge-banned">BANNED</span>
|
||||
{% else %}
|
||||
<span class="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="badge badge-locked">LOCKED</span>
|
||||
{% else %}
|
||||
<span class="badge badge-active">Unlocked</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">Mute Expires</div>
|
||||
<div class="detail-value">
|
||||
{% if account.mute_expire > 0 %}
|
||||
<span>{{ account.mute_expire }}</span>
|
||||
{% else %}
|
||||
<span class="text-muted">Not muted</span>
|
||||
{% endif %}
|
||||
</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>
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
function UnbanAccount() {
|
||||
if (confirm("Are you sure you want to unban this account?")) {
|
||||
alert("Unban functionality coming soon");
|
||||
// TODO: Call unban API endpoint
|
||||
}
|
||||
}
|
||||
|
||||
function LockAccount() {
|
||||
if (confirm("Are you sure you want to lock this account?")) {
|
||||
alert("Lock functionality coming soon");
|
||||
// TODO: Call lock API endpoint
|
||||
}
|
||||
}
|
||||
|
||||
function UnlockAccount() {
|
||||
if (confirm("Are you sure you want to unlock this account?")) {
|
||||
alert("Unlock functionality coming soon");
|
||||
// TODO: Call unlock API endpoint
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
133
dDashboardServer/templates/accounts.jinja2
Normal file
133
dDashboardServer/templates/accounts.jinja2
Normal file
@@ -0,0 +1,133 @@
|
||||
{% extends "base.jinja2" %}
|
||||
|
||||
{% block title %}Accounts - DarkflameServer{% endblock %}
|
||||
|
||||
{% block css %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="accounts-container">
|
||||
<div class="container-fluid">
|
||||
<div class="table-card">
|
||||
<div class="table-header">
|
||||
<h2 class="mb-0">Accounts</h2>
|
||||
<p class="text-muted">View and manage user accounts</p>
|
||||
</div>
|
||||
<div class="table-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>
|
||||
$(document).ready(function() {
|
||||
// Initialize DataTable with server-side processing
|
||||
$('#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
|
||||
});
|
||||
});
|
||||
|
||||
function viewAccount(id) {
|
||||
window.location.href = '/accounts/' + id;
|
||||
}
|
||||
|
||||
function editAccount(id) {
|
||||
alert('Edit account: ' + id);
|
||||
// TODO: Implement account edit modal
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -4,8 +4,8 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}DarkflameServer{% endblock %}</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.datatables.net/2.3.6/css/dataTables.dataTables.min.css">
|
||||
<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>
|
||||
@@ -26,9 +26,8 @@
|
||||
{% endblock %}
|
||||
</footer>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.datatables.net/2.3.6/js/jquery.dataTables.min.js"></script>
|
||||
<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>
|
||||
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
|
||||
97
dDashboardServer/templates/bug_reports.jinja2
Normal file
97
dDashboardServer/templates/bug_reports.jinja2
Normal file
@@ -0,0 +1,97 @@
|
||||
{% extends "base.jinja2" %}
|
||||
|
||||
{% block title %}Bug Reports - DarkflameServer{% endblock %}
|
||||
|
||||
{% block css %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="bug-reports-container">
|
||||
<div class="table-card">
|
||||
<div class="table-header">
|
||||
<h2 class="mb-0">Bug Reports</h2>
|
||||
<p class="text-muted">View and manage bug reports from players</p>
|
||||
</div>
|
||||
<div class="table-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>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Initialize DataTable with server-side processing
|
||||
$('#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
|
||||
});
|
||||
});
|
||||
|
||||
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 %}
|
||||
90
dDashboardServer/templates/characters.jinja2
Normal file
90
dDashboardServer/templates/characters.jinja2
Normal file
@@ -0,0 +1,90 @@
|
||||
{% extends "base.jinja2" %}
|
||||
|
||||
{% block title %}Characters - DarkflameServer{% endblock %}
|
||||
|
||||
{% block css %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="characters-container">
|
||||
<div class="table-card">
|
||||
<div class="table-header">
|
||||
<h2 class="mb-0">Characters</h2>
|
||||
<p class="text-muted">View and manage player characters</p>
|
||||
</div>
|
||||
<div class="table-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>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Initialize DataTable with server-side processing
|
||||
$('#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
|
||||
});
|
||||
});
|
||||
|
||||
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 %}
|
||||
@@ -1,27 +1,27 @@
|
||||
{# Navigation #}
|
||||
<nav class="navbar navbar-dark bg-dark flex-column" style="width: 280px; height: 100vh; position: fixed; left: 0; top: 0; overflow-y: auto;">
|
||||
<div class="p-3">
|
||||
<a class="navbar-brand fw-bold" href="#">🎮 DarkflameServer</a>
|
||||
<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 active" href="#">Home</a>
|
||||
<a class="nav-link" href="/">Home</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#accounts">Accounts</a>
|
||||
<a class="nav-link" href="/accounts">Accounts</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#characters">Characters</a>
|
||||
<a class="nav-link" href="/characters">Characters</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#properties">Properties</a>
|
||||
<a class="nav-link" href="/play_keys">Play Keys</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#players">Players</a>
|
||||
<a class="nav-link" href="/properties">Properties</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#servers">Servers</a>
|
||||
<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>
|
||||
|
||||
95
dDashboardServer/templates/play_keys.jinja2
Normal file
95
dDashboardServer/templates/play_keys.jinja2
Normal file
@@ -0,0 +1,95 @@
|
||||
{% extends "base.jinja2" %}
|
||||
|
||||
{% block title %}Play Keys - DarkflameServer{% endblock %}
|
||||
|
||||
{% block css %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="play-keys-container">
|
||||
<div class="table-card">
|
||||
<div class="table-header">
|
||||
<h2 class="mb-0">Play Keys</h2>
|
||||
<p class="text-muted">View and manage play keys</p>
|
||||
</div>
|
||||
<div class="table-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>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Initialize DataTable with server-side processing
|
||||
$('#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
|
||||
});
|
||||
});
|
||||
|
||||
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 %}
|
||||
92
dDashboardServer/templates/properties.jinja2
Normal file
92
dDashboardServer/templates/properties.jinja2
Normal file
@@ -0,0 +1,92 @@
|
||||
{% extends "base.jinja2" %}
|
||||
|
||||
{% block title %}Properties - DarkflameServer{% endblock %}
|
||||
|
||||
{% block css %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="properties-container">
|
||||
<div class="table-card">
|
||||
<div class="table-header">
|
||||
<h2 class="mb-0">Properties</h2>
|
||||
<p class="text-muted">View and manage player properties</p>
|
||||
</div>
|
||||
<div class="table-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>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Initialize DataTable with server-side processing
|
||||
$('#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
|
||||
});
|
||||
});
|
||||
|
||||
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 %}
|
||||
Reference in New Issue
Block a user