diff --git a/frontend/react/src/constants/constants.ts b/frontend/react/src/constants/constants.ts index 4fd77a29..7da5ae1c 100644 --- a/frontend/react/src/constants/constants.ts +++ b/frontend/react/src/constants/constants.ts @@ -300,6 +300,13 @@ export enum UnitControlSubState { UNIT_EXPLOSION_MENU = "Unit explosion menu", } +export enum LoginSubState { + NO_SUBSTATE = "No substate", + CREDENTIALS = "Credentials", + COMMAND_MODE = "Command mode", + CONNECT = "Connect" +} + export enum DrawSubState { NO_SUBSTATE = "No substate", DRAW_POLYGON = "Polygon", diff --git a/frontend/react/src/interfaces.ts b/frontend/react/src/interfaces.ts index 32d9edd1..c34e3e22 100644 --- a/frontend/react/src/interfaces.ts +++ b/frontend/react/src/interfaces.ts @@ -4,6 +4,11 @@ import { MapOptions } from "./types/types"; export interface OlympusConfig { frontend: { port: number; + customAuthHeaders: { + enabled: boolean, + username: string, + group: string + } elevationProvider: { provider: string; username: string | null; @@ -26,7 +31,7 @@ export interface OlympusConfig { WSPort?: number; WSEndpoint?: string; }; - profiles?: ProfileOptions; + profiles?: {[key: string]: ProfileOptions}; } export interface SessionData { @@ -277,6 +282,7 @@ export interface UnitBlueprint { indirectFire?: boolean; markerFile?: string; unitWhenGrouped?: string; + mainRole?: string; } export interface AirbaseOptions { diff --git a/frontend/react/src/map/map.ts b/frontend/react/src/map/map.ts index 80f58d9c..a582a05b 100644 --- a/frontend/react/src/map/map.ts +++ b/frontend/react/src/map/map.ts @@ -546,8 +546,9 @@ export class Map extends L.Map { this.#spawnRequestTable = spawnRequestTable; } - addStarredSpawnRequestTable(key, spawnRequestTable: SpawnRequestTable) { + addStarredSpawnRequestTable(key, spawnRequestTable: SpawnRequestTable, quickAccessName: string) { this.#starredSpawnRequestTables[key] = spawnRequestTable; + this.#starredSpawnRequestTables[key].quickAccessName = quickAccessName; StarredSpawnsChangedEvent.dispatch(this.#starredSpawnRequestTables); } diff --git a/frontend/react/src/mission/missionmanager.ts b/frontend/react/src/mission/missionmanager.ts index f95ff5de..79f107e6 100644 --- a/frontend/react/src/mission/missionmanager.ts +++ b/frontend/react/src/mission/missionmanager.ts @@ -32,6 +32,7 @@ export class MissionManager { #remainingSetupTime: number = 0; #spentSpawnPoint: number = 0; #coalitions: { red: string[]; blue: string[] } = { red: [], blue: [] }; + #enabledCommandModes: string[] = []; constructor() { AppStateChangedEvent.on((state, subState) => { @@ -220,6 +221,14 @@ export class MissionManager { return airbase ?? null; } + setEnabledCommandModes(enabledCommandModes: string[]) { + this.#enabledCommandModes = enabledCommandModes; + } + + getEnabledCommandModes() { + return this.#enabledCommandModes; + } + #setcommandModeOptions(commandModeOptions: CommandModeOptions) { /* Refresh all the data if we have exited the NONE state */ var requestRefresh = false; diff --git a/frontend/react/src/olympusapp.ts b/frontend/react/src/olympusapp.ts index d7cc97c7..0e2fd653 100644 --- a/frontend/react/src/olympusapp.ts +++ b/frontend/react/src/olympusapp.ts @@ -20,7 +20,7 @@ import { WeaponsManager } from "./weapon/weaponsmanager"; import { ServerManager } from "./server/servermanager"; import { AudioManager } from "./audio/audiomanager"; -import { NO_SUBSTATE, OlympusState, OlympusSubState } from "./constants/constants"; +import { LoginSubState, NO_SUBSTATE, OlympusState, OlympusSubState } from "./constants/constants"; import { AppStateChangedEvent, ConfigLoadedEvent, InfoPopupEvent, MapOptionsChangedEvent, SelectedUnitsChangedEvent, ShortcutsChangedEvent } from "./events"; import { OlympusConfig, ProfileOptions } from "./interfaces"; import { AWACSController } from "./controllers/awacs"; @@ -32,27 +32,26 @@ export var IP = window.location.toString(); export class OlympusApp { /* Global data */ #latestVersion: string | undefined = undefined; - #config: OlympusConfig | null = null; + #config: OlympusConfig; #state: OlympusState = OlympusState.NOT_INITIALIZED; #subState: OlympusSubState = NO_SUBSTATE; #infoMessages: string[] = []; - #profileName: string | null = null; /* Main leaflet map, extended by custom methods */ - #map: Map | null = null; + #map: Map; /* Managers */ - #missionManager: MissionManager | null = null; - #serverManager: ServerManager | null = null; - #shortcutManager: ShortcutManager | null = null; - #unitsManager: UnitsManager | null = null; - #weaponsManager: WeaponsManager | null = null; - #audioManager: AudioManager | null = null; - #sessionDataManager: SessionDataManager | null = null; + #missionManager: MissionManager; + #serverManager: ServerManager; + #shortcutManager: ShortcutManager; + #unitsManager: UnitsManager; + #weaponsManager: WeaponsManager; + #audioManager: AudioManager; + #sessionDataManager: SessionDataManager; //#pluginsManager: // TODO /* Controllers */ - #AWACSController: AWACSController | null = null; + #AWACSController: AWACSController; constructor() { SelectedUnitsChangedEvent.on((selectedUnits) => { @@ -69,31 +68,31 @@ export class OlympusApp { } getServerManager() { - return this.#serverManager as ServerManager; + return this.#serverManager; } getShortcutManager() { - return this.#shortcutManager as ShortcutManager; + return this.#shortcutManager; } getUnitsManager() { - return this.#unitsManager as UnitsManager; + return this.#unitsManager; } getWeaponsManager() { - return this.#weaponsManager as WeaponsManager; + return this.#weaponsManager; } getMissionManager() { - return this.#missionManager as MissionManager; + return this.#missionManager; } getAudioManager() { - return this.#audioManager as AudioManager; + return this.#audioManager; } getSessionDataManager() { - return this.#sessionDataManager as SessionDataManager; + return this.#sessionDataManager; } /* TODO @@ -154,20 +153,37 @@ export class OlympusApp { }); /* Load the config file from the server */ - const configRequest = new Request(this.getExpressAddress() + "/resources/config"); + const configRequest = new Request(this.getExpressAddress() + "/resources/config", { + headers: { + 'Cache-Control': 'no-cache', + } + }); + fetch(configRequest) .then((response) => { - if (response.status === 200) { - return response.json(); - } else { - throw new Error("Error retrieving config file"); - } + if (response.status === 200) + return new Promise((res: ([OlympusConfig, Headers]) => void, rej) => { + response + .json() + .then((result) => res([result, response.headers])) + .catch((error) => rej(error)); + }); + else throw new Error("Error retrieving config file"); }) - .then((res) => { - this.#config = res; + .then(([result, headers]) => { + this.#config = result; + if (this.#config.frontend.customAuthHeaders.enabled) { + if (headers.has(this.#config.frontend.customAuthHeaders.username) && headers.has(this.#config.frontend.customAuthHeaders.group)) { + this.getServerManager().setUsername(headers.get(this.#config.frontend.customAuthHeaders.username)); + this.setState(OlympusState.LOGIN, LoginSubState.COMMAND_MODE); + } + } + if (this.getState() !== OlympusState.LOGIN) { + this.setState(OlympusState.LOGIN, LoginSubState.CREDENTIALS); + } ConfigLoadedEvent.dispatch(this.#config as OlympusConfig); - this.setState(OlympusState.LOGIN); - }); + }) + .catch((error) => console.error); this.#shortcutManager?.addShortcut("idle", { label: "Deselect all", @@ -184,12 +200,9 @@ export class OlympusApp { return this.#config; } - setProfile(profileName: string) { - this.#profileName = profileName; - } - saveProfile() { - if (this.#profileName !== null) { + const username = this.getServerManager()?.getUsername(); + if (username) { let profile = {}; profile["mapOptions"] = this.#map?.getOptions(); profile["shortcuts"] = this.#shortcutManager?.getShortcutsOptions(); @@ -200,10 +213,10 @@ export class OlympusApp { body: JSON.stringify(profile), // Send the data in JSON format }; - fetch(this.getExpressAddress() + `/resources/profile/${this.#profileName}`, requestOptions) + fetch(this.getExpressAddress() + `/resources/profile/${username}`, requestOptions) .then((response) => { if (response.status === 200) { - console.log(`Profile ${this.#profileName} saved correctly`); + console.log(`Profile for ${username} saved correctly`); } else { this.addInfoMessage("Error saving profile"); throw new Error("Error saving profile"); @@ -214,17 +227,18 @@ export class OlympusApp { } resetProfile() { - if (this.#profileName !== null) { + const username = this.getServerManager().getUsername(); + if (username) { const requestOptions = { method: "PUT", // Specify the request method headers: { "Content-Type": "application/json" }, // Specify the content type body: "", // Send the data in JSON format }; - fetch(this.getExpressAddress() + `/resources/profile/reset/${this.#profileName}`, requestOptions) + fetch(this.getExpressAddress() + `/resources/profile/reset/${username}`, requestOptions) .then((response) => { if (response.status === 200) { - console.log(`Profile ${this.#profileName} reset correctly`); + console.log(`Profile for ${username} reset correctly`); location.reload(); } else { this.addInfoMessage("Error resetting profile"); @@ -256,22 +270,19 @@ export class OlympusApp { } getProfile() { - if (this.#profileName && this.#config?.profiles && this.#config?.profiles[this.#profileName]) - return this.#config?.profiles[this.#profileName] as ProfileOptions; + const username = this.getServerManager().getUsername(); + if (username && this.#config?.profiles && username in this.#config.profiles) return this.#config?.profiles[username]; else return null; } - getProfileName() { - return this.#profileName; - } - loadProfile() { + const username = this.getServerManager().getUsername(); const profile = this.getProfile(); - if (profile) { + if (username && profile) { this.#map?.setOptions(profile.mapOptions); this.#shortcutManager?.setShortcutsOptions(profile.shortcuts); this.addInfoMessage("Profile loaded correctly"); - console.log(`Profile ${this.#profileName} loaded correctly`); + console.log(`Profile for ${username} loaded correctly`); } else { this.addInfoMessage("Profile not found, creating new profile"); console.log(`Error loading profile`); @@ -302,6 +313,4 @@ export class OlympusApp { InfoPopupEvent.dispatch(this.#infoMessages); }, 5000); } - - } diff --git a/frontend/react/src/other/utils.ts b/frontend/react/src/other/utils.ts index b9ca3b9b..e398937b 100644 --- a/frontend/react/src/other/utils.ts +++ b/frontend/react/src/other/utils.ts @@ -404,4 +404,26 @@ export function blobToBase64(blob) { reader.onloadend = () => resolve(reader.result as string); reader.readAsDataURL(blob); }); +} + +export function mode(array) +{ + if(array.length == 0) + return null; + var modeMap = {}; + var maxEl = array[0], maxCount = 1; + for(var i = 0; i < array.length; i++) + { + var el = array[i]; + if(modeMap[el] == null) + modeMap[el] = 1; + else + modeMap[el]++; + if(modeMap[el] > maxCount) + { + maxEl = el; + maxCount = modeMap[el]; + } + } + return maxEl; } \ No newline at end of file diff --git a/frontend/react/src/server/servermanager.ts b/frontend/react/src/server/servermanager.ts index 8e2f00e6..089b6911 100644 --- a/frontend/react/src/server/servermanager.ts +++ b/frontend/react/src/server/servermanager.ts @@ -20,8 +20,8 @@ export class ServerManager { #connected: boolean = false; #paused: boolean = false; #REST_ADDRESS = "http://localhost:3001/olympus"; - #username = "no-username"; - #password = ""; + #username: null | string = null; + #password: null | string = null; #sessionHash: string | null = null; #lastUpdateTimes: { [key: string]: number } = {}; #previousMissionElapsedTime: number = 0; // Track if mission elapsed time is increasing (i.e. is the server paused) @@ -29,6 +29,7 @@ export class ServerManager { #intervals: number[] = []; #requests: { [key: string]: XMLHttpRequest } = {}; #updateMode = "normal"; // normal or awacs + #activeCommandMode = ""; constructor() { this.#lastUpdateTimes[UNITS_URI] = Date.now(); @@ -61,10 +62,18 @@ export class ServerManager { this.#username = newUsername; } + getUsername() { + return this.#username; + } + setPassword(newPassword: string) { this.#password = newPassword; } + setActiveCommandMode(activeCommandMode: string) { + this.#activeCommandMode = activeCommandMode; + } + GET( callback: CallableFunction, errorCallback: CallableFunction, @@ -94,7 +103,8 @@ export class ServerManager { xmlHttp.open("GET", `${this.#REST_ADDRESS}/${uri}${optionsString ? `?${optionsString}` : ""}`, true); /* If provided, set the credentials */ - if (this.#username && this.#password) xmlHttp.setRequestHeader("Authorization", "Basic " + btoa(`${this.#username}:${this.#password}`)); + xmlHttp.setRequestHeader("Authorization", "Basic " + btoa(`${this.#username ?? ""}:${this.#password ?? ""}`)); + xmlHttp.setRequestHeader("X-Command-Mode", this.#activeCommandMode); /* If specified, set the response type */ if (responseType) xmlHttp.responseType = responseType as XMLHttpRequestResponseType; @@ -105,6 +115,10 @@ export class ServerManager { this.setConnected(true); if (xmlHttp.responseType == "arraybuffer") this.#lastUpdateTimes[uri] = callback(xmlHttp.response); else { + /* Check if the response headers contain the enabled command modes and set them */ + if (xmlHttp.getResponseHeader("X-Enabled-Command-Modes")) + getApp().getMissionManager().setEnabledCommandModes(xmlHttp.getResponseHeader("X-Enabled-Command-Modes")?.split(",") ??[]) + const result = JSON.parse(xmlHttp.responseText); this.#lastUpdateTimes[uri] = callback(result); @@ -137,7 +151,8 @@ export class ServerManager { var xmlHttp = new XMLHttpRequest(); xmlHttp.open("PUT", this.#REST_ADDRESS); xmlHttp.setRequestHeader("Content-Type", "application/json"); - if (this.#username && this.#password) xmlHttp.setRequestHeader("Authorization", "Basic " + btoa(`${this.#username}:${this.#password}`)); + xmlHttp.setRequestHeader("Authorization", "Basic " + btoa(`${this.#username ?? ""}:${this.#password ?? ""}`)); + xmlHttp.setRequestHeader("X-Command-Mode", this.#activeCommandMode); xmlHttp.onload = (res: any) => { var res = JSON.parse(xmlHttp.responseText); callback(res.commandHash); diff --git a/frontend/react/src/sessiondata.ts b/frontend/react/src/sessiondata.ts index c01b3d15..aabb4828 100644 --- a/frontend/react/src/sessiondata.ts +++ b/frontend/react/src/sessiondata.ts @@ -87,10 +87,10 @@ export class SessionDataManager { body: JSON.stringify({ sessionHash }), // Send the data in JSON format }; - fetch(getApp().getExpressAddress() + `/resources/sessiondata/load/${getApp().getProfileName()}`, requestOptions) + fetch(getApp().getExpressAddress() + `/resources/sessiondata/load/${getApp().getServerManager().getUsername()}`, requestOptions) .then((response) => { if (response.status === 200) { - console.log(`Session data for profile ${getApp().getProfileName()} and session hash ${sessionHash} loaded correctly`); + console.log(`Session data for profile ${getApp().getServerManager().getUsername()} and session hash ${sessionHash} loaded correctly`); return response.json(); } else { getApp().addInfoMessage("No session data found for this profile"); @@ -118,10 +118,10 @@ export class SessionDataManager { body: JSON.stringify({ sessionHash: this.#sessionHash, sessionData: this.#sessionData }), // Send the data in JSON format }; - fetch(getApp().getExpressAddress() + `/resources/sessiondata/save/${getApp().getProfileName()}`, requestOptions) + fetch(getApp().getExpressAddress() + `/resources/sessiondata/save/${getApp().getServerManager().getUsername()}`, requestOptions) .then((response) => { if (response.status === 200) { - console.log(`Session data for profile ${getApp().getProfileName()} and session hash ${this.#sessionHash} saved correctly`); + console.log(`Session data for profile ${getApp().getServerManager().getUsername()} and session hash ${this.#sessionHash} saved correctly`); console.log(this.#sessionData); SessionDataChangedEvent.dispatch(this.#sessionData); } else { diff --git a/frontend/react/src/ui/components/olunitlistentry.tsx b/frontend/react/src/ui/components/olunitlistentry.tsx index ce3c63d3..75daaa36 100644 --- a/frontend/react/src/ui/components/olunitlistentry.tsx +++ b/frontend/react/src/ui/components/olunitlistentry.tsx @@ -3,11 +3,31 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { IconProp } from "@fortawesome/fontawesome-svg-core"; import { UnitBlueprint } from "../../interfaces"; import { faArrowRight } from "@fortawesome/free-solid-svg-icons/faArrowRight"; +import { mode } from "../../other/utils"; -export function OlUnitListEntry(props: { icon: IconProp; blueprint: UnitBlueprint; showCost: boolean; cost: number; onClick: () => void }) { - let pillString = "" as string | undefined - if (props.showCost) pillString = `${props.cost} points` - else pillString = !["aircraft", "helicopter"].includes(props.blueprint.category) ? props.blueprint.type : props.blueprint.abilities +export function OlUnitListEntry(props: { + icon?: IconProp; + silhouette?: string; + blueprint: UnitBlueprint; + showCost: boolean; + cost: number; + onClick: () => void; +}) { + let pillString = "" as string | undefined; + if (props.showCost) pillString = `${props.cost} points`; + else { + if (["aircraft", "helicopter"].includes(props.blueprint.category)) { + let roles = props.blueprint.loadouts?.flatMap((loadout) => loadout.roles).filter((role) => role !== "No task"); + if (roles !== undefined) { + let uniqueRoles = roles?.reduce((acc, current) => {if (!acc.includes(current)) {acc.push(current)} return acc}, [] as string[]) + let mainRole = mode(roles); + pillString = uniqueRoles.length > 6 ? "Multirole" : mainRole; + } + } else { + if (props.blueprint.category) + pillString = props.blueprint.type; + } + } return (
- + {props.icon && } + {props.silhouette && ( +
+ +
+ )}
{props.blueprint.label}
{pillString && (
- {appState === OlympusState.SPAWN_CONTEXT && ( - <> -
-
-
- { - spawnCoalition === "blue" && setSpawnCoalition("neutral"); - spawnCoalition === "neutral" && setSpawnCoalition("red"); - spawnCoalition === "red" && setSpawnCoalition("blue"); - }} +
+
+
+ { + spawnCoalition === "blue" && setSpawnCoalition("neutral"); + spawnCoalition === "neutral" && setSpawnCoalition("red"); + spawnCoalition === "red" && setSpawnCoalition("blue"); + }} + /> + (openAccordion !== CategoryGroup.AIRCRAFT ? setOpenAccordion(CategoryGroup.AIRCRAFT) : setOpenAccordion(CategoryGroup.NONE))} + icon={olButtonsVisibilityAircraft} + tooltip="Show aircraft units" + buttonColor={spawnCoalition === "blue" ? "#2563eb" : spawnCoalition === "neutral" ? "#9ca3af" : "#ef4444"} + /> + (openAccordion !== CategoryGroup.HELICOPTER ? setOpenAccordion(CategoryGroup.HELICOPTER) : setOpenAccordion(CategoryGroup.NONE))} + icon={olButtonsVisibilityHelicopter} + tooltip="Show helicopter units" + buttonColor={spawnCoalition === "blue" ? "#2563eb" : spawnCoalition === "neutral" ? "#9ca3af" : "#ef4444"} + /> + (openAccordion !== CategoryGroup.AIR_DEFENCE ? setOpenAccordion(CategoryGroup.AIR_DEFENCE) : setOpenAccordion(CategoryGroup.NONE))} + icon={olButtonsVisibilityGroundunitSam} + tooltip="Show air defence units" + buttonColor={spawnCoalition === "blue" ? "#2563eb" : spawnCoalition === "neutral" ? "#9ca3af" : "#ef4444"} + /> + (openAccordion !== CategoryGroup.GROUND_UNIT ? setOpenAccordion(CategoryGroup.GROUND_UNIT) : setOpenAccordion(CategoryGroup.NONE))} + icon={olButtonsVisibilityGroundunit} + tooltip="Show ground units" + buttonColor={spawnCoalition === "blue" ? "#2563eb" : spawnCoalition === "neutral" ? "#9ca3af" : "#ef4444"} + /> + (openAccordion !== CategoryGroup.NAVY_UNIT ? setOpenAccordion(CategoryGroup.NAVY_UNIT) : setOpenAccordion(CategoryGroup.NONE))} + icon={olButtonsVisibilityNavyunit} + tooltip="Show navy units" + buttonColor={spawnCoalition === "blue" ? "#2563eb" : spawnCoalition === "neutral" ? "#9ca3af" : "#ef4444"} + /> + setShowMore(!showMore)} icon={faEllipsisVertical} tooltip="Show more options" /> + {showMore && ( + <> + (openAccordion !== CategoryGroup.EFFECT ? setOpenAccordion(CategoryGroup.EFFECT) : setOpenAccordion(CategoryGroup.NONE))} + icon={faExplosion} + tooltip="Show effects" + className="ml-auto" /> (openAccordion !== CategoryGroup.AIRCRAFT ? setOpenAccordion(CategoryGroup.AIRCRAFT) : setOpenAccordion(CategoryGroup.NONE))} - icon={olButtonsVisibilityAircraft} - tooltip="Show aircraft units" - buttonColor={spawnCoalition === "blue" ? "#2563eb" : spawnCoalition === "neutral" ? "#9ca3af" : "#ef4444"} + checked={openAccordion === CategoryGroup.SEARCH} + onClick={() => (openAccordion !== CategoryGroup.SEARCH ? setOpenAccordion(CategoryGroup.SEARCH) : setOpenAccordion(CategoryGroup.NONE))} + icon={faSearch} + tooltip="Search unit" /> - openAccordion !== CategoryGroup.HELICOPTER ? setOpenAccordion(CategoryGroup.HELICOPTER) : setOpenAccordion(CategoryGroup.NONE) - } - icon={olButtonsVisibilityHelicopter} - tooltip="Show helicopter units" - buttonColor={spawnCoalition === "blue" ? "#2563eb" : spawnCoalition === "neutral" ? "#9ca3af" : "#ef4444"} + checked={openAccordion === CategoryGroup.STARRED} + onClick={() => (openAccordion !== CategoryGroup.STARRED ? setOpenAccordion(CategoryGroup.STARRED) : setOpenAccordion(CategoryGroup.NONE))} + icon={faStar} + tooltip="Show starred spanws" /> - - openAccordion !== CategoryGroup.AIR_DEFENCE ? setOpenAccordion(CategoryGroup.AIR_DEFENCE) : setOpenAccordion(CategoryGroup.NONE) - } - icon={olButtonsVisibilityGroundunitSam} - tooltip="Show air defence units" - buttonColor={spawnCoalition === "blue" ? "#2563eb" : spawnCoalition === "neutral" ? "#9ca3af" : "#ef4444"} - /> - - openAccordion !== CategoryGroup.GROUND_UNIT ? setOpenAccordion(CategoryGroup.GROUND_UNIT) : setOpenAccordion(CategoryGroup.NONE) - } - icon={olButtonsVisibilityGroundunit} - tooltip="Show ground units" - buttonColor={spawnCoalition === "blue" ? "#2563eb" : spawnCoalition === "neutral" ? "#9ca3af" : "#ef4444"} - /> - (openAccordion !== CategoryGroup.NAVY_UNIT ? setOpenAccordion(CategoryGroup.NAVY_UNIT) : setOpenAccordion(CategoryGroup.NONE))} - icon={olButtonsVisibilityNavyunit} - tooltip="Show navy units" - buttonColor={spawnCoalition === "blue" ? "#2563eb" : spawnCoalition === "neutral" ? "#9ca3af" : "#ef4444"} - /> - setShowMore(!showMore)} icon={faEllipsisVertical} tooltip="Show more options" /> - {showMore && ( - <> - (openAccordion !== CategoryGroup.EFFECT ? setOpenAccordion(CategoryGroup.EFFECT) : setOpenAccordion(CategoryGroup.NONE))} - icon={faExplosion} - tooltip="Show effects" - className="ml-auto" - /> - (openAccordion !== CategoryGroup.SEARCH ? setOpenAccordion(CategoryGroup.SEARCH) : setOpenAccordion(CategoryGroup.NONE))} - icon={faSearch} - tooltip="Search unit" - /> - (openAccordion !== CategoryGroup.STARRED ? setOpenAccordion(CategoryGroup.STARRED) : setOpenAccordion(CategoryGroup.NONE))} - icon={faStar} - tooltip="Show starred spanws" - /> - - )} -
- {blueprint === null && effect === null && openAccordion !== CategoryGroup.NONE && ( -
- <> - <> - {openAccordion === CategoryGroup.AIRCRAFT && ( - <> -
- {roles.aircraft.sort().map((role) => { - return ( -
{ - selectedRole === role ? setSelectedRole(null) : setSelectedRole(role); - }} - > - {role} -
- ); - })} -
-
- {blueprints - ?.sort((a, b) => (a.label > b.label ? 1 : -1)) - .filter((blueprint) => blueprint.category === "aircraft") - .map((blueprint) => { - return ( - setBlueprint(blueprint)} - showCost={showCost} - cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)} - /> - ); - })} -
- - )} - {openAccordion === CategoryGroup.HELICOPTER && ( - <> -
- {roles.helicopter.sort().map((role) => { - return ( -
{ - selectedRole === role ? setSelectedRole(null) : setSelectedRole(role); - }} - > - {role} -
- ); - })} -
-
- {blueprints - ?.sort((a, b) => (a.label > b.label ? 1 : -1)) - .filter((blueprint) => blueprint.category === "helicopter") - .map((blueprint) => { - return ( - setBlueprint(blueprint)} - showCost={showCost} - cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)} - /> - ); - })} -
- - )} - {openAccordion === CategoryGroup.AIR_DEFENCE && ( - <> -
- {types.groundunit - .sort() - ?.filter((type) => type === "SAM Site" || type === "AAA") - .map((type) => { - return ( -
{ - selectedType === type ? setSelectedType(null) : setSelectedType(type); - }} - > - {type} -
- ); - })} -
-
- {blueprints - ?.sort((a, b) => (a.label > b.label ? 1 : -1)) - .filter((blueprint) => blueprint.category === "groundunit" && (blueprint.type === "SAM Site" || blueprint.type === "AAA")) - .map((blueprint) => { - return ( - setBlueprint(blueprint)} - showCost={showCost} - cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)} - /> - ); - })} -
- - )} - {openAccordion === CategoryGroup.GROUND_UNIT && ( - <> -
- {types.groundunit - .sort() - ?.filter((type) => type !== "SAM Site" && type !== "AAA") - .map((type) => { - return ( -
{ - selectedType === type ? setSelectedType(null) : setSelectedType(type); - }} - > - {type} -
- ); - })} -
-
- {blueprints - ?.sort((a, b) => (a.label > b.label ? 1 : -1)) - .filter((blueprint) => blueprint.category === "groundunit" && blueprint.type !== "SAM Site" && blueprint.type !== "AAA") - .map((blueprint) => { - return ( - setBlueprint(blueprint)} - showCost={showCost} - cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)} - /> - ); - })} -
- - )} - {openAccordion === CategoryGroup.NAVY_UNIT && ( - <> -
- {types.navyunit.sort().map((type) => { - return ( -
{ - selectedType === type ? setSelectedType(null) : setSelectedType(type); - }} - > - {type} -
- ); - })} -
-
- {blueprints - ?.sort((a, b) => (a.label > b.label ? 1 : -1)) - .filter((blueprint) => blueprint.category === "navyunit") - .map((blueprint) => { - return ( - setBlueprint(blueprint)} - showCost={showCost} - cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)} - /> - ); - })} -
- - )} - {openAccordion === CategoryGroup.EFFECT && ( - <> -
- { - setEffect("explosion"); - }} - /> - { - setEffect("smoke"); - }} - /> -
- - )} - {openAccordion === CategoryGroup.SEARCH && ( -
- setFilterString(value)} text={filterString} /> -
- {filteredBlueprints.length > 0 ? ( - filteredBlueprints.map((blueprint) => { - return ( - setBlueprint(blueprint)} - showCost={showCost} - cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)} - /> - ); - }) - ) : filterString === "" ? ( - Type to search - ) : ( - No results - )} -
-
- )} - {openAccordion === CategoryGroup.STARRED && ( -
- {Object.values(starredSpawns).length > 0 ? ( - Object.values(starredSpawns).map((spawnRequestTable) => { - return ( - { - if (latlng) { - spawnRequestTable.unit.location = latlng; - getApp() - .getUnitsManager() - .spawnUnits(spawnRequestTable.category, Array(spawnRequestTable.amount).fill(spawnRequestTable.unit), spawnRequestTable.coalition, false); - getApp().setState(OlympusState.IDLE); - } - }} - > - -
- {getApp().getUnitsManager().getDatabase().getByName(spawnRequestTable.unit.unitType)?.label} ( - {spawnRequestTable.quickAccessName}) -
-
- ); - }) - ) : ( -
No starred spawns, use the spawn menu to create a quick access spawn
- )} -
- )} - - -
- )} - {!(blueprint === null) && setBlueprint(null)}/>} - {!(effect === null) && latlng && setEffect(null)} />} -
+ + )}
- - )} + {blueprint === null && effect === null && openAccordion !== CategoryGroup.NONE && ( +
+ <> + <> + {openAccordion === CategoryGroup.AIRCRAFT && ( + <> +
+ {roles.aircraft.sort().map((role) => { + return ( +
{ + selectedRole === role ? setSelectedRole(null) : setSelectedRole(role); + }} + > + {role} +
+ ); + })} +
+
+ {blueprints + ?.sort((a, b) => (a.label > b.label ? 1 : -1)) + .filter((blueprint) => blueprint.category === "aircraft") + .map((blueprint) => { + return ( + setBlueprint(blueprint)} + showCost={showCost} + cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)} + /> + ); + })} +
+ + )} + {openAccordion === CategoryGroup.HELICOPTER && ( + <> +
+ {roles.helicopter.sort().map((role) => { + return ( +
{ + selectedRole === role ? setSelectedRole(null) : setSelectedRole(role); + }} + > + {role} +
+ ); + })} +
+
+ {blueprints + ?.sort((a, b) => (a.label > b.label ? 1 : -1)) + .filter((blueprint) => blueprint.category === "helicopter") + .map((blueprint) => { + return ( + setBlueprint(blueprint)} + showCost={showCost} + cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)} + /> + ); + })} +
+ + )} + {openAccordion === CategoryGroup.AIR_DEFENCE && ( + <> +
+ {types.groundunit + .sort() + ?.filter((type) => type === "SAM Site" || type === "AAA") + .map((type) => { + return ( +
{ + selectedType === type ? setSelectedType(null) : setSelectedType(type); + }} + > + {type} +
+ ); + })} +
+
+ {blueprints + ?.sort((a, b) => (a.label > b.label ? 1 : -1)) + .filter((blueprint) => blueprint.category === "groundunit" && (blueprint.type === "SAM Site" || blueprint.type === "AAA")) + .map((blueprint) => { + return ( + setBlueprint(blueprint)} + showCost={showCost} + cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)} + /> + ); + })} +
+ + )} + {openAccordion === CategoryGroup.GROUND_UNIT && ( + <> +
+ {types.groundunit + .sort() + ?.filter((type) => type !== "SAM Site" && type !== "AAA") + .map((type) => { + return ( +
{ + selectedType === type ? setSelectedType(null) : setSelectedType(type); + }} + > + {type} +
+ ); + })} +
+
+ {blueprints + ?.sort((a, b) => (a.label > b.label ? 1 : -1)) + .filter((blueprint) => blueprint.category === "groundunit" && blueprint.type !== "SAM Site" && blueprint.type !== "AAA") + .map((blueprint) => { + return ( + setBlueprint(blueprint)} + showCost={showCost} + cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)} + /> + ); + })} +
+ + )} + {openAccordion === CategoryGroup.NAVY_UNIT && ( + <> +
+ {types.navyunit.sort().map((type) => { + return ( +
{ + selectedType === type ? setSelectedType(null) : setSelectedType(type); + }} + > + {type} +
+ ); + })} +
+
+ {blueprints + ?.sort((a, b) => (a.label > b.label ? 1 : -1)) + .filter((blueprint) => blueprint.category === "navyunit") + .map((blueprint) => { + return ( + setBlueprint(blueprint)} + showCost={showCost} + cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)} + /> + ); + })} +
+ + )} + {openAccordion === CategoryGroup.EFFECT && ( + <> +
+ { + setEffect("explosion"); + }} + /> + { + setEffect("smoke"); + }} + /> +
+ + )} + {openAccordion === CategoryGroup.SEARCH && ( +
+ setFilterString(value)} text={filterString} /> +
+ {filteredBlueprints.length > 0 ? ( + filteredBlueprints.map((blueprint) => { + return ( + setBlueprint(blueprint)} + showCost={showCost} + cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)} + /> + ); + }) + ) : filterString === "" ? ( + Type to search + ) : ( + No results + )} +
+
+ )} + {openAccordion === CategoryGroup.STARRED && ( +
+ {Object.values(starredSpawns).length > 0 ? ( + Object.values(starredSpawns).map((spawnRequestTable) => { + return ( + { + if (latlng) { + spawnRequestTable.unit.location = latlng; + getApp() + .getUnitsManager() + .spawnUnits( + spawnRequestTable.category, + Array(spawnRequestTable.amount).fill(spawnRequestTable.unit), + spawnRequestTable.coalition, + false + ); + getApp().setState(OlympusState.IDLE); + } + }} + > + +
+ {getApp().getUnitsManager().getDatabase().getByName(spawnRequestTable.unit.unitType)?.label} ( + {spawnRequestTable.quickAccessName}) +
+
+ ); + }) + ) : ( +
No starred spawns, use the spawn menu to create a quick access spawn
+ )} +
+ )} + + +
+ )} + setBlueprint(null)} + /> + {!(effect === null) && latlng && setEffect(null)} />} +
+
); } diff --git a/frontend/react/src/ui/modals/loginmodal.tsx b/frontend/react/src/ui/modals/loginmodal.tsx index c7d0aa8c..b1b1ff21 100644 --- a/frontend/react/src/ui/modals/loginmodal.tsx +++ b/frontend/react/src/ui/modals/loginmodal.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import { Modal } from "./components/modal"; import { Card } from "./components/card"; import { ErrorCallout } from "../components/olcallout"; @@ -6,33 +6,49 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faArrowRight, faCheckCircle, faExternalLink } from "@fortawesome/free-solid-svg-icons"; import { getApp, VERSION } from "../../olympusapp"; import { sha256 } from "js-sha256"; -import { BLUE_COMMANDER, GAME_MASTER, OlympusState, RED_COMMANDER } from "../../constants/constants"; +import { BLUE_COMMANDER, GAME_MASTER, LoginSubState, NO_SUBSTATE, OlympusState, RED_COMMANDER } from "../../constants/constants"; +import { OlDropdown, OlDropdownItem } from "../components/oldropdown"; +import { AppStateChangedEvent } from "../../events"; + +var hash = sha256.create(); export function LoginModal(props: { open: boolean }) { // TODO: add warning if not in secure context and some features are disabled + const [subState, setSubState] = useState(NO_SUBSTATE); const [password, setPassword] = useState(""); - const [profileName, setProfileName] = useState(""); + const [username, setUsername] = useState(""); const [checkingPassword, setCheckingPassword] = useState(false); const [loginError, setLoginError] = useState(false); - const [commandMode, setCommandMode] = useState(null as null | string); + const [commandModes, setCommandModes] = useState(null as null | string[]); + const [activeCommandMode, setActiveCommandMode] = useState(null as null | string); useEffect(() => { - /* Set the profile name */ - if (profileName !== "") getApp().setProfile(profileName); - }, [profileName]); + AppStateChangedEvent.on((state, subState) => { + setSubState(subState); + }); + }, []); - function checkPassword(password: string) { + const usernameCallback = useCallback(() => getApp()?.getServerManager().setUsername(username), [username]); + useEffect(usernameCallback, [username]); + + const passwordCallback = useCallback(() => getApp()?.getServerManager().setPassword(hash.update(password).hex()), [password]); + useEffect(passwordCallback, [password]); + + const login = useCallback(() => { setCheckingPassword(true); - var hash = sha256.create(); - getApp().getServerManager().setPassword(hash.update(password).hex()); + getApp() .getServerManager() .getMission( (response) => { - const commandMode = response.mission.commandModeOptions.commandMode; - try { - [GAME_MASTER, BLUE_COMMANDER, RED_COMMANDER].includes(commandMode) ? setCommandMode(commandMode) : setLoginError(true); - } catch { + const commandModes = getApp().getMissionManager().getEnabledCommandModes(); + if (commandModes.length > 1) { + setCommandModes(commandModes); + setActiveCommandMode(commandModes[0]); + } else if (commandModes.length == 1) { + setActiveCommandMode(commandModes[0]); + getApp().setState(OlympusState.LOGIN, LoginSubState.CONNECT); + } else { setLoginError(true); } setCheckingPassword(false); @@ -42,34 +58,44 @@ export function LoginModal(props: { open: boolean }) { setCheckingPassword(false); } ); - } + }, [commandModes, username, password]); - function connect() { - getApp().getServerManager().setUsername(profileName); - getApp().getServerManager().startUpdate(); - getApp().setState(OlympusState.IDLE); + const connect = useCallback(() => { + if (activeCommandMode) { + getApp().getServerManager().setActiveCommandMode(activeCommandMode); + getApp().getServerManager().startUpdate(); + getApp().setState(OlympusState.IDLE); - /* If no profile exists already with that name, create it from scratch from the defaults */ - if (getApp().getProfile() === null) getApp().saveProfile(); - /* Load the profile */ - getApp().loadProfile(); - } + /* If no profile exists already with that name, create it from scratch from the defaults */ + if (getApp().getProfile() === null) getApp().saveProfile(); + /* Load the profile */ + getApp().loadProfile(); + } + }, [activeCommandMode]); + + const subStateCallback = useCallback(() => { + if (subState === LoginSubState.COMMAND_MODE) { + login(); + } else if (subState === LoginSubState.CONNECT) { + connect(); + } + }, [subState, activeCommandMode, commandModes, username, password]); + useEffect(subStateCallback, [subState]); return ( - +
- +

{!loginError ? ( <> - {commandMode === null ? ( + {subState === LoginSubState.CREDENTIALS && ( <>
+ + setUsername(ev.currentTarget.value)} + className={` + block w-full max-w-80 rounded-lg border + border-gray-300 bg-gray-50 p-2.5 text-sm + text-gray-900 + dark:border-gray-600 dark:bg-gray-700 + dark:text-white dark:placeholder-gray-400 + dark:focus:border-blue-500 + dark:focus:ring-blue-500 + focus:border-blue-500 focus:ring-blue-500 + `} + placeholder="Enter display name" + value={username} + required + /> +
- ) : ( + )} + {subState === LoginSubState.COMMAND_MODE && ( <>
- setProfileName(ev.currentTarget.value)} - className={` - block w-full max-w-80 rounded-lg border - border-gray-300 bg-gray-50 p-2.5 text-sm - text-gray-900 - dark:border-gray-600 dark:bg-gray-700 - dark:text-white dark:placeholder-gray-400 - dark:focus:border-blue-500 - dark:focus:ring-blue-500 - focus:border-blue-500 focus:ring-blue-500 - `} - placeholder="Enter display name" - value={profileName} - required - /> + + {commandModes?.map((commandMode) => { + return setActiveCommandMode(commandMode)}>{commandMode}; + })} +
-
The profile name you choose determines the saved key binds, groups and options you see.
-
)} diff --git a/frontend/react/src/ui/panels/compactunitspawnmenu.tsx b/frontend/react/src/ui/panels/compactunitspawnmenu.tsx deleted file mode 100644 index 522c1039..00000000 --- a/frontend/react/src/ui/panels/compactunitspawnmenu.tsx +++ /dev/null @@ -1,445 +0,0 @@ -import React, { useState, useEffect, useCallback } from "react"; -import { OlUnitSummary } from "../components/olunitsummary"; -import { OlCoalitionToggle } from "../components/olcoalitiontoggle"; -import { OlNumberInput } from "../components/olnumberinput"; -import { OlLabelToggle } from "../components/ollabeltoggle"; -import { OlRangeSlider } from "../components/olrangeslider"; -import { OlDropdownItem, OlDropdown } from "../components/oldropdown"; -import { LoadoutBlueprint, SpawnRequestTable, UnitBlueprint } from "../../interfaces"; -import { OlStateButton } from "../components/olstatebutton"; -import { Coalition } from "../../types/types"; -import { getApp } from "../../olympusapp"; -import { ftToM, hash } from "../../other/utils"; -import { LatLng } from "leaflet"; -import { Airbase } from "../../mission/airbase"; -import { altitudeIncrements, groupUnitCount, maxAltitudeValues, minAltitudeValues, OlympusState, SpawnSubState } from "../../constants/constants"; -import { faArrowLeft, faStar } from "@fortawesome/free-solid-svg-icons"; -import { OlStringInput } from "../components/olstringinput"; -import { countryCodes } from "../data/codes"; -import { OlAccordion } from "../components/olaccordion"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; - -export function CompactUnitSpawnMenu(props: { - starredSpawns: { [key: string]: SpawnRequestTable }; - blueprint: UnitBlueprint; - onBack: () => void; - latlng?: LatLng | null; - airbase?: Airbase | null; - coalition?: Coalition; -}) { - /* Compute the min and max values depending on the unit type */ - const minNumber = 1; - const maxNumber = groupUnitCount[props.blueprint.category]; - const minAltitude = minAltitudeValues[props.blueprint.category]; - const maxAltitude = maxAltitudeValues[props.blueprint.category]; - const altitudeStep = altitudeIncrements[props.blueprint.category]; - - /* State initialization */ - const [spawnCoalition, setSpawnCoalition] = useState("blue" as Coalition); - const [spawnNumber, setSpawnNumber] = useState(1); - const [spawnRole, setSpawnRole] = useState(""); - const [spawnLoadoutName, setSpawnLoadout] = useState(""); - const [spawnAltitude, setSpawnAltitude] = useState((maxAltitude - minAltitude) / 2); - const [spawnAltitudeType, setSpawnAltitudeType] = useState(false); - const [spawnLiveryID, setSpawnLiveryID] = useState(""); - const [spawnSkill, setSpawnSkill] = useState("High"); - - const [showAdvancedOptions, setShowAdvancedOptions] = useState(false); - const [showLoadout, setShowLoadout] = useState(false); - const [showUnitSummary, setShowUnitSummary] = useState(false); - - const [quickAccessName, setQuickAccessName] = useState("No name"); - const [key, setKey] = useState(""); - const [spawnRequestTable, setSpawnRequestTable] = useState(null as null | SpawnRequestTable); - - /* When the menu is opened show the unit preview on the map as a cursor */ - useEffect(() => { - if (!props.airbase && !props.latlng && spawnRequestTable) { - /* Refresh the unique key identified */ - const newKey = hash(JSON.stringify(spawnRequestTable)); - setKey(newKey); - - getApp()?.getMap()?.setSpawnRequestTable(spawnRequestTable); - getApp().setState(OlympusState.SPAWN, SpawnSubState.SPAWN_UNIT); - } - }, [spawnRequestTable]); - - /* Callback and effect to update the quick access name of the starredSpawn */ - const updateStarredSpawnQuickAccessNameS = useCallback(() => { - if (key in props.starredSpawns) props.starredSpawns[key].quickAccessName = quickAccessName; - }, [props.starredSpawns, key, quickAccessName]); - useEffect(updateStarredSpawnQuickAccessNameS, [quickAccessName]); - - /* Callback and effect to update the quick access name in the input field */ - const updateQuickAccessName = useCallback(() => { - /* If the spawn is starred, set the quick access name */ - if (key in props.starredSpawns && props.starredSpawns[key].quickAccessName) setQuickAccessName(props.starredSpawns[key].quickAccessName); - else setQuickAccessName("No name"); - }, [props.starredSpawns, key]); - useEffect(updateQuickAccessName, [key]); - - /* Callback and effect to update the spawn request table */ - const updateSpawnRequestTable = useCallback(() => { - if (props.blueprint !== null) { - setSpawnRequestTable({ - category: props.blueprint.category, - unit: { - unitType: props.blueprint.name, - location: props.latlng ?? new LatLng(0, 0), // This will be filled when the user clicks on the map to spawn the unit - skill: spawnSkill, - liveryID: spawnLiveryID, - altitude: ftToM(spawnAltitude), - loadout: props.blueprint.loadouts?.find((loadout) => loadout.name === spawnLoadoutName)?.code ?? "", - }, - amount: spawnNumber, - coalition: spawnCoalition, - }); - } - }, [props.blueprint, spawnAltitude, spawnLoadoutName, spawnCoalition, spawnNumber, spawnLiveryID, spawnSkill]); - useEffect(updateSpawnRequestTable, [props.blueprint, spawnAltitude, spawnLoadoutName, spawnCoalition, spawnNumber, spawnLiveryID, spawnSkill]); - - /* Effect to update the coalition if it is force externally */ - useEffect(() => { - if (props.coalition) setSpawnCoalition(props.coalition); - }, [props.coalition]); - - /* Get a list of all the roles */ - const roles: string[] = []; - (props.blueprint as UnitBlueprint).loadouts?.forEach((loadout) => { - loadout.roles.forEach((role) => { - !roles.includes(role) && roles.push(role); - }); - }); - - /* Initialize the role */ - spawnRole === "" && roles.length > 0 && setSpawnRole(roles[0]); - - /* Get a list of all the loadouts */ - const loadouts: LoadoutBlueprint[] = []; - (props.blueprint as UnitBlueprint).loadouts?.forEach((loadout) => { - loadout.roles.includes(spawnRole) && loadouts.push(loadout); - }); - - /* Initialize the loadout */ - spawnLoadoutName === "" && loadouts.length > 0 && setSpawnLoadout(loadouts[0].name); - const spawnLoadout = props.blueprint.loadouts?.find((loadout) => { - return loadout.name === spawnLoadoutName; - }); - - return ( -
-
-
- -
{props.blueprint.label}
- { - setSpawnNumber(Math.max(minNumber, spawnNumber - 1)); - }} - onIncrease={() => { - setSpawnNumber(Math.min(maxNumber, spawnNumber + 1)); - }} - onChange={(ev) => { - !isNaN(Number(ev.target.value)) && setSpawnNumber(Math.max(minNumber, Math.min(maxNumber, Number(ev.target.value)))); - }} - /> -
-
-
Quick access:
- { - setQuickAccessName(e.target.value); - }} - value={quickAccessName} - /> - { - if (spawnRequestTable) - key in props.starredSpawns - ? getApp().getMap().removeStarredSpawnRequestTable(key) - : getApp().getMap().addStarredSpawnRequestTable(key, spawnRequestTable); - }} - tooltip="Save this spawn for quick access" - checked={key in props.starredSpawns} - icon={faStar} - > -
- {["aircraft", "helicopter"].includes(props.blueprint.category) && ( - <> - {!props.airbase && ( -
-
-
- - Altitude - - {`${Intl.NumberFormat("en-US").format(spawnAltitude)} FT`} -
- setSpawnAltitudeType(!spawnAltitudeType)} /> -
- setSpawnAltitude(Number(ev.target.value))} - value={spawnAltitude} - min={minAltitude} - max={maxAltitude} - step={altitudeStep} - /> -
- )} -
- - Role - - - {roles.map((role) => { - return ( - { - setSpawnRole(role); - setSpawnLoadout(""); - }} - className={`w-full`} - > - {role} - - ); - })} - -
-
- - Weapons - - - {loadouts.map((loadout) => { - return ( - { - setSpawnLoadout(loadout.name); - }} - className={`w-full`} - > - - {loadout.name} - - - ); - })} - -
- - )} - { - setShowAdvancedOptions(!showAdvancedOptions); - }} - open={showAdvancedOptions} - title="Advanced options" - > -
-
- - Livery - - - {props.blueprint.liveries && - Object.keys(props.blueprint.liveries) - .sort((ida, idb) => { - if (props.blueprint.liveries) { - if (props.blueprint.liveries[ida].countries.length > 1) return 1; - return props.blueprint.liveries[ida].countries[0] > props.blueprint.liveries[idb].countries[0] ? 1 : -1; - } else return -1; - }) - .map((id) => { - let country = Object.values(countryCodes).find((countryCode) => { - if (props.blueprint.liveries && countryCode.liveryCodes?.includes(props.blueprint.liveries[id].countries[0])) return true; - }); - return ( - { - setSpawnLiveryID(id); - }} - className={`w-full`} - > - - {props.blueprint.liveries && props.blueprint.liveries[id].countries.length == 1 && ( - - )} - -
- - {props.blueprint.liveries ? props.blueprint.liveries[id].name : ""} - -
-
-
- ); - })} -
-
-
- - Skill - - - {["Average", "Good", "High", "Excellent"].map((skill) => { - return ( - { - setSpawnSkill(skill); - }} - className={`w-full`} - > - -
{skill}
-
-
- ); - })} -
-
-
-
-
- { - setShowUnitSummary(!showUnitSummary); - }} - open={showUnitSummary} - title="Unit summary" - > - - - {spawnLoadout && spawnLoadout.items.length > 0 && ( - { - setShowLoadout(!showLoadout); - }} - open={showLoadout} - title="Loadout" - > - {spawnLoadout.items.map((item) => { - return ( -
-
- {item.quantity} -
-
- {item.name} -
-
- ); - })} -
- )} - {(props.latlng || props.airbase) && ( - - )} -
- ); -} diff --git a/frontend/react/src/ui/panels/spawnmenu.tsx b/frontend/react/src/ui/panels/spawnmenu.tsx index 54ffd5bd..7c166b3e 100644 --- a/frontend/react/src/ui/panels/spawnmenu.tsx +++ b/frontend/react/src/ui/panels/spawnmenu.tsx @@ -166,7 +166,7 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children? return ( setBlueprint(blueprint)} showCost={showCost} @@ -433,13 +433,14 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children?

)} - {!(blueprint === null) && ( - - )} + + {!(effect === null) && } diff --git a/frontend/react/src/ui/panels/unitspawnmenu.tsx b/frontend/react/src/ui/panels/unitspawnmenu.tsx index 960c017e..6516349a 100644 --- a/frontend/react/src/ui/panels/unitspawnmenu.tsx +++ b/frontend/react/src/ui/panels/unitspawnmenu.tsx @@ -9,69 +9,84 @@ import { LoadoutBlueprint, SpawnRequestTable, UnitBlueprint } from "../../interf import { OlStateButton } from "../components/olstatebutton"; import { Coalition } from "../../types/types"; import { getApp } from "../../olympusapp"; -import { ftToM, hash } from "../../other/utils"; +import { ftToM, hash, mode } from "../../other/utils"; import { LatLng } from "leaflet"; import { Airbase } from "../../mission/airbase"; import { altitudeIncrements, groupUnitCount, maxAltitudeValues, minAltitudeValues, OlympusState, SpawnSubState } from "../../constants/constants"; -import { faStar } from "@fortawesome/free-solid-svg-icons"; +import { faArrowLeft, faStar } from "@fortawesome/free-solid-svg-icons"; import { OlStringInput } from "../components/olstringinput"; import { countryCodes } from "../data/codes"; import { OlAccordion } from "../components/olaccordion"; import { AppStateChangedEvent } from "../../events"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; export function UnitSpawnMenu(props: { + visible: boolean; + compact: boolean; starredSpawns: { [key: string]: SpawnRequestTable }; - blueprint: UnitBlueprint; + blueprint: UnitBlueprint | null; airbase?: Airbase | null; + latlng?: LatLng | null; coalition?: Coalition; + onBack?: () => void; }) { /* Compute the min and max values depending on the unit type */ const minNumber = 1; - const maxNumber = groupUnitCount[props.blueprint.category]; - const minAltitude = minAltitudeValues[props.blueprint.category]; - const maxAltitude = maxAltitudeValues[props.blueprint.category]; - const altitudeStep = altitudeIncrements[props.blueprint.category]; + const maxNumber = groupUnitCount[props.blueprint?.category ?? "aircraft"]; + const minAltitude = minAltitudeValues[props.blueprint?.category ?? "aircraft"]; + const maxAltitude = maxAltitudeValues[props.blueprint?.category ?? "aircraft"]; + const altitudeStep = altitudeIncrements[props.blueprint?.category ?? "aircraft"]; /* State initialization */ const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED); const [spawnCoalition, setSpawnCoalition] = useState("blue" as Coalition); const [spawnNumber, setSpawnNumber] = useState(1); const [spawnRole, setSpawnRole] = useState(""); - const [spawnLoadoutName, setSpawnLoadout] = useState(""); + const [spawnLoadoutName, setSpawnLoadoutName] = useState(""); const [spawnAltitude, setSpawnAltitude] = useState((maxAltitude - minAltitude) / 2); const [spawnAltitudeType, setSpawnAltitudeType] = useState(false); const [spawnLiveryID, setSpawnLiveryID] = useState(""); const [spawnSkill, setSpawnSkill] = useState("High"); const [showLoadout, setShowLoadout] = useState(false); - + const [showAdvancedOptions, setShowAdvancedOptions] = useState(false); + const [showUnitSummary, setShowUnitSummary] = useState(false); const [quickAccessName, setQuickAccessName] = useState("No name"); const [key, setKey] = useState(""); const [spawnRequestTable, setSpawnRequestTable] = useState(null as null | SpawnRequestTable); useEffect(() => { - setAppState(getApp()?.getState()) + setAppState(getApp()?.getState()); AppStateChangedEvent.on((state, subState) => setAppState(state)); }, []); + useEffect(() => { + setSpawnRole(""); + setSpawnLoadoutName(""); + setSpawnLiveryID(""); + }, [props.blueprint]); + /* When the menu is opened show the unit preview on the map as a cursor */ const setSpawnRequestTableCallback = useCallback(() => { - if (!props.airbase && spawnRequestTable && appState === OlympusState.SPAWN) { + if (spawnRequestTable) { /* Refresh the unique key identified */ + const tempTable = {...spawnRequestTable} as any; + delete tempTable.quickAccessName; + delete tempTable.unit.location; + delete tempTable.unit.altitude; const newKey = hash(JSON.stringify(spawnRequestTable)); setKey(newKey); getApp()?.getMap()?.setSpawnRequestTable(spawnRequestTable); - getApp().setState(OlympusState.SPAWN, SpawnSubState.SPAWN_UNIT); + if (!props.airbase && !props.latlng && appState === OlympusState.SPAWN) getApp().setState(OlympusState.SPAWN, SpawnSubState.SPAWN_UNIT); } }, [spawnRequestTable, appState]); - useEffect(setSpawnRequestTableCallback, [spawnRequestTable]); /* Callback and effect to update the quick access name of the starredSpawn */ - const updateStarredSpawnQuickAccessNameS = useCallback(() => { + const updateStarredSpawnQuickAccessName = useCallback(() => { if (key in props.starredSpawns) props.starredSpawns[key].quickAccessName = quickAccessName; }, [props.starredSpawns, key, quickAccessName]); - useEffect(updateStarredSpawnQuickAccessNameS, [quickAccessName]); + useEffect(updateStarredSpawnQuickAccessName, [quickAccessName]); /* Callback and effect to update the quick access name in the input field */ const updateQuickAccessName = useCallback(() => { @@ -87,365 +102,725 @@ export function UnitSpawnMenu(props: { const updateSpawnRequestTable = useCallback(() => { if (props.blueprint !== null) { setSpawnRequestTable({ - category: props.blueprint.category, + category: props.blueprint?.category, unit: { - unitType: props.blueprint.name, - location: new LatLng(0, 0), // This will be filled when the user clicks on the map to spawn the unit + unitType: props.blueprint?.name, + location: props.latlng ?? new LatLng(0, 0), // This will be filled when the user clicks on the map to spawn the unit skill: spawnSkill, liveryID: spawnLiveryID, altitude: ftToM(spawnAltitude), - loadout: props.blueprint.loadouts?.find((loadout) => loadout.name === spawnLoadoutName)?.code ?? "", + loadout: props.blueprint?.loadouts?.find((loadout) => loadout.name === spawnLoadoutName)?.code ?? "", }, amount: spawnNumber, coalition: spawnCoalition, }); } - }, [props.blueprint, spawnAltitude, spawnLoadoutName, spawnCoalition, spawnNumber, spawnLiveryID, spawnSkill]); - useEffect(updateSpawnRequestTable, [props.blueprint, spawnAltitude, spawnLoadoutName, spawnCoalition, spawnNumber, spawnLiveryID, spawnSkill]); + }, [props.blueprint, props.latlng, spawnAltitude, spawnLoadoutName, spawnCoalition, spawnNumber, spawnLiveryID, spawnSkill]); + useEffect(updateSpawnRequestTable, [props.blueprint, props.latlng, spawnAltitude, spawnLoadoutName, spawnCoalition, spawnNumber, spawnLiveryID, spawnSkill]); - /* Effect to update the coalition if it is force externally */ + /* Effect to update the coalition if it is forced externally */ useEffect(() => { if (props.coalition) setSpawnCoalition(props.coalition); }, [props.coalition]); /* Get a list of all the roles */ const roles: string[] = []; - (props.blueprint as UnitBlueprint).loadouts?.forEach((loadout) => { + props.blueprint?.loadouts?.forEach((loadout) => { loadout.roles.forEach((role) => { !roles.includes(role) && roles.push(role); }); }); /* Initialize the role */ - spawnRole === "" && roles.length > 0 && setSpawnRole(roles[0]); + let allRoles = props.blueprint?.loadouts?.flatMap((loadout) => loadout.roles).filter((role) => role !== "No task"); + let mainRole = roles[0]; + if (allRoles !== undefined) mainRole = mode(allRoles); + spawnRole === "" && roles.length > 0 && setSpawnRole(mainRole); /* Get a list of all the loadouts */ const loadouts: LoadoutBlueprint[] = []; - (props.blueprint as UnitBlueprint).loadouts?.forEach((loadout) => { + props.blueprint?.loadouts?.forEach((loadout) => { loadout.roles.includes(spawnRole) && loadouts.push(loadout); }); /* Initialize the loadout */ - spawnLoadoutName === "" && loadouts.length > 0 && setSpawnLoadout(loadouts[0].name); - const spawnLoadout = props.blueprint.loadouts?.find((loadout) => { + spawnLoadoutName === "" && loadouts.length > 0 && setSpawnLoadoutName(loadouts[0].name); + const spawnLoadout = props.blueprint?.loadouts?.find((loadout) => { return loadout.name === spawnLoadoutName; }); return ( -
- -
-
-
Quick access:
- { - setQuickAccessName(e.target.value); - }} - value={quickAccessName} - /> - { - if (spawnRequestTable) - key in props.starredSpawns - ? getApp().getMap().removeStarredSpawnRequestTable(key) - : getApp().getMap().addStarredSpawnRequestTable(key, spawnRequestTable); - }} - tooltip="Save this spawn for quick access" - checked={key in props.starredSpawns} - icon={faStar} - > -
-
- {!props.coalition && ( - { - spawnCoalition === "blue" && setSpawnCoalition("neutral"); - spawnCoalition === "neutral" && setSpawnCoalition("red"); - spawnCoalition === "red" && setSpawnCoalition("blue"); - }} - /> - )} - { - setSpawnNumber(Math.max(minNumber, spawnNumber - 1)); - }} - onIncrease={() => { - setSpawnNumber(Math.min(maxNumber, spawnNumber + 1)); - }} - onChange={(ev) => { - !isNaN(Number(ev.target.value)) && setSpawnNumber(Math.max(minNumber, Math.min(maxNumber, Number(ev.target.value)))); - }} - /> -
- - {["aircraft", "helicopter"].includes(props.blueprint.category) && ( - <> - {!props.airbase && ( -
+ <> + {props.compact ? ( + <> + {props.visible && ( +
+
+
+ +
{props.blueprint?.label}
+ { + setSpawnNumber(Math.max(minNumber, spawnNumber - 1)); + }} + onIncrease={() => { + setSpawnNumber(Math.min(maxNumber, spawnNumber + 1)); + }} + onChange={(ev) => { + !isNaN(Number(ev.target.value)) && setSpawnNumber(Math.max(minNumber, Math.min(maxNumber, Number(ev.target.value)))); + }} + /> +
-
- - Altitude - - {`${Intl.NumberFormat("en-US").format(spawnAltitude)} FT`} -
- setSpawnAltitudeType(!spawnAltitudeType)} /> +
Quick access:
+ { + setQuickAccessName(e.target.value); + }} + value={quickAccessName} + /> + { + if (spawnRequestTable) + if (key in props.starredSpawns) getApp().getMap().removeStarredSpawnRequestTable(key); + else getApp().getMap().addStarredSpawnRequestTable(key, spawnRequestTable, quickAccessName); + }} + tooltip="Save this spawn for quick access" + checked={key in props.starredSpawns} + icon={faStar} + >
- setSpawnAltitude(Number(ev.target.value))} - value={spawnAltitude} - min={minAltitude} - max={maxAltitude} - step={altitudeStep} - /> -
- )} -
- - Role - - - {roles.map((role) => { - return ( - { - setSpawnRole(role); - setSpawnLoadout(""); - }} - className={`w-full`} - > - {role} - - ); - })} - -
-
- - Weapons - - - {loadouts.map((loadout) => { - return ( - { - setSpawnLoadout(loadout.name); - }} - className={`w-full`} - > - - {loadout.name} - - - ); - })} - -
- - )} -
- - Livery - - - {props.blueprint.liveries && - Object.keys(props.blueprint.liveries) - .sort((ida, idb) => { - if (props.blueprint.liveries) { - if (props.blueprint.liveries[ida].countries.length > 1) return 1; - return props.blueprint.liveries[ida].countries[0] > props.blueprint.liveries[idb].countries[0] ? 1 : -1; - } else return -1; - }) - .map((id) => { - let country = Object.values(countryCodes).find((countryCode) => { - if (props.blueprint.liveries && countryCode.liveryCodes?.includes(props.blueprint.liveries[id].countries[0])) return true; - }); - return ( - { - setSpawnLiveryID(id); - }} - className={`w-full`} - > - - {props.blueprint.liveries && props.blueprint.liveries[id].countries.length == 1 && ( - + {!props.airbase && ( +
+
+
+ + Altitude + + {`${Intl.NumberFormat("en-US").format(spawnAltitude)} FT`} +
+ setSpawnAltitudeType(!spawnAltitudeType)} /> - )} - -
- - {props.blueprint.liveries ? props.blueprint.liveries[id].name : ""} -
+ setSpawnAltitude(Number(ev.target.value))} + value={spawnAltitude} + min={minAltitude} + max={maxAltitude} + step={altitudeStep} + /> +
+ )} +
+ + Role - - ); - })} - -
-
- - Skill - - - {["Average", "Good", "High", "Excellent"].map((skill) => { - return ( - + {roles.map((role) => { + return ( + { + setSpawnRole(role); + setSpawnLoadoutName(""); + }} + className={`w-full`} + > + {role} + + ); + })} + +
+
+ + Weapons + + + {loadouts.map((loadout) => { + return ( + { + setSpawnLoadoutName(loadout.name); + }} + className={`w-full`} + > + + {loadout.name} + + + ); + })} + +
+ + )} + { - setSpawnSkill(skill); + setShowAdvancedOptions(!showAdvancedOptions); }} - className={`w-full`} + open={showAdvancedOptions} + title="Advanced options" > +
+
+ + Livery + + + {props.blueprint?.liveries && + Object.keys(props.blueprint?.liveries) + .sort((ida, idb) => { + if (props.blueprint?.liveries) { + if (props.blueprint?.liveries[ida].countries.length > 1) return 1; + return props.blueprint?.liveries[ida].countries[0] > props.blueprint?.liveries[idb].countries[0] ? 1 : -1; + } else return -1; + }) + .map((id) => { + let country = Object.values(countryCodes).find((countryCode) => { + if (props.blueprint?.liveries && countryCode.liveryCodes?.includes(props.blueprint?.liveries[id].countries[0])) return true; + }); + return ( + { + setSpawnLiveryID(id); + }} + className={`w-full`} + > + + {props.blueprint?.liveries && props.blueprint?.liveries[id].countries.length == 1 && ( + + )} + +
+ + {props.blueprint?.liveries ? props.blueprint?.liveries[id].name : ""} + +
+
+
+ ); + })} +
+
+
+ + Skill + + + {["Average", "Good", "High", "Excellent"].map((skill) => { + return ( + { + setSpawnSkill(skill); + }} + className={`w-full`} + > + +
{skill}
+
+
+ ); + })} +
+
+
+
+
+ { + setShowUnitSummary(!showUnitSummary); + }} + open={showUnitSummary} + title="Unit summary" + > + {props.blueprint ? : } + + {spawnLoadout && spawnLoadout.items.length > 0 && ( + { + setShowLoadout(!showLoadout); + }} + open={showLoadout} + title="Loadout" + > + {spawnLoadout.items.map((item) => { + return ( +
+
+ {item.quantity} +
+
+ {item.name} +
+
+ ); + })} +
+ )} + {(props.latlng || props.airbase) && ( + + )} +
+ )} + + ) : ( + <> + {" "} + {props.visible && ( +
+ {props.blueprint && } +
+
+
Quick access:
+ { + setQuickAccessName(e.target.value); + }} + value={quickAccessName} + /> + { + if (spawnRequestTable) + if (key in props.starredSpawns) getApp().getMap().removeStarredSpawnRequestTable(key); + else getApp().getMap().addStarredSpawnRequestTable(key, spawnRequestTable, quickAccessName); + }} + tooltip="Save this spawn for quick access" + checked={key in props.starredSpawns} + icon={faStar} + > +
+
+ {!props.coalition && ( + { + spawnCoalition === "blue" && setSpawnCoalition("neutral"); + spawnCoalition === "neutral" && setSpawnCoalition("red"); + spawnCoalition === "red" && setSpawnCoalition("blue"); + }} + /> + )} + { + setSpawnNumber(Math.max(minNumber, spawnNumber - 1)); + }} + onIncrease={() => { + setSpawnNumber(Math.min(maxNumber, spawnNumber + 1)); + }} + onChange={(ev) => { + !isNaN(Number(ev.target.value)) && setSpawnNumber(Math.max(minNumber, Math.min(maxNumber, Number(ev.target.value)))); + }} + /> +
+ + {["aircraft", "helicopter"].includes(props.blueprint?.category ?? "aircraft") && ( + <> + {!props.airbase && ( +
+
+
+ + Altitude + + {`${Intl.NumberFormat("en-US").format(spawnAltitude)} FT`} +
+ setSpawnAltitudeType(!spawnAltitudeType)} + /> +
+ setSpawnAltitude(Number(ev.target.value))} + value={spawnAltitude} + min={minAltitude} + max={maxAltitude} + step={altitudeStep} + /> +
+ )} +
+ + Role + + + {roles.map((role) => { + return ( + { + setSpawnRole(role); + setSpawnLoadoutName(""); + }} + className={`w-full`} + > + {role} + + ); + })} + +
+
+ + Weapons + + + {loadouts.map((loadout) => { + return ( + { + setSpawnLoadoutName(loadout.name); + }} + className={`w-full`} + > + + {loadout.name} + + + ); + })} + +
+ + )} +
-
{skill}
+ Livery
- - ); - })} - -
-
- {spawnLoadout && spawnLoadout.items.length > 0 && ( -
- { - setShowLoadout(!showLoadout); - }} - open={showLoadout} - title="Loadout" - > - {spawnLoadout.items.map((item) => { - return ( -
-
- {item.quantity} -
-
- {item.name} -
+ {props.blueprint?.liveries && + Object.keys(props.blueprint?.liveries) + .sort((ida, idb) => { + if (props.blueprint?.liveries) { + if (props.blueprint?.liveries[ida].countries.length > 1) return 1; + return props.blueprint?.liveries[ida].countries[0] > props.blueprint?.liveries[idb].countries[0] ? 1 : -1; + } else return -1; + }) + .map((id) => { + let country = Object.values(countryCodes).find((countryCode) => { + if (props.blueprint?.liveries && countryCode.liveryCodes?.includes(props.blueprint?.liveries[id].countries[0])) return true; + }); + return ( + { + setSpawnLiveryID(id); + }} + className={`w-full`} + > + + {props.blueprint?.liveries && props.blueprint?.liveries[id].countries.length == 1 && ( + + )} + +
+ + {props.blueprint?.liveries ? props.blueprint?.liveries[id].name : ""} + +
+
+
+ ); + })} +
- ); - })} -
-
+
+ + Skill + + + {["Average", "Good", "High", "Excellent"].map((skill) => { + return ( + { + setSpawnSkill(skill); + }} + className={`w-full`} + > + +
{skill}
+
+
+ ); + })} +
+
+
+ {spawnLoadout && spawnLoadout.items.length > 0 && ( +
+ { + setShowLoadout(!showLoadout); + }} + open={showLoadout} + title="Loadout" + > + {spawnLoadout.items.map((item) => { + return ( +
+
+ {item.quantity} +
+
+ {item.name} +
+
+ ); + })} +
+
+ )} + {props.airbase && ( + + )} +
+ )} + )} - {props.airbase && ( - - )} -
+ ); } diff --git a/frontend/server/src/app.ts b/frontend/server/src/app.ts index 605fbc78..c982dbc9 100644 --- a/frontend/server/src/app.ts +++ b/frontend/server/src/app.ts @@ -6,10 +6,39 @@ import fs = require("fs"); import bodyParser = require("body-parser"); import cors = require("cors"); import { AudioBackend } from "./audio/audiobackend"; +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 +} + module.exports = function (configLocation, viteProxy) { /* Config specific routers */ const elevationRouter = require("./routes/api/elevation")(configLocation); @@ -29,6 +58,30 @@ module.exports = function (configLocation, viteProxy) { ); 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" } + ); + 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" } + ); + groupsConfig = JSON.parse(rawdata); + } + /* Load the config and create the express app */ let config = {}; console.log(`Loading configuration file from ${configLocation}`); @@ -46,8 +99,94 @@ module.exports = function (configLocation, viteProxy) { /* Start the express app */ const app = express(); + /* Define the authentication */ + const defaultUsers = { + "Game master": config["authentication"]["gameMasterPassword"], + "Blue commander": config["authentication"]["blueCommanderPassword"], + "Red commander": config["authentication"]["redCommanderPassword"], + }; + let users = {}; + Object.keys(usersConfig).forEach( + (user) => (users[user] = usersConfig[user].password) + ); + const auth = expressBasicAuth({ + users: { ...defaultUsers, ...users }, + }); + /* Define middleware */ app.use(logger("dev")); + + /* Authorization middleware */ + 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); + + 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()]) + } + + next() + }) + } else { + /* Simple internal authorization will be used */ + app.use("/olympus", auth); + } + + /* 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 = req.auth?.user ?? checkCustomHeaders(config, usersConfig, groupsConfig, req); + + /* If either simple authentication or custom authentication has succeded */ + if (user) { + const userConfig = usersConfig[user]; + + /* Check that the user is authorized to at least one role */ + if (userConfig.roles.length > 0) { + /* If a specific command role is requested, proceed with that role */ + if (req.headers["x-command-mode"]) { + /* Check that the user is authorized to that role */ + if (userConfig.roles.includes(req.headers["x-command-mode"]) ) { + /* Check that the role is valid */ + if (req.headers["x-command-mode"] in defaultUsers) { + /* Apply the authorization headers */ + req.headers.authorization = `Basic ${btoa( + user + ":" + defaultUsers[req.headers["x-command-mode"]] + )}`; + } else { + res.sendStatus(401); // Unauthorized + } + } else { + res.sendStatus(401); // Unauthorized + } + } else { + /* No role has been specified, continue with the highest role */ + /* Check that the role is valid */ + if (userConfig.roles[0] in defaultUsers) { + /* Apply the authorization headers */ + req.headers.authorization = `Basic ${btoa( + user + ":" + defaultUsers[userConfig.roles[0]] + )}`; + } else { + res.sendStatus(401); // Unauthorized + } + } + } else { + res.sendStatus(401); // Unauthorized + } + + /* Send back the roles that the user is enabled to */ + res.set('X-Enabled-Command-Modes', `${userConfig.roles}`); + next(); + } else { + res.sendStatus(401); // Unauthorized + } + }); + + /* Proxy middleware */ app.use( "/olympus", httpProxyMiddleware.createProxyMiddleware({ @@ -63,7 +202,7 @@ module.exports = function (configLocation, viteProxy) { "/vite", httpProxyMiddleware.createProxyMiddleware({ target: `http://localhost:8080/`, - ws: true + ws: true, }) ); } @@ -92,7 +231,10 @@ module.exports = function (configLocation, viteProxy) { } 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 8c7ab160..3efe8afa 100644 --- a/frontend/server/src/routes/resources.ts +++ b/frontend/server/src/routes/resources.ts @@ -1,12 +1,26 @@ import express = require("express"); import fs = require("fs"); +import path = require("path"); + const router = express.Router(); let sessionHash = ""; -let sessionData = {} +let sessionData = {}; module.exports = function (configLocation) { router.get("/config", function (req, res, next) { + let profiles = null; + if ( + fs.existsSync( + path.join(path.dirname(configLocation), "olympusProfiles.json") + ) + ) { + let rawdata = fs.readFileSync( + path.join(path.dirname(configLocation), "olympusProfiles.json"), + "utf-8" + ); + profiles = JSON.parse(rawdata); + } if (fs.existsSync(configLocation)) { let rawdata = fs.readFileSync(configLocation, "utf-8"); const config = JSON.parse(rawdata); @@ -14,7 +28,7 @@ module.exports = function (configLocation) { JSON.stringify({ frontend: { ...config.frontend }, audio: { ...(config.audio ?? {}) }, - profiles: { ...(config.profiles ?? {}) }, + profiles: { ...(profiles ?? {}) }, }) ); res.end(); @@ -24,14 +38,34 @@ module.exports = function (configLocation) { }); router.put("/profile/:profileName", function (req, res, next) { - if (fs.existsSync(configLocation)) { - let rawdata = fs.readFileSync(configLocation, "utf-8"); - const config = JSON.parse(rawdata); - if (config.profiles === undefined) config.profiles = {}; - config.profiles[req.params.profileName] = req.body; + /* Create empty profiles file*/ + if ( + !fs.existsSync( + path.join(path.dirname(configLocation), "olympusProfiles.json") + ) + ) { fs.writeFileSync( - configLocation, - JSON.stringify(config, null, 2), + path.join(path.dirname(configLocation), "olympusProfiles.json"), + "{}", + "utf-8" + ); + } + + /* Check that the previous operation was successfull */ + if ( + fs.existsSync( + path.join(path.dirname(configLocation), "olympusProfiles.json") + ) + ) { + let rawdata = fs.readFileSync( + path.join(path.dirname(configLocation), "olympusProfiles.json"), + "utf-8" + ); + const usersProfiles = JSON.parse(rawdata); + usersProfiles[req.params.profileName] = req.body; + fs.writeFileSync( + path.join(path.dirname(configLocation), "olympusProfiles.json"), + JSON.stringify(usersProfiles, null, 2), "utf-8" ); res.end(); @@ -41,14 +75,21 @@ module.exports = function (configLocation) { }); router.put("/profile/reset/:profileName", function (req, res, next) { - if (fs.existsSync(configLocation)) { - let rawdata = fs.readFileSync(configLocation, "utf-8"); - const config = JSON.parse(rawdata); - if (config.profiles[req.params.profileName]) - delete config.profiles[req.params.profileName]; + if ( + fs.existsSync( + path.join(path.dirname(configLocation), "olympusProfiles.json") + ) + ) { + let rawdata = fs.readFileSync( + path.join(path.dirname(configLocation), "olympusProfiles.json"), + "utf-8" + ); + const usersProfiles = JSON.parse(rawdata); + if (req.params.profileName in usersProfiles) + delete usersProfiles[req.params.profileName]; fs.writeFileSync( - configLocation, - JSON.stringify(config, null, 2), + path.join(path.dirname(configLocation), "olympusProfiles.json"), + JSON.stringify(usersProfiles, null, 2), "utf-8" ); res.end(); @@ -58,13 +99,14 @@ module.exports = function (configLocation) { }); router.put("/profile/delete/all", function (req, res, next) { - if (fs.existsSync(configLocation)) { - let rawdata = fs.readFileSync(configLocation, "utf-8"); - const config = JSON.parse(rawdata); - config.profiles = {}; + if ( + fs.existsSync( + path.join(path.dirname(configLocation), "olympusProfiles.json") + ) + ) { fs.writeFileSync( - configLocation, - JSON.stringify(config, null, 2), + path.join(path.dirname(configLocation), "olympusProfiles.json"), + "{}", "utf-8" ); res.end(); @@ -74,15 +116,19 @@ module.exports = function (configLocation) { }); router.put("/sessiondata/save/:profileName", function (req, res, next) { - if (req.body.sessionHash === undefined || req.body.sessionData === undefined) res.sendStatus(400); + if ( + req.body.sessionHash === undefined || + req.body.sessionData === undefined + ) + res.sendStatus(400); let thisSessionHash = req.body.sessionHash; if (thisSessionHash !== sessionHash) { sessionHash = thisSessionHash; sessionData = {}; } sessionData[req.params.profileName] = req.body.sessionData; - res.end() - }) + res.end(); + }); router.put("/sessiondata/load/:profileName", function (req, res, next) { if (req.body.sessionHash === undefined) res.sendStatus(400); @@ -95,7 +141,7 @@ module.exports = function (configLocation) { res.send(sessionData[req.params.profileName]); res.end(); } - }) + }); return router; }; diff --git a/olympus.json b/olympus.json index bf4a63d1..7f2b62de 100644 --- a/olympus.json +++ b/olympus.json @@ -10,6 +10,11 @@ }, "frontend": { "port": 3000, + "customAuthHeaders": { + "enabled": false, + "username": "X-Authorized", + "group": "X-Group" + }, "elevationProvider": { "provider": "https://srtm.fasma.org/{lat}{lng}.SRTMGL3S.hgt.zip", "username": null,