feat: Added custom authentication headers support

bugfix: Fixed incorrect saving of quick access spawn
refactor: Removed compactunitspawnmenu and condensed in single unitspawnmenu class to reuse code
This commit is contained in:
Davide Passoni 2024-12-05 19:37:38 +01:00
parent c11a9728e8
commit 258d21672c
17 changed files with 1628 additions and 1379 deletions

View File

@ -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",

View File

@ -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 {

View File

@ -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);
}

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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);

View File

@ -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 {

View File

@ -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 (
<div
onClick={props.onClick}
@ -17,7 +37,18 @@ export function OlUnitListEntry(props: { icon: IconProp; blueprint: UnitBlueprin
dark:text-gray-300 dark:hover:bg-olympus-500
`}
>
<FontAwesomeIcon icon={props.icon} className="text-sm"></FontAwesomeIcon>
{props.icon && <FontAwesomeIcon icon={props.icon} className="text-sm"></FontAwesomeIcon>}
{props.silhouette && (
<div className={`
mr-2 flex h-6 w-6 rotate-90 content-center justify-center opacity-50
invert
`}>
<img
src={`public/images/units/${props.silhouette}`}
className="my-auto max-h-full max-w-full"
/>
</div>
)}
<div className="flex-1 px-2 text-left font-normal">{props.blueprint.label}</div>
{pillString && (
<div

View File

@ -28,7 +28,6 @@ import { OlDropdownItem } from "../components/oldropdown";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { OlCoalitionToggle } from "../components/olcoalitiontoggle";
import { Coalition } from "../../types/types";
import { CompactUnitSpawnMenu } from "../panels/compactunitspawnmenu";
import { CompactEffectSpawnMenu } from "../panels/compacteffectspawnmenu";
enum CategoryGroup {
@ -158,449 +157,450 @@ export function SpawnContextMenu(props: {}) {
return (
<>
{appState === OlympusState.SPAWN_CONTEXT && (
<>
<div
ref={contentRef}
className={`
absolute flex w-[395px] flex-wrap gap-2 rounded-md bg-olympus-800
`}
>
<div className="flex w-full flex-col gap-4 px-6 py-3">
<div className="flex flex-wrap justify-between gap-2">
<OlCoalitionToggle
coalition={spawnCoalition}
onClick={() => {
spawnCoalition === "blue" && setSpawnCoalition("neutral");
spawnCoalition === "neutral" && setSpawnCoalition("red");
spawnCoalition === "red" && setSpawnCoalition("blue");
}}
<div
ref={contentRef}
data-hidden={appState !== OlympusState.SPAWN_CONTEXT}
className={`
absolute flex w-[395px] data- flex-wrap gap-2 rounded-md
bg-olympus-800
data-[hidden=true]:hidden
`}
>
<div className="flex w-full flex-col gap-4 px-6 py-3">
<div className="flex flex-wrap justify-between gap-2">
<OlCoalitionToggle
coalition={spawnCoalition}
onClick={() => {
spawnCoalition === "blue" && setSpawnCoalition("neutral");
spawnCoalition === "neutral" && setSpawnCoalition("red");
spawnCoalition === "red" && setSpawnCoalition("blue");
}}
/>
<OlStateButton
checked={openAccordion === CategoryGroup.AIRCRAFT}
onClick={() => (openAccordion !== CategoryGroup.AIRCRAFT ? setOpenAccordion(CategoryGroup.AIRCRAFT) : setOpenAccordion(CategoryGroup.NONE))}
icon={olButtonsVisibilityAircraft}
tooltip="Show aircraft units"
buttonColor={spawnCoalition === "blue" ? "#2563eb" : spawnCoalition === "neutral" ? "#9ca3af" : "#ef4444"}
/>
<OlStateButton
checked={openAccordion === CategoryGroup.HELICOPTER}
onClick={() => (openAccordion !== CategoryGroup.HELICOPTER ? setOpenAccordion(CategoryGroup.HELICOPTER) : setOpenAccordion(CategoryGroup.NONE))}
icon={olButtonsVisibilityHelicopter}
tooltip="Show helicopter units"
buttonColor={spawnCoalition === "blue" ? "#2563eb" : spawnCoalition === "neutral" ? "#9ca3af" : "#ef4444"}
/>
<OlStateButton
checked={openAccordion === CategoryGroup.AIR_DEFENCE}
onClick={() => (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"}
/>
<OlStateButton
checked={openAccordion === CategoryGroup.GROUND_UNIT}
onClick={() => (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"}
/>
<OlStateButton
checked={openAccordion === CategoryGroup.NAVY_UNIT}
onClick={() => (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"}
/>
<OlStateButton checked={showMore} onClick={() => setShowMore(!showMore)} icon={faEllipsisVertical} tooltip="Show more options" />
{showMore && (
<>
<OlStateButton
checked={openAccordion === CategoryGroup.EFFECT}
onClick={() => (openAccordion !== CategoryGroup.EFFECT ? setOpenAccordion(CategoryGroup.EFFECT) : setOpenAccordion(CategoryGroup.NONE))}
icon={faExplosion}
tooltip="Show effects"
className="ml-auto"
/>
<OlStateButton
checked={openAccordion === CategoryGroup.AIRCRAFT}
onClick={() => (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"
/>
<OlStateButton
checked={openAccordion === CategoryGroup.HELICOPTER}
onClick={() =>
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"
/>
<OlStateButton
checked={openAccordion === CategoryGroup.AIR_DEFENCE}
onClick={() =>
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"}
/>
<OlStateButton
checked={openAccordion === CategoryGroup.GROUND_UNIT}
onClick={() =>
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"}
/>
<OlStateButton
checked={openAccordion === CategoryGroup.NAVY_UNIT}
onClick={() => (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"}
/>
<OlStateButton checked={showMore} onClick={() => setShowMore(!showMore)} icon={faEllipsisVertical} tooltip="Show more options" />
{showMore && (
<>
<OlStateButton
checked={openAccordion === CategoryGroup.EFFECT}
onClick={() => (openAccordion !== CategoryGroup.EFFECT ? setOpenAccordion(CategoryGroup.EFFECT) : setOpenAccordion(CategoryGroup.NONE))}
icon={faExplosion}
tooltip="Show effects"
className="ml-auto"
/>
<OlStateButton
checked={openAccordion === CategoryGroup.SEARCH}
onClick={() => (openAccordion !== CategoryGroup.SEARCH ? setOpenAccordion(CategoryGroup.SEARCH) : setOpenAccordion(CategoryGroup.NONE))}
icon={faSearch}
tooltip="Search unit"
/>
<OlStateButton
checked={openAccordion === CategoryGroup.STARRED}
onClick={() => (openAccordion !== CategoryGroup.STARRED ? setOpenAccordion(CategoryGroup.STARRED) : setOpenAccordion(CategoryGroup.NONE))}
icon={faStar}
tooltip="Show starred spanws"
/>
</>
)}
</div>
{blueprint === null && effect === null && openAccordion !== CategoryGroup.NONE && (
<div className="mb-3 flex flex-col gap-4">
<>
<>
{openAccordion === CategoryGroup.AIRCRAFT && (
<>
<div className="flex flex-wrap gap-1">
{roles.aircraft.sort().map((role) => {
return (
<div
key={role}
data-selected={selectedRole === role}
className={`
cursor-pointer rounded-full bg-olympus-900
px-2 py-0.5 text-xs font-bold
text-olympus-50
data-[selected='true']:bg-blue-500
data-[selected='true']:text-gray-200
`}
onClick={() => {
selectedRole === role ? setSelectedRole(null) : setSelectedRole(role);
}}
>
{role}
</div>
);
})}
</div>
<div
className={`
flex max-h-[350px] flex-col gap-1
overflow-y-scroll no-scrollbar
`}
>
{blueprints
?.sort((a, b) => (a.label > b.label ? 1 : -1))
.filter((blueprint) => blueprint.category === "aircraft")
.map((blueprint) => {
return (
<OlUnitListEntry
key={blueprint.name}
icon={olButtonsVisibilityAircraft}
blueprint={blueprint}
onClick={() => setBlueprint(blueprint)}
showCost={showCost}
cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)}
/>
);
})}
</div>
</>
)}
{openAccordion === CategoryGroup.HELICOPTER && (
<>
<div className="flex flex-wrap gap-1">
{roles.helicopter.sort().map((role) => {
return (
<div
key={role}
data-selected={selectedRole === role}
className={`
cursor-pointer rounded-full bg-olympus-900
px-2 py-0.5 text-xs font-bold
text-olympus-50
data-[selected='true']:bg-blue-500
data-[selected='true']:text-gray-200
`}
onClick={() => {
selectedRole === role ? setSelectedRole(null) : setSelectedRole(role);
}}
>
{role}
</div>
);
})}
</div>
<div
className={`
flex max-h-[350px] flex-col gap-1
overflow-y-scroll no-scrollbar
`}
>
{blueprints
?.sort((a, b) => (a.label > b.label ? 1 : -1))
.filter((blueprint) => blueprint.category === "helicopter")
.map((blueprint) => {
return (
<OlUnitListEntry
key={blueprint.name}
icon={olButtonsVisibilityHelicopter}
blueprint={blueprint}
onClick={() => setBlueprint(blueprint)}
showCost={showCost}
cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)}
/>
);
})}
</div>
</>
)}
{openAccordion === CategoryGroup.AIR_DEFENCE && (
<>
<div className="flex flex-wrap gap-1">
{types.groundunit
.sort()
?.filter((type) => type === "SAM Site" || type === "AAA")
.map((type) => {
return (
<div
key={type}
data-selected={selectedType === type}
className={`
cursor-pointer rounded-full bg-olympus-900
px-2 py-0.5 text-xs font-bold
text-olympus-50
data-[selected='true']:bg-blue-500
data-[selected='true']:text-gray-200
`}
onClick={() => {
selectedType === type ? setSelectedType(null) : setSelectedType(type);
}}
>
{type}
</div>
);
})}
</div>
<div
className={`
flex max-h-[350px] flex-col gap-1
overflow-y-scroll no-scrollbar
`}
>
{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 (
<OlUnitListEntry
key={blueprint.name}
icon={olButtonsVisibilityGroundunit}
blueprint={blueprint}
onClick={() => setBlueprint(blueprint)}
showCost={showCost}
cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)}
/>
);
})}
</div>
</>
)}
{openAccordion === CategoryGroup.GROUND_UNIT && (
<>
<div className="flex flex-wrap gap-1">
{types.groundunit
.sort()
?.filter((type) => type !== "SAM Site" && type !== "AAA")
.map((type) => {
return (
<div
key={type}
data-selected={selectedType === type}
className={`
cursor-pointer rounded-full bg-olympus-900
px-2 py-0.5 text-xs font-bold
text-olympus-50
data-[selected='true']:bg-blue-500
data-[selected='true']:text-gray-200
`}
onClick={() => {
selectedType === type ? setSelectedType(null) : setSelectedType(type);
}}
>
{type}
</div>
);
})}
</div>
<div
className={`
flex max-h-[350px] flex-col gap-1
overflow-y-scroll no-scrollbar
`}
>
{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 (
<OlUnitListEntry
key={blueprint.name}
icon={olButtonsVisibilityGroundunit}
blueprint={blueprint}
onClick={() => setBlueprint(blueprint)}
showCost={showCost}
cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)}
/>
);
})}
</div>
</>
)}
{openAccordion === CategoryGroup.NAVY_UNIT && (
<>
<div className="flex flex-wrap gap-1">
{types.navyunit.sort().map((type) => {
return (
<div
key={type}
data-selected={selectedType === type}
className={`
cursor-pointer rounded-full bg-olympus-900
px-2 py-0.5 text-xs font-bold
text-olympus-50
data-[selected='true']:bg-blue-500
data-[selected='true']:text-gray-200
`}
onClick={() => {
selectedType === type ? setSelectedType(null) : setSelectedType(type);
}}
>
{type}
</div>
);
})}
</div>
<div
className={`
flex max-h-[350px] flex-col gap-1
overflow-y-scroll no-scrollbar
`}
>
{blueprints
?.sort((a, b) => (a.label > b.label ? 1 : -1))
.filter((blueprint) => blueprint.category === "navyunit")
.map((blueprint) => {
return (
<OlUnitListEntry
key={blueprint.name}
icon={olButtonsVisibilityNavyunit}
blueprint={blueprint}
onClick={() => setBlueprint(blueprint)}
showCost={showCost}
cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)}
/>
);
})}
</div>
</>
)}
{openAccordion === CategoryGroup.EFFECT && (
<>
<div
className={`
flex max-h-[350px] flex-col gap-1
overflow-y-scroll no-scrollbar
`}
>
<OlEffectListEntry
key={"explosion"}
icon={faExplosion}
label={"Explosion"}
onClick={() => {
setEffect("explosion");
}}
/>
<OlEffectListEntry
key={"smoke"}
icon={faSmog}
label={"Smoke"}
onClick={() => {
setEffect("smoke");
}}
/>
</div>
</>
)}
{openAccordion === CategoryGroup.SEARCH && (
<div className="flex flex-col gap-2">
<OlSearchBar onChange={(value) => setFilterString(value)} text={filterString} />
<div
className={`
flex max-h-[350px] flex-col gap-1
overflow-y-scroll no-scrollbar
`}
>
{filteredBlueprints.length > 0 ? (
filteredBlueprints.map((blueprint) => {
return (
<OlUnitListEntry
key={blueprint.name}
icon={olButtonsVisibilityNavyunit}
blueprint={blueprint}
onClick={() => setBlueprint(blueprint)}
showCost={showCost}
cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)}
/>
);
})
) : filterString === "" ? (
<span className={`text-gray-200`}>Type to search</span>
) : (
<span className={`text-gray-200`}>No results</span>
)}
</div>
</div>
)}
{openAccordion === CategoryGroup.STARRED && (
<div className="flex flex-col gap-2">
{Object.values(starredSpawns).length > 0 ? (
Object.values(starredSpawns).map((spawnRequestTable) => {
return (
<OlDropdownItem
className={`
flex w-full content-center gap-2 text-sm
text-white
`}
onClick={() => {
if (latlng) {
spawnRequestTable.unit.location = latlng;
getApp()
.getUnitsManager()
.spawnUnits(spawnRequestTable.category, Array(spawnRequestTable.amount).fill(spawnRequestTable.unit), spawnRequestTable.coalition, false);
getApp().setState(OlympusState.IDLE);
}
}}
>
<FontAwesomeIcon
data-coalition={spawnRequestTable.coalition}
className={`
my-auto
data-[coalition='blue']:text-blue-500
data-[coalition='neutral']:text-gay-500
data-[coalition='red']:text-red-500
`}
icon={faStar}
/>
<div>
{getApp().getUnitsManager().getDatabase().getByName(spawnRequestTable.unit.unitType)?.label} (
{spawnRequestTable.quickAccessName})
</div>
</OlDropdownItem>
);
})
) : (
<div className="p-2 text-sm text-white">No starred spawns, use the spawn menu to create a quick access spawn</div>
)}
</div>
)}
</>
</>
</div>
)}
{!(blueprint === null) && <CompactUnitSpawnMenu blueprint={blueprint} starredSpawns={starredSpawns} latlng={latlng} coalition={spawnCoalition} onBack={() => setBlueprint(null)}/>}
{!(effect === null) && latlng && <CompactEffectSpawnMenu effect={effect} latlng={latlng} onBack={() => setEffect(null)} />}
</div>
</>
)}
</div>
</>
)}
{blueprint === null && effect === null && openAccordion !== CategoryGroup.NONE && (
<div className="mb-3 flex flex-col gap-4">
<>
<>
{openAccordion === CategoryGroup.AIRCRAFT && (
<>
<div className="flex flex-wrap gap-1">
{roles.aircraft.sort().map((role) => {
return (
<div
key={role}
data-selected={selectedRole === role}
className={`
cursor-pointer rounded-full bg-olympus-900 px-2
py-0.5 text-xs font-bold text-olympus-50
data-[selected='true']:bg-blue-500
data-[selected='true']:text-gray-200
`}
onClick={() => {
selectedRole === role ? setSelectedRole(null) : setSelectedRole(role);
}}
>
{role}
</div>
);
})}
</div>
<div
className={`
flex max-h-[350px] flex-col gap-1 overflow-y-scroll
no-scrollbar
`}
>
{blueprints
?.sort((a, b) => (a.label > b.label ? 1 : -1))
.filter((blueprint) => blueprint.category === "aircraft")
.map((blueprint) => {
return (
<OlUnitListEntry
key={blueprint.name}
silhouette={blueprint.filename}
blueprint={blueprint}
onClick={() => setBlueprint(blueprint)}
showCost={showCost}
cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)}
/>
);
})}
</div>
</>
)}
{openAccordion === CategoryGroup.HELICOPTER && (
<>
<div className="flex flex-wrap gap-1">
{roles.helicopter.sort().map((role) => {
return (
<div
key={role}
data-selected={selectedRole === role}
className={`
cursor-pointer rounded-full bg-olympus-900 px-2
py-0.5 text-xs font-bold text-olympus-50
data-[selected='true']:bg-blue-500
data-[selected='true']:text-gray-200
`}
onClick={() => {
selectedRole === role ? setSelectedRole(null) : setSelectedRole(role);
}}
>
{role}
</div>
);
})}
</div>
<div
className={`
flex max-h-[350px] flex-col gap-1 overflow-y-scroll
no-scrollbar
`}
>
{blueprints
?.sort((a, b) => (a.label > b.label ? 1 : -1))
.filter((blueprint) => blueprint.category === "helicopter")
.map((blueprint) => {
return (
<OlUnitListEntry
key={blueprint.name}
silhouette={blueprint.filename}
blueprint={blueprint}
onClick={() => setBlueprint(blueprint)}
showCost={showCost}
cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)}
/>
);
})}
</div>
</>
)}
{openAccordion === CategoryGroup.AIR_DEFENCE && (
<>
<div className="flex flex-wrap gap-1">
{types.groundunit
.sort()
?.filter((type) => type === "SAM Site" || type === "AAA")
.map((type) => {
return (
<div
key={type}
data-selected={selectedType === type}
className={`
cursor-pointer rounded-full bg-olympus-900
px-2 py-0.5 text-xs font-bold text-olympus-50
data-[selected='true']:bg-blue-500
data-[selected='true']:text-gray-200
`}
onClick={() => {
selectedType === type ? setSelectedType(null) : setSelectedType(type);
}}
>
{type}
</div>
);
})}
</div>
<div
className={`
flex max-h-[350px] flex-col gap-1 overflow-y-scroll
no-scrollbar
`}
>
{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 (
<OlUnitListEntry
key={blueprint.name}
icon={olButtonsVisibilityGroundunitSam}
blueprint={blueprint}
onClick={() => setBlueprint(blueprint)}
showCost={showCost}
cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)}
/>
);
})}
</div>
</>
)}
{openAccordion === CategoryGroup.GROUND_UNIT && (
<>
<div className="flex flex-wrap gap-1">
{types.groundunit
.sort()
?.filter((type) => type !== "SAM Site" && type !== "AAA")
.map((type) => {
return (
<div
key={type}
data-selected={selectedType === type}
className={`
cursor-pointer rounded-full bg-olympus-900
px-2 py-0.5 text-xs font-bold text-olympus-50
data-[selected='true']:bg-blue-500
data-[selected='true']:text-gray-200
`}
onClick={() => {
selectedType === type ? setSelectedType(null) : setSelectedType(type);
}}
>
{type}
</div>
);
})}
</div>
<div
className={`
flex max-h-[350px] flex-col gap-1 overflow-y-scroll
no-scrollbar
`}
>
{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 (
<OlUnitListEntry
key={blueprint.name}
icon={olButtonsVisibilityGroundunit}
blueprint={blueprint}
onClick={() => setBlueprint(blueprint)}
showCost={showCost}
cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)}
/>
);
})}
</div>
</>
)}
{openAccordion === CategoryGroup.NAVY_UNIT && (
<>
<div className="flex flex-wrap gap-1">
{types.navyunit.sort().map((type) => {
return (
<div
key={type}
data-selected={selectedType === type}
className={`
cursor-pointer rounded-full bg-olympus-900 px-2
py-0.5 text-xs font-bold text-olympus-50
data-[selected='true']:bg-blue-500
data-[selected='true']:text-gray-200
`}
onClick={() => {
selectedType === type ? setSelectedType(null) : setSelectedType(type);
}}
>
{type}
</div>
);
})}
</div>
<div
className={`
flex max-h-[350px] flex-col gap-1 overflow-y-scroll
no-scrollbar
`}
>
{blueprints
?.sort((a, b) => (a.label > b.label ? 1 : -1))
.filter((blueprint) => blueprint.category === "navyunit")
.map((blueprint) => {
return (
<OlUnitListEntry
key={blueprint.name}
icon={olButtonsVisibilityNavyunit}
blueprint={blueprint}
onClick={() => setBlueprint(blueprint)}
showCost={showCost}
cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)}
/>
);
})}
</div>
</>
)}
{openAccordion === CategoryGroup.EFFECT && (
<>
<div
className={`
flex max-h-[350px] flex-col gap-1 overflow-y-scroll
no-scrollbar
`}
>
<OlEffectListEntry
key={"explosion"}
icon={faExplosion}
label={"Explosion"}
onClick={() => {
setEffect("explosion");
}}
/>
<OlEffectListEntry
key={"smoke"}
icon={faSmog}
label={"Smoke"}
onClick={() => {
setEffect("smoke");
}}
/>
</div>
</>
)}
{openAccordion === CategoryGroup.SEARCH && (
<div className="flex flex-col gap-2">
<OlSearchBar onChange={(value) => setFilterString(value)} text={filterString} />
<div
className={`
flex max-h-[350px] flex-col gap-1 overflow-y-scroll
no-scrollbar
`}
>
{filteredBlueprints.length > 0 ? (
filteredBlueprints.map((blueprint) => {
return (
<OlUnitListEntry
key={blueprint.name}
icon={olButtonsVisibilityNavyunit}
blueprint={blueprint}
onClick={() => setBlueprint(blueprint)}
showCost={showCost}
cost={getApp().getUnitsManager().getDatabase().getSpawnPointsByName(blueprint.name)}
/>
);
})
) : filterString === "" ? (
<span className={`text-gray-200`}>Type to search</span>
) : (
<span className={`text-gray-200`}>No results</span>
)}
</div>
</div>
)}
{openAccordion === CategoryGroup.STARRED && (
<div className="flex flex-col gap-2">
{Object.values(starredSpawns).length > 0 ? (
Object.values(starredSpawns).map((spawnRequestTable) => {
return (
<OlDropdownItem
className={`
flex w-full content-center gap-2 text-sm
text-white
`}
onClick={() => {
if (latlng) {
spawnRequestTable.unit.location = latlng;
getApp()
.getUnitsManager()
.spawnUnits(
spawnRequestTable.category,
Array(spawnRequestTable.amount).fill(spawnRequestTable.unit),
spawnRequestTable.coalition,
false
);
getApp().setState(OlympusState.IDLE);
}
}}
>
<FontAwesomeIcon
data-coalition={spawnRequestTable.coalition}
className={`
my-auto
data-[coalition='blue']:text-blue-500
data-[coalition='neutral']:text-gay-500
data-[coalition='red']:text-red-500
`}
icon={faStar}
/>
<div>
{getApp().getUnitsManager().getDatabase().getByName(spawnRequestTable.unit.unitType)?.label} (
{spawnRequestTable.quickAccessName})
</div>
</OlDropdownItem>
);
})
) : (
<div className="p-2 text-sm text-white">No starred spawns, use the spawn menu to create a quick access spawn</div>
)}
</div>
)}
</>
</>
</div>
)}
<UnitSpawnMenu
compact={true}
visible={blueprint !== null}
blueprint={blueprint}
starredSpawns={starredSpawns}
latlng={latlng}
coalition={spawnCoalition}
onBack={() => setBlueprint(null)}
/>
{!(effect === null) && latlng && <CompactEffectSpawnMenu effect={effect} latlng={latlng} onBack={() => setEffect(null)} />}
</div>
</div>
</>
);
}

View File

@ -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 (
<Modal
open={props.open}
className={`
inline-flex h-[75%] max-h-[570px] w-[80%] max-w-[1100px] overflow-y-auto
inline-flex h-[75%] max-h-[600px] w-[80%] max-w-[1100px] overflow-y-auto
scroll-smooth bg-white
dark:bg-olympus-800
max-md:h-full max-md:max-h-full max-md:w-full max-md:rounded-none
max-md:border-none
`}
>
<img
src="/vite/images/splash/1.jpg"
className={`contents-center w-full object-cover opacity-[7%]`}
></img>
<img src="/vite/images/splash/1.jpg" className={`
contents-center w-full object-cover opacity-[7%]
`}></img>
<div
className={`
absolute h-full w-full bg-gradient-to-r from-blue-200/25
@ -127,10 +153,9 @@ export function LoginModal(props: { open: boolean }) {
`}
>
<span className="size-[80px] min-w-14">
<img
src="..\vite\images\olympus-500x500.png"
className={`flex w-full`}
></img>
<img src="..\vite\images\olympus-500x500.png" className={`
flex w-full
`}></img>
</span>
<div className={`flex flex-col items-start gap-1`}>
<h1
@ -155,9 +180,36 @@ export function LoginModal(props: { open: boolean }) {
</div>
{!loginError ? (
<>
{commandMode === null ? (
{subState === LoginSubState.CREDENTIALS && (
<>
<div className={`flex flex-col items-start gap-2`}>
<label
className={`
text-gray-800 text-md
dark:text-white
`}
>
Username
</label>
<input
type="text"
autoComplete="username"
onChange={(ev) => 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
/>
<label
className={`
text-gray-800 text-md
@ -186,7 +238,7 @@ export function LoginModal(props: { open: boolean }) {
<div className="flex">
<button
type="button"
onClick={() => checkPassword(password)}
onClick={() => getApp().setState(OlympusState.LOGIN, LoginSubState.COMMAND_MODE)}
className={`
mb-2 me-2 flex content-center items-center gap-2
rounded-sm bg-blue-700 px-5 py-2.5 text-sm
@ -208,7 +260,8 @@ export function LoginModal(props: { open: boolean }) {
*/}
</div>
</>
) : (
)}
{subState === LoginSubState.COMMAND_MODE && (
<>
<div className={`flex flex-col items-start gap-2`}>
<label
@ -217,32 +270,20 @@ export function LoginModal(props: { open: boolean }) {
dark:text-white
`}
>
Set profile name
Choose your role
</label>
<input
type="text"
autoComplete="username"
onChange={(ev) => 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
/>
<OlDropdown label={activeCommandMode ?? ""} className={`
w-48
`}>
{commandModes?.map((commandMode) => {
return <OlDropdownItem onClick={() => setActiveCommandMode(commandMode)}>{commandMode}</OlDropdownItem>;
})}
</OlDropdown>
</div>
<div className="text-xs text-gray-400">The profile name you choose determines the saved key binds, groups and options you see.</div>
<div className="flex">
<button
type="button"
onClick={() => connect()}
onClick={() => getApp().setState(OlympusState.LOGIN, LoginSubState.CONNECT)}
className={`
mb-2 me-2 flex content-center items-center gap-2
rounded-sm bg-blue-700 px-5 py-2.5 text-sm
@ -257,22 +298,6 @@ export function LoginModal(props: { open: boolean }) {
Continue
<FontAwesomeIcon className={`my-auto`} icon={faArrowRight} />
</button>
<button
type="button"
className={`
mb-2 me-2 flex content-center items-center gap-2
rounded-sm border-[1px] bg-blue-700 px-5 py-2.5
text-sm font-medium text-white
dark:border-gray-600 dark:bg-gray-800
dark:text-gray-400 dark:hover:bg-gray-700
dark:focus:ring-blue-800
focus:outline-none focus:ring-4
focus:ring-blue-300
hover:bg-blue-800
`}
>
Back
</button>
</div>
</>
)}

View File

@ -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 (
<div className="flex flex-col">
<div className="flex h-fit flex-col gap-3">
<div className="flex">
<FontAwesomeIcon
onClick={props.onBack}
icon={faArrowLeft}
className={`
my-auto mr-1 h-4 cursor-pointer rounded-md p-2
dark:text-gray-500 dark:hover:bg-gray-700 dark:hover:text-white
`}
/>
<h5 className="my-auto text-gray-200">{props.blueprint.label}</h5>
<OlNumberInput
className={"ml-auto"}
value={spawnNumber}
min={minNumber}
max={maxNumber}
onDecrease={() => {
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))));
}}
/>
</div>
<div
className={`
inline-flex w-full flex-row content-center justify-between gap-2
`}
>
<div className="my-auto text-sm text-white">Quick access: </div>
<OlStringInput
onChange={(e) => {
setQuickAccessName(e.target.value);
}}
value={quickAccessName}
/>
<OlStateButton
onClick={() => {
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}
></OlStateButton>
</div>
{["aircraft", "helicopter"].includes(props.blueprint.category) && (
<>
{!props.airbase && (
<div>
<div
className={`
flex flex-row content-center items-center justify-between
`}
>
<div className="flex flex-col">
<span
className={`
font-normal
dark:text-white
`}
>
Altitude
</span>
<span
className={`
font-bold
dark:text-blue-500
`}
>{`${Intl.NumberFormat("en-US").format(spawnAltitude)} FT`}</span>
</div>
<OlLabelToggle toggled={spawnAltitudeType} leftLabel={"AGL"} rightLabel={"ASL"} onClick={() => setSpawnAltitudeType(!spawnAltitudeType)} />
</div>
<OlRangeSlider
onChange={(ev) => setSpawnAltitude(Number(ev.target.value))}
value={spawnAltitude}
min={minAltitude}
max={maxAltitude}
step={altitudeStep}
/>
</div>
)}
<div className="flex content-center justify-between gap-2">
<span
className={`
my-auto font-normal
dark:text-white
`}
>
Role
</span>
<OlDropdown label={spawnRole} className="w-64">
{roles.map((role) => {
return (
<OlDropdownItem
onClick={() => {
setSpawnRole(role);
setSpawnLoadout("");
}}
className={`w-full`}
>
{role}
</OlDropdownItem>
);
})}
</OlDropdown>
</div>
<div className="flex content-center justify-between gap-2">
<span
className={`
my-auto font-normal
dark:text-white
`}
>
Weapons
</span>
<OlDropdown label={spawnLoadoutName} className={`w-64`}>
{loadouts.map((loadout) => {
return (
<OlDropdownItem
onClick={() => {
setSpawnLoadout(loadout.name);
}}
className={`w-full`}
>
<span
className={`
w-full overflow-hidden text-ellipsis text-nowrap
text-left w-max-full
`}
>
{loadout.name}
</span>
</OlDropdownItem>
);
})}
</OlDropdown>
</div>
</>
)}
<OlAccordion
onClick={() => {
setShowAdvancedOptions(!showAdvancedOptions);
}}
open={showAdvancedOptions}
title="Advanced options"
>
<div className="flex flex-col gap-2">
<div className="flex content-center justify-between gap-2">
<span
className={`
my-auto font-normal
dark:text-white
`}
>
Livery
</span>
<OlDropdown
label={props.blueprint.liveries ? (props.blueprint.liveries[spawnLiveryID]?.name ?? "Default") : "No livery"}
className={`w-64`}
>
{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 (
<OlDropdownItem
onClick={() => {
setSpawnLiveryID(id);
}}
className={`w-full`}
>
<span
className={`
w-full content-center overflow-hidden
text-ellipsis text-nowrap text-left w-max-full
flex gap-2
`}
>
{props.blueprint.liveries && props.blueprint.liveries[id].countries.length == 1 && (
<img src={`images/countries/${country?.flagCode.toLowerCase()}.svg`} className={`
h-6
`} />
)}
<div className="my-auto truncate">
<span
className={`
w-full overflow-hidden text-left w-max-full
`}
>
{props.blueprint.liveries ? props.blueprint.liveries[id].name : ""}
</span>
</div>
</span>
</OlDropdownItem>
);
})}
</OlDropdown>
</div>
<div className="flex content-center justify-between gap-2">
<span
className={`
my-auto font-normal
dark:text-white
`}
>
Skill
</span>
<OlDropdown label={spawnSkill} className={`w-64`}>
{["Average", "Good", "High", "Excellent"].map((skill) => {
return (
<OlDropdownItem
onClick={() => {
setSpawnSkill(skill);
}}
className={`w-full`}
>
<span
className={`
w-full content-center overflow-hidden text-ellipsis
text-nowrap text-left w-max-full flex gap-2
`}
>
<div className="my-auto">{skill}</div>
</span>
</OlDropdownItem>
);
})}
</OlDropdown>
</div>
</div>
</OlAccordion>
</div>
<OlAccordion
onClick={() => {
setShowUnitSummary(!showUnitSummary);
}}
open={showUnitSummary}
title="Unit summary"
>
<OlUnitSummary blueprint={props.blueprint} coalition={spawnCoalition} />
</OlAccordion>
{spawnLoadout && spawnLoadout.items.length > 0 && (
<OlAccordion
onClick={() => {
setShowLoadout(!showLoadout);
}}
open={showLoadout}
title="Loadout"
>
{spawnLoadout.items.map((item) => {
return (
<div className="flex content-center gap-2">
<div
className={`
my-auto w-6 min-w-6 rounded-full py-0.5 text-center text-sm
font-bold text-gray-500
dark:bg-[#17212D]
`}
>
{item.quantity}
</div>
<div
className={`
my-auto overflow-hidden text-ellipsis text-nowrap text-sm
dark:text-gray-300
`}
>
{item.name}
</div>
</div>
);
})}
</OlAccordion>
)}
{(props.latlng || props.airbase) && (
<button
type="button"
data-coalition={props.coalition ?? "blue"}
className={`
m-2 rounded-lg px-5 py-2.5 text-sm font-medium text-white
data-[coalition='blue']:bg-blue-600
data-[coalition='neutral']:bg-gray-400
data-[coalition='red']:bg-red-500
focus:outline-none focus:ring-4
`}
onClick={() => {
if (spawnRequestTable)
getApp()
.getUnitsManager()
.spawnUnits(spawnRequestTable.category, Array(spawnRequestTable.amount).fill(spawnRequestTable.unit), spawnRequestTable.coalition, false, props.airbase?.getName() ?? undefined);
getApp().setState(OlympusState.IDLE)
}}
>
Spawn
</button>
)}
</div>
);
}

View File

@ -166,7 +166,7 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children?
return (
<OlUnitListEntry
key={blueprint.name}
icon={olButtonsVisibilityAircraft}
silhouette={blueprint.filename}
blueprint={blueprint}
onClick={() => setBlueprint(blueprint)}
showCost={showCost}
@ -433,13 +433,14 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children?
</div>
)}
{!(blueprint === null) && (
<UnitSpawnMenu
blueprint={blueprint}
starredSpawns={starredSpawns}
coalition={commandModeOptions.commandMode !== GAME_MASTER ? (commandModeOptions.commandMode === BLUE_COMMANDER ? "blue" : "red") : undefined}
/>
)}
<UnitSpawnMenu
visible={blueprint !== null}
compact={false}
blueprint={blueprint}
starredSpawns={starredSpawns}
coalition={commandModeOptions.commandMode !== GAME_MASTER ? (commandModeOptions.commandMode === BLUE_COMMANDER ? "blue" : "red") : undefined}
/>
{!(effect === null) && <EffectSpawnMenu effect={effect} />}
</>
</Menu>

File diff suppressed because it is too large Load Diff

View File

@ -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();
}

View File

@ -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;
};

View File

@ -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,