From 24293ea5c018245c1c08320c12a7653b04555ee5 Mon Sep 17 00:00:00 2001 From: ann7415 Date: Sun, 18 Jan 2026 22:28:57 +0100 Subject: [PATCH 1/6] feat: add cpp-httplib to vcpkg.json for enhanced HTTP support --- vcpkg.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vcpkg.json b/vcpkg.json index 6f59f55c7..8531748f7 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -7,6 +7,7 @@ "sqlite3", "openssl", "nlohmann-json", - "lz4" + "lz4", + "cpp-httplib" ] } From c36c3f6143236dcc10a49ff766e15bb5d1b54cd7 Mon Sep 17 00:00:00 2001 From: ann7415 Date: Sun, 18 Jan 2026 22:30:18 +0100 Subject: [PATCH 2/6] feat: add initial CSS styles for improved admin web interface layout --- server/src/admin/webServer/style.css | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 server/src/admin/webServer/style.css diff --git a/server/src/admin/webServer/style.css b/server/src/admin/webServer/style.css new file mode 100644 index 000000000..826638355 --- /dev/null +++ b/server/src/admin/webServer/style.css @@ -0,0 +1,6 @@ +body { font-family: sans-serif; margin: 20px; } +table { border-collapse: collapse; width: 100%; margin-bottom: 20px; } +th, td { border: 1px solid #ccc; padding: 6px; } +th { background: #eee; text-align: left; } +button { margin-right: 6px; } +pre { background: #111; color: #0f0; padding: 10px; } \ No newline at end of file From b06c2d850b1866607fe6888784b4b1a592c01161 Mon Sep 17 00:00:00 2001 From: ann7415 Date: Sun, 18 Jan 2026 22:30:38 +0100 Subject: [PATCH 3/6] feat: add initial HTML structure for R-Type Admin Dashboard --- server/src/admin/webServer/index.html | 31 +++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 server/src/admin/webServer/index.html diff --git a/server/src/admin/webServer/index.html b/server/src/admin/webServer/index.html new file mode 100644 index 000000000..8b0442be0 --- /dev/null +++ b/server/src/admin/webServer/index.html @@ -0,0 +1,31 @@ + + + + + R-Type Admin + + + +

R-Type Admin Dashboard

+ +
+ + + +
+ +

Status

+

+
+

Sessions

+
+ +

Rooms

+
+ +

Bans

+
+ + + + From 919d65e6106678667f7cd15e15a2862f77702ab9 Mon Sep 17 00:00:00 2001 From: ann7415 Date: Sun, 18 Jan 2026 22:30:44 +0100 Subject: [PATCH 4/6] feat: implement API interaction and table rendering for admin web interface --- server/src/admin/webServer/app.js | 73 +++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 server/src/admin/webServer/app.js diff --git a/server/src/admin/webServer/app.js b/server/src/admin/webServer/app.js new file mode 100644 index 000000000..b13049cd5 --- /dev/null +++ b/server/src/admin/webServer/app.js @@ -0,0 +1,73 @@ +function authHeaders() { + const t = document.getElementById("token").value.trim(); + return { "Authorization": "Bearer " + t }; +} + +async function apiGet(path) { + const r = await fetch(path, { headers: authHeaders() }); + if (!r.ok) throw new Error(await r.text()); + return r.json(); +} + +async function apiPost(path, body) { + const r = await fetch(path, { + method: "POST", + headers: { ...authHeaders(), "Content-Type": "application/json" }, + body: JSON.stringify(body || {}) + }); + if (!r.ok) throw new Error(await r.text()); + return r.json(); +} + +function renderTable(el, cols, rows) { + const t = document.getElementById(el); + t.innerHTML = ""; + const thead = document.createElement("tr"); + cols.forEach(c => { const th = document.createElement("th"); th.textContent = c; thead.appendChild(th); }); + t.appendChild(thead); + + rows.forEach(r => { + const tr = document.createElement("tr"); + cols.forEach(c => { + const td = document.createElement("td"); + td.appendChild(r[c] instanceof Node ? r[c] : document.createTextNode(String(r[c] ?? ""))); + tr.appendChild(td); + }); + t.appendChild(tr); + }); +} + +async function refreshAll() { + try { + const status = await apiGet("/api/status"); + document.getElementById("status").textContent = JSON.stringify(status, null, 2); + const rooms = await apiGet("/api/rooms"); + renderTable("rooms", ["roomId","name","players","maxPlayers"], rooms); + const sessions = await apiGet("/api/sessions"); + const rows = sessions.map(s => { + const kick = document.createElement("button"); + kick.textContent = "Kick"; + kick.onclick = async () => { await apiPost("/api/kick", { who: String(s.id) }); await refreshAll(); }; + + const ban = document.createElement("button"); + ban.textContent = "Ban 60m"; + ban.onclick = async () => { await apiPost("/api/ban", { who: String(s.id), minutes: 60 }); await refreshAll(); }; + + return { + id: s.id, + username: s.username, + authed: s.authed, + roomId: s.roomId, + kick: kick, + ban: ban + }; + }); + renderTable("sessions", ["id","username","authed","roomId","kick","ban"], rows); + + const bans = await apiGet("/api/bans"); + renderTable("bans", ["ip","remainingSeconds"], bans); + + } catch (e) { + alert("Error: " + e.message); + } +} From 01c69ceb59ae578a22524f227ed51135ae688f9a Mon Sep 17 00:00:00 2001 From: ann7415 Date: Sun, 18 Jan 2026 22:30:58 +0100 Subject: [PATCH 5/6] feat: integrate web-based admin dashboard into server runtime --- server/src/thread/ServerRuntime.cpp | 4 ++++ server/src/thread/ServerRuntime.hpp | 2 ++ 2 files changed, 6 insertions(+) diff --git a/server/src/thread/ServerRuntime.cpp b/server/src/thread/ServerRuntime.cpp index 0019a2e03..1ddf24534 100644 --- a/server/src/thread/ServerRuntime.cpp +++ b/server/src/thread/ServerRuntime.cpp @@ -66,6 +66,10 @@ void ServerRuntime::start() requestStop(); }); _adminConsole->start(); + _adminWeb = std::make_unique(_sessionManager, _roomManager, [this]() { + requestStop(); + }); + _adminWeb->start(); _receiverThread = std::thread(&ServerRuntime::runReceiver, this); _processorThread = std::thread(&ServerRuntime::runProcessor, this); _snapshotThread = std::thread(&ServerRuntime::runSnapshot, this); diff --git a/server/src/thread/ServerRuntime.hpp b/server/src/thread/ServerRuntime.hpp index 5e284e56a..753b84f96 100644 --- a/server/src/thread/ServerRuntime.hpp +++ b/server/src/thread/ServerRuntime.hpp @@ -11,6 +11,7 @@ #include #include #include "AdminConsole.hpp" +#include "AdminWebServer.hpp" #include "AuthService.hpp" #include "GameServer.hpp" #include "IServer.hpp" @@ -134,6 +135,7 @@ namespace Net::Thread std::shared_ptr _sessionManager; ///> Manages client sessions std::shared_ptr _roomManager; ///> Manages game rooms std::unique_ptr _adminConsole; ///> Text-mode admin dashboard + std::unique_ptr _adminWeb; ///> Web-based admin dashboard std::thread _receiverThread; ///> Thread for receiving packets std::thread _processorThread; ///> Thread for processing packets From 38497b342634da8888faa891a9414c0f1a2a9ba8 Mon Sep 17 00:00:00 2001 From: ann7415 Date: Sun, 18 Jan 2026 22:33:31 +0100 Subject: [PATCH 6/6] feat: implement AdminWebServer for managing sessions and rooms via a web interface --- server/src/admin/webServer/AdminWebServer.cpp | 611 ++++++++++++++++++ server/src/admin/webServer/AdminWebServer.hpp | 88 +++ 2 files changed, 699 insertions(+) create mode 100644 server/src/admin/webServer/AdminWebServer.cpp create mode 100644 server/src/admin/webServer/AdminWebServer.hpp diff --git a/server/src/admin/webServer/AdminWebServer.cpp b/server/src/admin/webServer/AdminWebServer.cpp new file mode 100644 index 000000000..29fd56c67 --- /dev/null +++ b/server/src/admin/webServer/AdminWebServer.cpp @@ -0,0 +1,611 @@ +/* +** EPITECH PROJECT, 2026 +** R-Type +** File description: +** AdminWebServer.cpp +*/ + +#include "AdminWebServer.hpp" + +namespace +{ + using json = nlohmann::json; + + static std::string ipToString(uint32_t ipNbo) + { + char buf[64] = {}; + in_addr a{}; + a.s_addr = ipNbo; +#ifdef _WIN32 + InetNtopA(AF_INET, &a, buf, static_cast(sizeof(buf))); +#else + inet_ntop(AF_INET, &a, buf, sizeof(buf)); +#endif + return std::string(buf); + } + + static std::optional parseSessionId(const std::string &s) + { + if (s.empty()) + return std::nullopt; + if (!std::all_of(s.begin(), s.end(), [](unsigned char c) { + return std::isdigit(c) != 0; + })) + return std::nullopt; + try { + return std::stoi(s); + } catch (...) { + return std::nullopt; + } + } + + static bool hasBearerToken(const httplib::Request &req, const std::string &token) + { + const auto it = req.headers.find("Authorization"); + if (it == req.headers.end()) + return false; + + const std::string &v = it->second; + static constexpr auto kPrefix = "Bearer "; + constexpr auto prefixLen = std::char_traits::length(kPrefix); + + if (v.size() <= prefixLen) + return false; + if (v.rfind(kPrefix, 0) != 0) + return false; + return v.substr(prefixLen) == token; + } + + static void setJson(httplib::Response &res, const json &j, int status = 200) + { + res.status = status; + res.set_content(j.dump(2), "application/json; charset=utf-8"); + } + + static void setText(httplib::Response &res, int status, const char *msg) + { + res.status = status; + res.set_content(msg, "text/plain; charset=utf-8"); + } + + static std::optional parseJsonBody(const httplib::Request &req, httplib::Response &res) + { + try { + return json::parse(req.body.empty() ? "{}" : req.body); + } catch (...) { + setText(res, 400, "Bad JSON"); + return std::nullopt; + } + } + + template + static bool requirePtr(const std::shared_ptr &ptr, httplib::Response &res) + { + if (ptr) + return true; + setText(res, 500, "Service unavailable"); + return false; + } + + static auto *kIndexHtml = R"HTML( + + + + + + R-Type Admin + + + +
+

R-Type Admin

+ +
+ + + +
+ +

Status

+
{}
+ +

Rooms

+
+ +

Sessions

+
+ +

Room moderation

+
+ + + + + + + +
+ +
+ +
+
+ + + + +)HTML"; + + static auto kStyleCss = R"CSS( +body { font-family: sans-serif; background:#0b0f14; color:#eaeef5; margin:0; } +.wrap { max-width: 1100px; margin: 24px auto; padding: 0 16px; } +h1 { margin: 8px 0 20px; } +h2 { margin-top: 24px; } +.row { display:flex; gap: 10px; align-items:center; margin: 10px 0 14px; flex-wrap: wrap; } +input { padding: 8px 10px; width: 260px; } +button { padding: 8px 12px; cursor:pointer; } +pre { background:#121a24; padding: 12px; border-radius: 8px; overflow:auto; } +table { width:100%; border-collapse: collapse; background:#121a24; border-radius: 8px; overflow:hidden; } +th, td { padding: 10px; border-bottom: 1px solid #233247; vertical-align: middle; } +th { text-align:left; color:#b9c4d6; } +tr:last-child td { border-bottom: none; } +td button { padding: 6px 10px; } +.small { width: 140px; } +)CSS"; + + static auto kAppJs = R"JS( +function authHeaders() { + const t = document.getElementById("token").value.trim(); + return { "Authorization": "Bearer " + t }; +} + +async function apiGet(path) { + const r = await fetch(path, { headers: authHeaders() }); + if (!r.ok) throw new Error(await r.text()); + return r.json(); +} + +async function apiPost(path, body) { + const r = await fetch(path, { + method: "POST", + headers: { ...authHeaders(), "Content-Type": "application/json" }, + body: JSON.stringify(body || {}) + }); + if (!r.ok) throw new Error(await r.text()); + return r.json(); +} + +function renderTable(el, cols, rows) { + const t = document.getElementById(el); + t.innerHTML = ""; + const thead = document.createElement("tr"); + cols.forEach(c => { const th = document.createElement("th"); th.textContent = c; thead.appendChild(th); }); + t.appendChild(thead); + + rows.forEach(r => { + const tr = document.createElement("tr"); + cols.forEach(c => { + const td = document.createElement("td"); + td.appendChild(r[c] instanceof Node ? r[c] : document.createTextNode(String(r[c] ?? ""))); + tr.appendChild(td); + }); + t.appendChild(tr); + }); +} + +function vRoomId() { + const s = document.getElementById("rm_roomId").value.trim(); + const n = Number(s); + if (!Number.isFinite(n) || n <= 0) return null; + return n; +} + +function vUsername() { + return document.getElementById("rm_username").value.trim(); +} + +async function banRoomManual() { + const roomId = vRoomId(); + const username = vUsername(); + if (!roomId || !username) return alert("roomId + username required"); + await apiPost("/api/banroom", { roomId, who: username }); + await refreshAll(); +} + +async function unbanRoomManual() { + const roomId = vRoomId(); + const username = vUsername(); + if (!roomId || !username) return alert("roomId + username required"); + await apiPost("/api/unbanroom", { roomId, who: username }); + await refreshAll(); +} + +async function kickRoomManual() { + const roomId = vRoomId(); + const username = vUsername(); + if (!roomId || !username) return alert("roomId + username required"); + await apiPost("/api/kickroom", { roomId, who: username }); + await refreshAll(); +} + +async function shutdownServer() { + await apiPost("/api/shutdown", {}); +} + +async function refreshAll() { + try { + const status = await apiGet("/api/status"); + document.getElementById("status").textContent = JSON.stringify(status, null, 2); + + const rooms = await apiGet("/api/rooms"); + renderTable("rooms", ["roomId","name","players","maxPlayers","playerNames"], rooms); + + const sessions = await apiGet("/api/sessions"); + const rows = sessions.map(s => { + const kick = document.createElement("button"); + kick.textContent = "Kick (server)"; + kick.onclick = async () => { await apiPost("/api/kick", { who: String(s.id) }); await refreshAll(); }; + + const kickRoom = document.createElement("button"); + kickRoom.textContent = "KickRoom"; + kickRoom.disabled = (s.roomId === null || s.roomId === undefined); + kickRoom.onclick = async () => { await apiPost("/api/kickroom", { roomId: s.roomId, who: String(s.id) }); await refreshAll(); }; + + const banRoom = document.createElement("button"); + banRoom.textContent = "BanRoom"; + banRoom.disabled = (s.roomId === null || s.roomId === undefined); + banRoom.onclick = async () => { await apiPost("/api/banroom", { roomId: s.roomId, who: String(s.id) }); await refreshAll(); }; + + return { + id: s.id, + username: s.username, + authed: s.authed, + ip: s.ip, + port: s.port, + roomId: s.roomId, + kick: kick, + kickRoom: kickRoom, + banRoom: banRoom + }; + }); + renderTable("sessions", ["id","username","authed","ip","port","roomId","kickRoom","banRoom","kick"], rows); + + } catch (e) { + alert("Error: " + e.message); + } +} + +window.addEventListener("load", () => { + const token = document.getElementById("token"); + if (token && !token.value) token.value = "admin"; + refreshAll(); +}); +)JS"; + + static std::optional resolveWhoToSessionId( + const std::shared_ptr &sessions, const std::string &who) + { + if (!sessions) + return std::nullopt; + if (const auto sid = parseSessionId(who); sid.has_value()) + return sid; + return sessions->findSessionIdByUsername(who); + } + + static std::optional resolveWhoToUsername( + const std::shared_ptr &sessions, const std::string &who) + { + if (!sessions) + return std::nullopt; + if (const auto sid = parseSessionId(who); sid.has_value()) { + const std::string u = sessions->getUsername(*sid); + if (u.empty()) + return std::nullopt; + return u; + } + if (who.empty()) + return std::nullopt; + return who; + } + + static void handleStatus(const std::shared_ptr &sessions, + const std::shared_ptr &rooms, httplib::Response &res) + { + json out; + out["ok"] = true; + out["sessions"] = sessions ? sessions->getAllSessions().size() : 0u; + out["rooms"] = rooms ? rooms->listRooms().size() : 0u; + setJson(res, out, 200); + } + + static void handleRooms(const std::shared_ptr &rooms, httplib::Response &res) + { + if (!rooms) { + setJson(res, json::array(), 200); + return; + } + + json arr = json::array(); + for (const auto &rd : rooms->listRooms()) { + json r; + r["roomId"] = rd.roomId; + r["name"] = rd.roomName; + if constexpr (requires { rd.currentPlayers; }) + r["players"] = rd.currentPlayers; + else + r["players"] = rd.playerNames.size(); + r["maxPlayers"] = rd.maxPlayers; + r["playerNames"] = rd.playerNames; + arr.push_back(std::move(r)); + } + setJson(res, arr, 200); + } + + static void handleSessions(const std::shared_ptr &sessions, + const std::shared_ptr &rooms, httplib::Response &res) + { + if (!sessions || !rooms) { + setJson(res, json::array(), 200); + return; + } + + json arr = json::array(); + for (const auto &[sid, addr] : sessions->getAllSessions()) { + json s; + s["id"] = sid; + s["username"] = sessions->getUsername(sid); + s["authed"] = sessions->isAuthed(sid); + + s["ip"] = ipToString(addr.sin_addr.s_addr); + s["port"] = ntohs(addr.sin_port); + + const auto roomId = rooms->getRoomIdOfPlayer(sid); + s["roomId"] = roomId == 0 ? json(nullptr) : json(roomId); + + arr.push_back(std::move(s)); + } + setJson(res, arr, 200); + } + + static void handleKick(const std::shared_ptr &sessions, + const std::shared_ptr &rooms, const httplib::Request &req, httplib::Response &res) + { + const auto inOpt = parseJsonBody(req, res); + if (!inOpt) + return; + const json &in = *inOpt; + + if (!requirePtr(sessions, res) || !requirePtr(rooms, res)) + return; + + const std::string who = in.value("who", ""); + const auto sidOpt = resolveWhoToSessionId(sessions, who); + if (!sidOpt.has_value()) { + setText(res, 404, "Unknown session/user"); + return; + } + + const int sid = *sidOpt; + (void) rooms->removePlayer(sid); + sessions->removeSession(sid); + + setJson(res, json{{"ok", true}}, 200); + } + + static void handleKickRoom(const std::shared_ptr &sessions, + const std::shared_ptr &rooms, const httplib::Request &req, httplib::Response &res) + { + const auto inOpt = parseJsonBody(req, res); + if (!inOpt) + return; + const json &in = *inOpt; + + if (!requirePtr(sessions, res) || !requirePtr(rooms, res)) + return; + + const uint32_t roomId = in.value("roomId", 0u); + const std::string who = in.value("who", ""); + if (roomId == 0 || who.empty()) { + setText(res, 400, "Expected roomId + who"); + return; + } + + const auto sidOpt = resolveWhoToSessionId(sessions, who); + if (!sidOpt.has_value()) { + setText(res, 404, "Unknown session/user"); + return; + } + + const int sid = *sidOpt; + if (rooms->getRoomIdOfPlayer(sid) != roomId) { + setText(res, 404, "Player not in that room"); + return; + } + + (void) rooms->removePlayer(sid); + setJson(res, json{{"ok", true}}, 200); + } + + static void handleBanRoom(const std::shared_ptr &sessions, + const std::shared_ptr &rooms, const httplib::Request &req, httplib::Response &res) + { + const auto inOpt = parseJsonBody(req, res); + if (!inOpt) + return; + const json &in = *inOpt; + if (!requirePtr(sessions, res) || !requirePtr(rooms, res)) + return; + const uint32_t roomId = in.value("roomId", 0u); + const std::string who = in.value("who", ""); + if (roomId == 0 || who.empty()) { + setText(res, 400, "Expected roomId + who"); + return; + } + const auto room = rooms->getRoomById(roomId); + if (!room) { + setText(res, 404, "Room not found"); + return; + } + const auto usernameOpt = resolveWhoToUsername(sessions, who); + if (!usernameOpt.has_value()) { + setText(res, 404, "Unknown username"); + return; + } + const std::string &username = *usernameOpt; + room->banUsername(username); + if (const auto sidOpt = sessions->findSessionIdByUsername(username); sidOpt.has_value()) { + const int sid = *sidOpt; + if (rooms->getRoomIdOfPlayer(sid) == roomId) + (void) rooms->removePlayer(sid); + } + + setJson(res, json{{"ok", true}}, 200); + } + + static void handleUnbanRoom(std::shared_ptr sessions, + std::shared_ptr rooms, const httplib::Request &req, httplib::Response &res) + { + const auto inOpt = parseJsonBody(req, res); + if (!inOpt) + return; + const json &in = *inOpt; + if (!requirePtr(sessions, res) || !requirePtr(rooms, res)) + return; + const uint32_t roomId = in.value("roomId", 0u); + const std::string who = in.value("who", ""); + if (roomId == 0 || who.empty()) { + setText(res, 400, "Expected roomId + who"); + return; + } + const auto room = rooms->getRoomById(roomId); + if (!room) { + setText(res, 404, "Room not found"); + return; + } + const auto usernameOpt = resolveWhoToUsername(sessions, who); + if (!usernameOpt.has_value()) { + setText(res, 404, "Unknown username"); + return; + } + room->unbanUsername(*usernameOpt); + setJson(res, json{{"ok", true}}, 200); + } + + static void handleShutdown(std::function onShutdown, httplib::Response &res) + { + if (onShutdown) + onShutdown(); + setJson(res, json{{"ok", true}}, 200); + } + +} // namespace + +namespace Net::Admin +{ + AdminWebServer::AdminWebServer(std::shared_ptr sessions, + std::shared_ptr rooms, std::function onShutdown, int port, std::string token) + : _sessions(std::move(sessions)), _rooms(std::move(rooms)), _onShutdown(std::move(onShutdown)), _port(port), + _token(std::move(token)) + { + } + + AdminWebServer::~AdminWebServer() + { + stop(); + } + + void AdminWebServer::start() + { + if (bool expected = false; !_running.compare_exchange_strong(expected, true)) + return; + _thread = std::thread(&AdminWebServer::run, this); + } + + void AdminWebServer::stop() noexcept + { + _running.store(false, std::memory_order_relaxed); + if (_srv) + _srv->stop(); + if (_thread.joinable() && std::this_thread::get_id() != _thread.get_id()) + _thread.join(); + } + + int AdminWebServer::port() const noexcept + { + return _port; + } + + void AdminWebServer::run() + { + _srv = std::make_unique(); + auto &srv = *_srv; + srv.Get("/", [](const httplib::Request &, httplib::Response &res) { + res.set_content(kIndexHtml, "text/html; charset=utf-8"); + }); + srv.Get("/style.css", [](const httplib::Request &, httplib::Response &res) { + res.set_content(kStyleCss, "text/css; charset=utf-8"); + }); + srv.Get("/app.js", [](const httplib::Request &, httplib::Response &res) { + res.set_content(kAppJs, "application/javascript; charset=utf-8"); + }); + auto requireAuth = [&](const httplib::Request &req, httplib::Response &res) -> bool { + if (!hasBearerToken(req, _token)) { + res.status = 401; + res.set_content("Unauthorized", "text/plain; charset=utf-8"); + return false; + } + return true; + }; + + srv.Get("/api/status", [&](const httplib::Request &req, httplib::Response &res) { + if (!requireAuth(req, res)) + return; + handleStatus(_sessions, _rooms, res); + }); + + srv.Get("/api/rooms", [&](const httplib::Request &req, httplib::Response &res) { + if (!requireAuth(req, res)) + return; + handleRooms(_rooms, res); + }); + + srv.Get("/api/sessions", [&](const httplib::Request &req, httplib::Response &res) { + if (!requireAuth(req, res)) + return; + handleSessions(_sessions, _rooms, res); + }); + + srv.Post("/api/kick", [&](const httplib::Request &req, httplib::Response &res) { + if (!requireAuth(req, res)) + return; + handleKick(_sessions, _rooms, req, res); + }); + + srv.Post("/api/kickroom", [&](const httplib::Request &req, httplib::Response &res) { + if (!requireAuth(req, res)) + return; + handleKickRoom(_sessions, _rooms, req, res); + }); + + srv.Post("/api/banroom", [&](const httplib::Request &req, httplib::Response &res) { + if (!requireAuth(req, res)) + return; + handleBanRoom(_sessions, _rooms, req, res); + }); + + srv.Post("/api/unbanroom", [&](const httplib::Request &req, httplib::Response &res) { + if (!requireAuth(req, res)) + return; + handleUnbanRoom(_sessions, _rooms, req, res); + }); + + srv.Post("/api/shutdown", [&](const httplib::Request &req, httplib::Response &res) { + if (!requireAuth(req, res)) + return; + handleShutdown(_onShutdown, res); + }); + + srv.listen("127.0.0.1", _port); + _srv.reset(); + } +} // namespace Net::Admin diff --git a/server/src/admin/webServer/AdminWebServer.hpp b/server/src/admin/webServer/AdminWebServer.hpp new file mode 100644 index 000000000..eccfb1216 --- /dev/null +++ b/server/src/admin/webServer/AdminWebServer.hpp @@ -0,0 +1,88 @@ +/* +** EPITECH PROJECT, 2026 +** R-Type +** File description: +** AdminWebServer +*/ + +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef _WIN32 + #include +#else + #include + #include +#endif + +#include "ISessionManager.hpp" +#include "RoomManager.hpp" + +namespace Net::Admin +{ + /** + * @class AdminWebServer + * @brief A simple administrative web server for managing sessions and rooms. + */ + class AdminWebServer { + public: + /** + * @brief Constructs a new AdminWebServer object. + * @param sessions Shared pointer to the session manager. + * @param rooms Shared pointer to the room manager. + * @param onShutdown Callback function to be called on server shutdown. + * @param port Port number for the web server (default is 8082). + * @param token Authentication token for accessing admin endpoints (default is "admin"). + */ + AdminWebServer(std::shared_ptr sessions, std::shared_ptr rooms, + std::function onShutdown, int port = 8082, std::string token = "admin"); + + /** + * @brief Destroys the AdminWebServer object and stops the server if running. + */ + ~AdminWebServer(); + + /** + * @brief Starts the admin web server in a separate thread. + */ + void start(); + + /** + * @brief Stops the admin web server. + */ + void stop() noexcept; + + /** + * @brief Gets the port number the server is running on. + * @return The port number. + */ + [[nodiscard]] int port() const noexcept; + + private: + void run(); ///> Thread function to run the web server + + std::shared_ptr _sessions; ///> Session manager + std::shared_ptr _rooms; ///> Room manager + std::function _onShutdown; ///> Shutdown callback + std::unique_ptr _srv; ///> HTTP server instance + + int _port = 8082; ///> Server port + std::string _token; ///> Authentication token + + std::atomic _running{false}; ///> Flag indicating if the server is running + std::thread _thread; ///> Thread for the web server + }; +} // namespace Net::Admin