Add dashboard audit log and configuration management

- Implemented dashboard audit logging with InsertAuditLog, GetRecentAuditLogs, GetAuditLogsByIP, and CleanupOldAuditLogs methods.
- Created dashboard configuration management with GetDashboardConfig and SetDashboardConfig methods.
- Added new tables for dashboard_audit_log and dashboard_config in both MySQL and SQLite migrations.
- Updated CMakeLists to include Crow and ASIO for dashboard server functionality.
- Enhanced existing database classes to support new dashboard features, including character, play key, and property management.
- Added new methods for retrieving and managing play keys, properties, and pet names.
- Updated TestSQLDatabase to include stubs for new dashboard-related methods.
- Modified shared and dashboard configuration files for new settings.
This commit is contained in:
Aaron Kimbrell
2026-04-22 11:01:41 -05:00
parent d532a9b063
commit e3467465b4
92 changed files with 9133 additions and 77 deletions

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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