diff --git a/frontend/react/src/interfaces.ts b/frontend/react/src/interfaces.ts index 407ca7e6..af34f7dd 100644 --- a/frontend/react/src/interfaces.ts +++ b/frontend/react/src/interfaces.ts @@ -43,7 +43,7 @@ export interface OlympusConfig { authentication?: { // Only sent when in localhost mode for autologin gameMasterPassword: string; - blueCommanderPasword: string; + blueCommanderPassword: string; redCommanderPassword: string; }; } diff --git a/frontend/react/src/mission/missionmanager.ts b/frontend/react/src/mission/missionmanager.ts index d61247dd..68812ceb 100644 --- a/frontend/react/src/mission/missionmanager.ts +++ b/frontend/react/src/mission/missionmanager.ts @@ -275,11 +275,9 @@ export class MissionManager { var requestRefresh = false; if (this.#commandModeOptions.commandMode === NONE && commandModeOptions.commandMode !== NONE) requestRefresh = true; - /* Refresh the page if we have lost Game Master priviledges */ - if (this.#commandModeOptions.commandMode === GAME_MASTER && commandModeOptions.commandMode !== GAME_MASTER) location.reload(); - /* Check if any option has changed */ var commandModeOptionsChanged = + commandModeOptions.commandMode !== this.getCommandModeOptions().commandMode || !commandModeOptions.eras.every((value: string, idx: number) => { return value === this.getCommandModeOptions().eras[idx]; }) || diff --git a/frontend/react/src/ui/panels/drawingmenu.tsx b/frontend/react/src/ui/panels/drawingmenu.tsx index 472e1c03..c7c6dfa6 100644 --- a/frontend/react/src/ui/panels/drawingmenu.tsx +++ b/frontend/react/src/ui/panels/drawingmenu.tsx @@ -16,7 +16,7 @@ import { DrawSubState, ERAS_ORDER, IADSTypes, NO_SUBSTATE, OlympusState, Olympus import { AppStateChangedEvent, CoalitionAreasChangedEvent, CoalitionAreaSelectedEvent, DrawingsInitEvent, DrawingsUpdatedEvent } from "../../events"; import { FaCopy, FaPencil, FaRegCompass, FaXmark } from "react-icons/fa6"; import { deepCopyTable } from "../../other/utils"; -import { DCSDrawingsContainer, DCSEmptyLayer } from "../../map/drawings/drawingsmanager"; +import { DCSDrawing, DCSDrawingsContainer, DCSEmptyLayer } from "../../map/drawings/drawingsmanager"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { OlSearchBar } from "../components/olsearchbar"; @@ -67,6 +67,10 @@ export function DrawingMenu(props: { open: boolean; onClose: () => void }) { CoalitionAreasChangedEvent.on((coalitionAreas) => setCoalitionAreas([...coalitionAreas])); }, []); + function getDrawingLabelColor(drawing: DCSDrawingsContainer | DCSDrawing) { + return drawing.getVisibility() ? `text-gray-200` : `text-gray-600`; + } + function renderDrawingsContainerControls(container: DCSDrawingsContainer, containerSearchString: string) { if (container.hasSearchString(containerSearchString)) { /* The following snippet automatically open containers that contains searched drawings */ @@ -98,8 +102,10 @@ export function DrawingMenu(props: { open: boolean; onClose: () => void }) { { if (container === mainDrawingsContainer.container) { @@ -111,7 +117,9 @@ export function DrawingMenu(props: { open: boolean; onClose: () => void }) { />
{container.getName()} @@ -139,15 +147,20 @@ export function DrawingMenu(props: { open: boolean; onClose: () => void }) { { drawing.setVisibility(!drawing.getVisibility()); }} /> -
{drawing.getName()}
+
{drawing.getName()}
{ HiddenTypesChangedEvent.on((hiddenTypes) => setMapHiddenTypes({ ...hiddenTypes })); @@ -60,9 +72,11 @@ export function Header() { }); CommandModeOptionsChangedEvent.on((commandModeOptions) => { setCommandModeOptions(commandModeOptions); + setLoadingNewCommandMode(false); }); SessionDataChangedEvent.on(() => setSavingSessionData(true)); SessionDataSavedEvent.on(() => setSavingSessionData(false)); + EnabledCommandModesChangedEvent.on((enabledCommandModes) => setEnabledCommandModes(enabledCommandModes)); /* Check if we are running the latest version */ const request = new Request("https://raw.githubusercontent.com/Pax1601/DCSOlympus/main/version.json"); @@ -213,14 +227,79 @@ export function Header() { )}
+ {commandModeOptions.commandMode === GAME_MASTER && ( +
{ + if (enabledCommandModes.length > 0) { + let blueCommandModeIndex = enabledCommandModes.indexOf(BLUE_COMMANDER); + let redCommandModeIndex = enabledCommandModes.indexOf(RED_COMMANDER); + if (blueCommandModeIndex >= 0) getApp().getServerManager().setActiveCommandMode(BLUE_COMMANDER); + else if (redCommandModeIndex >= 0) getApp().getServerManager().setActiveCommandMode(RED_COMMANDER); + setLoadingNewCommandMode(true); + } + }} + > + Game Master + {enabledCommandModes.length > 0 && ( + <>{loadingNewCommandMode ? : } + )} +
+ )} {commandModeOptions.commandMode === BLUE_COMMANDER && ( -
- BLUE Commander ({commandModeOptions.spawnPoints.blue} points) +
{ + if (enabledCommandModes.length > 0) { + let gameMasterCommandModeIndex = enabledCommandModes.indexOf(GAME_MASTER); + let redCommandModeIndex = enabledCommandModes.indexOf(RED_COMMANDER); + if (redCommandModeIndex >= 0) getApp().getServerManager().setActiveCommandMode(RED_COMMANDER); + else if (gameMasterCommandModeIndex >= 0) getApp().getServerManager().setActiveCommandMode(GAME_MASTER); + setLoadingNewCommandMode(true); + } + }} + > + BLUE Commander + {enabledCommandModes.length > 0 && ( + <>{loadingNewCommandMode ? : } + )}
)} {commandModeOptions.commandMode === RED_COMMANDER && ( -
- BLUE Commander ({commandModeOptions.spawnPoints.blue} points) +
{ + if (enabledCommandModes.length > 0) { + let gameMasterCommandModeIndex = enabledCommandModes.indexOf(GAME_MASTER); + let blueCommandModeIndex = enabledCommandModes.indexOf(BLUE_COMMANDER); + if (gameMasterCommandModeIndex >= 0) getApp().getServerManager().setActiveCommandMode(GAME_MASTER); + else if (blueCommandModeIndex >= 0) getApp().getServerManager().setActiveCommandMode(BLUE_COMMANDER); + setLoadingNewCommandMode(true); + } + }} + > + RED Commander + {enabledCommandModes.length > 0 && ( + <>{loadingNewCommandMode ? : } + )}
)}
diff --git a/frontend/server/src/app.ts b/frontend/server/src/app.ts index 0ceb1a37..403af0aa 100644 --- a/frontend/server/src/app.ts +++ b/frontend/server/src/app.ts @@ -10,42 +10,7 @@ import expressBasicAuth from "express-basic-auth"; /* Load the proxy middleware plugin */ import httpProxyMiddleware = require("http-proxy-middleware"); - -function checkCustomHeaders(config, usersConfig, groupsConfig, req) { - let user = req.auth?.user ?? null; - let group = null; - - /* Check if custom authorization headers are enabled */ - if ( - "customAuthHeaders" in config["frontend"] && - config["frontend"]["customAuthHeaders"]["enabled"] - ) { - /* If so, check that the custom headers are indeed present */ - if ( - config["frontend"]["customAuthHeaders"]["username"].toLowerCase() in - req.headers && - config["frontend"]["customAuthHeaders"]["group"].toLowerCase() in - req.headers - ) { - /* If they are, assign the group */ - group = - req.headers[ - config["frontend"]["customAuthHeaders"]["group"].toLowerCase() - ]; - - /* Check that the user is in an existing group */ - if (group in groupsConfig) { - user = - req.headers[ - config["frontend"]["customAuthHeaders"]["username"].toLowerCase() - ]; - usersConfig[user] = { password: null, roles: groupsConfig[group] }; - } - } - } - - return user; -} +import { getUserFromCustomHeaders, connectionIsLocal } from "./utils"; module.exports = function (configLocation, viteProxy) { /* Config specific routers */ @@ -53,41 +18,25 @@ module.exports = function (configLocation, viteProxy) { const resourcesRouter = require("./routes/resources")(configLocation); const adminRouter = require("./routes/admin")(configLocation); + /* Database routers */ + const databasesLocation = path.join(path.dirname(configLocation), "..", "Mods", "Services", "Olympus", "databases"); + const databasesRouter = require("./routes/api/databases")(databasesLocation); + /* Default routers */ const airbasesRouter = require("./routes/api/airbases"); - const databasesRouter = require("./routes/api/databases")( - path.join( - path.dirname(configLocation), - "..", - "Mods", - "Services", - "Olympus", - "databases" - ) - ); const speechRouter = require("./routes/api/speech")(); /* Read the users configuration file */ let usersConfig = {}; - if ( - fs.existsSync(path.join(path.dirname(configLocation), "olympusUsers.json")) - ) { - let rawdata = fs.readFileSync( - path.join(path.dirname(configLocation), "olympusUsers.json"), - { encoding: "utf-8" } - ); + if (fs.existsSync(path.join(path.dirname(configLocation), "olympusUsers.json"))) { + let rawdata = fs.readFileSync(path.join(path.dirname(configLocation), "olympusUsers.json"), { encoding: "utf-8" }); usersConfig = JSON.parse(rawdata); } /* Read the groups configuration file */ let groupsConfig = {}; - if ( - fs.existsSync(path.join(path.dirname(configLocation), "olympusGroups.json")) - ) { - let rawdata = fs.readFileSync( - path.join(path.dirname(configLocation), "olympusGroups.json"), - { encoding: "utf-8" } - ); + if (fs.existsSync(path.join(path.dirname(configLocation), "olympusGroups.json"))) { + let rawdata = fs.readFileSync(path.join(path.dirname(configLocation), "olympusGroups.json"), { encoding: "utf-8" }); groupsConfig = JSON.parse(rawdata); } @@ -109,23 +58,21 @@ module.exports = function (configLocation, viteProxy) { const app = express(); /* Define the authentication */ - const defaultUsers = { + const commandRoles = { "Game master": config["authentication"]["gameMasterPassword"], "Blue commander": config["authentication"]["blueCommanderPassword"], "Red commander": config["authentication"]["redCommanderPassword"], }; if (config["authentication"]["adminPassword"]) { - defaultUsers["Admin"] = config["authentication"]["adminPassword"]; + commandRoles["Admin"] = config["authentication"]["adminPassword"]; } let users = {}; - Object.keys(usersConfig).forEach( - (user) => (users[user] = usersConfig[user].password) - ); + Object.keys(usersConfig).forEach((user) => (users[user] = usersConfig[user].password)); const auth = expressBasicAuth({ - users: { ...defaultUsers, ...users }, + users: { ...commandRoles, ...users }, }); - /* Define middleware */ + /* Define logging middleware */ app.use( logger("dev", { skip: function (req, res) { @@ -135,28 +82,18 @@ module.exports = function (configLocation, viteProxy) { ); /* Authorization middleware */ - if ( - "customAuthHeaders" in config["frontend"] && - config["frontend"]["customAuthHeaders"]["enabled"] - ) { + if ("customAuthHeaders" in config["frontend"] && config["frontend"]["customAuthHeaders"]["enabled"]) { /* Custom authorization will be used */ app.use("/", async (req, res, next) => { - const user = checkCustomHeaders(config, usersConfig, groupsConfig, req); + const user = getUserFromCustomHeaders(config, usersConfig, groupsConfig, req); + + const customHeadersUsername = config["frontend"]["customAuthHeaders"]["username"].toLowerCase(); + const customHeadersGroup = config["frontend"]["customAuthHeaders"]["group"].toLowerCase(); if (user) { /* If the user is preauthorized, set the authorization headers to the response */ - res.set( - config["frontend"]["customAuthHeaders"]["username"], - req.headers[ - config["frontend"]["customAuthHeaders"]["username"].toLowerCase() - ] - ); - res.set( - config["frontend"]["customAuthHeaders"]["group"], - req.headers[ - config["frontend"]["customAuthHeaders"]["group"].toLowerCase() - ] - ); + res.set(customHeadersUsername, req.headers[customHeadersUsername]); + res.set(customHeadersGroup, req.headers[customHeadersGroup]); } next(); @@ -169,10 +106,7 @@ module.exports = function (configLocation, viteProxy) { /* Define the middleware to replace the authorization header for the olympus backend */ app.use("/olympus", async (req, res, next) => { /* Check if custom authorization headers are being used */ - const user = - //@ts-ignore - req.auth?.user ?? - checkCustomHeaders(config, usersConfig, groupsConfig, req); + const user = req.auth?.user ?? getUserFromCustomHeaders(config, usersConfig, groupsConfig, req); /* If either simple authentication or custom authentication has succeded */ if (user) { @@ -185,12 +119,10 @@ module.exports = function (configLocation, viteProxy) { /* Check that the user is authorized to that role */ if (userConfig.roles.includes(req.headers["x-command-mode"])) { /* Check that the role is valid */ - //@ts-ignore - if (req.headers["x-command-mode"] in defaultUsers) { + if (req.headers["x-command-mode"] in commandRoles) { /* Apply the authorization headers */ req.headers.authorization = `Basic ${btoa( - //@ts-ignore - user + ":" + defaultUsers[req.headers["x-command-mode"]] + user + ":" + commandRoles[req.headers["x-command-mode"]] )}`; } else { res.sendStatus(401); // Unauthorized @@ -201,23 +133,33 @@ module.exports = function (configLocation, viteProxy) { } else { /* No role has been specified, continue with the highest role */ /* Check that the role is valid */ - if (userConfig.roles[0] in defaultUsers) { + if (userConfig.roles[0] in commandRoles) { /* Apply the authorization headers */ - req.headers.authorization = `Basic ${btoa( - userConfig.roles[0] + ":" + defaultUsers[userConfig.roles[0]] - )}`; + req.headers.authorization = `Basic ${btoa(userConfig.roles[0] + ":" + commandRoles[userConfig.roles[0]])}`; } else { res.sendStatus(401); // Unauthorized } } } else { - if (!(user in defaultUsers)) res.sendStatus(401); // Unauthorized + if (!(user in commandRoles)) res.sendStatus(401); // Unauthorized } /* Send back the roles that the user is enabled to */ - if (userConfig) res.set("X-Enabled-Command-Modes", `${userConfig.roles}`); - else if (user in defaultUsers) - res.set("X-Enabled-Command-Modes", `${user}`); + if (connectionIsLocal(config, req)) { + /* If the connection is local, all roles are enabled */ + res.set("X-Enabled-Command-Modes", "Game master,Blue commander,Red commander"); + if (req.headers["x-command-mode"]) { + const commandMode = req.headers["x-command-mode"]; + /* Apply the authorization headers */ + if (commandMode in commandRoles) { + req.headers.authorization = `Basic ${btoa(commandMode + ":" + commandRoles[commandMode])}`; + } + } + } else { + /* If the connection is not local, only the roles that the user is enabled to are enabled */ + if (userConfig) res.set("X-Enabled-Command-Modes", `${userConfig.roles}`); + else if (user in commandRoles) res.set("X-Enabled-Command-Modes", `${user}`); + } next(); } else { res.sendStatus(401); // Unauthorized @@ -225,28 +167,27 @@ module.exports = function (configLocation, viteProxy) { }); /* Proxy middleware */ + /* If a port is defined we assume the backend is of the type IP:port */ if (config["backend"]["port"]) { app.use( "/olympus", httpProxyMiddleware.createProxyMiddleware({ - target: `http://${ - backendAddress === "*" ? "localhost" : backendAddress - }:${config["backend"]["port"]}/olympus`, + target: `http://${backendAddress === "*" ? "localhost" : backendAddress}:${config["backend"]["port"]}/olympus`, changeOrigin: true, }) ); } else { + /* Otherwise we assume it is a url */ app.use( "/olympus", httpProxyMiddleware.createProxyMiddleware({ - target: `https://${ - backendAddress === "*" ? "localhost" : backendAddress - }/olympus`, + target: `https://${backendAddress === "*" ? "localhost" : backendAddress}/olympus`, changeOrigin: true, }) ); } + /* More middleware */ app.use(bodyParser.json({ limit: "50mb" })); app.use(bodyParser.urlencoded({ limit: "50mb", extended: true })); app.use(express.static(path.join(__dirname, "..", "public"))); @@ -259,10 +200,12 @@ module.exports = function (configLocation, viteProxy) { app.use("/api/speech", speechRouter); app.use("/resources", resourcesRouter); + /* Admin routers */ app.use("/admin", auth); app.use("/admin", adminRouter); /* Set default index */ + /* If we are in Vite mode, proxy the requests to the vite server */ if (viteProxy) { app.use( "/", @@ -272,6 +215,7 @@ module.exports = function (configLocation, viteProxy) { }) ); } else { + /* Otherwise serve the static files */ app.get("/", function (req, res) { res.sendFile(path.join(__dirname, "..", "public", "index.html")); }); @@ -279,10 +223,7 @@ module.exports = function (configLocation, viteProxy) { /* Start the audio backend */ if (config["audio"]) { - let audioBackend = new AudioBackend( - config["audio"]["SRSPort"], - config["audio"]["WSPort"] - ); + let audioBackend = new AudioBackend(config["audio"]["SRSPort"], config["audio"]["WSPort"]); audioBackend.start(); } diff --git a/frontend/server/src/routes/resources.ts b/frontend/server/src/routes/resources.ts index c29fb178..6e556984 100644 --- a/frontend/server/src/routes/resources.ts +++ b/frontend/server/src/routes/resources.ts @@ -1,6 +1,7 @@ import express = require("express"); import fs = require("fs"); import path = require("path"); +import { connectionIsLocal as checkConnectionIsLocal } from "../utils"; const router = express.Router(); @@ -26,10 +27,7 @@ module.exports = function (configLocation) { let rawdata = fs.readFileSync(configLocation, "utf-8"); const config = JSON.parse(rawdata); - /* Check if the connection is local */ - let local = false; - if (config.frontend.autoconnectWhenLocal) - local = req.headers[config.frontend.proxyHeader] === undefined; + const local = checkConnectionIsLocal(config, req); let resConfig = { frontend: { ...config.frontend }, diff --git a/frontend/server/src/utils.ts b/frontend/server/src/utils.ts index da095138..dc555b20 100644 --- a/frontend/server/src/utils.ts +++ b/frontend/server/src/utils.ts @@ -28,3 +28,47 @@ export function doubleToByteArray(number) { export function byteArrayToDouble(array) { return new DataView(array.reverse().buffer).getFloat64(0); } + +export function connectionIsLocal(config, req) { + /* Check if the connection is local, and if autoconnection is enabled */ + let local = false; + if (config.frontend.autoconnectWhenLocal) { + + var ip = req.connection.remoteAddress; + var host = req.get('host'); + + /* If the request address is not localhost, we are not local */ + if (!(ip === "127.0.0.1" || ip === "::ffff:127.0.0.1" || ip === "::1" || host.indexOf("localhost") !== -1)) { + local = false; + } else { + /* If the request address is localhost, we are local unless a proxyHeader is present (to be used with reverse proxies) */ + local = req.headers[config.frontend.proxyHeader] === undefined; + } + } + return local; +} + +export function getUserFromCustomHeaders(config, usersConfig, groupsConfig, req) { + let user = req.auth?.user ?? null; + let group = null; + + /* Check if custom authorization headers are enabled */ + if ("customAuthHeaders" in config["frontend"] && config["frontend"]["customAuthHeaders"]["enabled"]) { + /* If so, check that the custom headers are indeed present */ + if ( + config["frontend"]["customAuthHeaders"]["username"].toLowerCase() in req.headers && + config["frontend"]["customAuthHeaders"]["group"].toLowerCase() in req.headers + ) { + /* If they are, assign the group */ + group = req.headers[config["frontend"]["customAuthHeaders"]["group"].toLowerCase()]; + + /* Check that the user is in an existing group */ + if (group in groupsConfig) { + user = req.headers[config["frontend"]["customAuthHeaders"]["username"].toLowerCase()]; + usersConfig[user] = { password: null, roles: groupsConfig[group] }; + } + } + } + + return user; +}