mirror of
https://github.com/Pax1601/DCSOlympus.git
synced 2025-10-29 16:56:34 +00:00
feat: added admin password and admin modal
This commit is contained in:
parent
9a7af84cd4
commit
386d5298a2
@ -328,7 +328,8 @@ export enum OlympusState {
|
||||
WARNING = "Warning modal",
|
||||
DATABASE_EDITOR = "Database editor",
|
||||
MEASURE = "Measure",
|
||||
TRAINING = "Training"
|
||||
TRAINING = "Training",
|
||||
ADMIN = "Admin",
|
||||
}
|
||||
|
||||
export const NO_SUBSTATE = "No substate";
|
||||
@ -384,6 +385,7 @@ export enum WarningSubstate {
|
||||
NO_SUBSTATE = "No substate",
|
||||
NOT_CHROME = "Not chrome",
|
||||
NOT_SECURE = "Not secure",
|
||||
ERROR_UPLOADING_CONFIG = "Error uploading config"
|
||||
}
|
||||
|
||||
export type OlympusSubState = DrawSubState | JTACSubState | SpawnSubState | OptionsSubstate | string;
|
||||
|
||||
@ -230,6 +230,23 @@ export class SessionDataChangedEvent {
|
||||
export class SessionDataSavedEvent extends SessionDataChangedEvent {}
|
||||
export class SessionDataLoadedEvent extends SessionDataChangedEvent {}
|
||||
|
||||
export class AdminPasswordChangedEvent {
|
||||
static on(callback: (password: string) => void, singleShot = false) {
|
||||
document.addEventListener(
|
||||
this.name,
|
||||
(ev: CustomEventInit) => {
|
||||
callback(ev.detail.password);
|
||||
},
|
||||
{ once: singleShot }
|
||||
);
|
||||
}
|
||||
|
||||
static dispatch(password: string) {
|
||||
document.dispatchEvent(new CustomEvent(this.name, { detail: { password } }));
|
||||
console.log(`Event ${this.name} dispatched`);
|
||||
}
|
||||
}
|
||||
|
||||
/************** Map events ***************/
|
||||
export class MouseMovedEvent {
|
||||
static on(callback: (latlng: LatLng, elevation: number) => void, singleShot = false) {
|
||||
|
||||
@ -21,7 +21,7 @@ import { ServerManager } from "./server/servermanager";
|
||||
import { AudioManager } from "./audio/audiomanager";
|
||||
|
||||
import { GAME_MASTER, LoginSubState, NO_SUBSTATE, OlympusState, OlympusSubState, WarningSubstate } from "./constants/constants";
|
||||
import { AppStateChangedEvent, ConfigLoadedEvent, InfoPopupEvent, MapOptionsChangedEvent, SelectedUnitsChangedEvent, ShortcutsChangedEvent } from "./events";
|
||||
import { AdminPasswordChangedEvent, AppStateChangedEvent, ConfigLoadedEvent, InfoPopupEvent, MapOptionsChangedEvent, SelectedUnitsChangedEvent, ShortcutsChangedEvent } from "./events";
|
||||
import { OlympusConfig } from "./interfaces";
|
||||
import { SessionDataManager } from "./sessiondata";
|
||||
import { ControllerManager } from "./controllers/controllermanager";
|
||||
@ -57,6 +57,8 @@ export class OlympusApp {
|
||||
#drawingsManager: DrawingsManager;
|
||||
//#pluginsManager: // TODO
|
||||
|
||||
#adminPassword: string = "";
|
||||
|
||||
constructor() {
|
||||
SelectedUnitsChangedEvent.on((selectedUnits) => {
|
||||
if (selectedUnits.length > 0) this.setState(OlympusState.UNIT_CONTROL);
|
||||
@ -348,6 +350,11 @@ export class OlympusApp {
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
setAdminPassword(newAdminPassword: string) {
|
||||
this.#adminPassword = newAdminPassword;
|
||||
AdminPasswordChangedEvent.dispatch(newAdminPassword);
|
||||
}
|
||||
|
||||
startServerMode() {
|
||||
//ConfigLoadedEvent.on((config) => {
|
||||
// this.getAudioManager().start();
|
||||
|
||||
337
frontend/react/src/ui/modals/adminmodal.tsx
Normal file
337
frontend/react/src/ui/modals/adminmodal.tsx
Normal file
@ -0,0 +1,337 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { Modal } from "./components/modal";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faArrowRight } from "@fortawesome/free-solid-svg-icons";
|
||||
import { getApp } from "../../olympusapp";
|
||||
import { OlympusState, WarningSubstate } from "../../constants/constants";
|
||||
import { FaPlus, FaTrash } from "react-icons/fa";
|
||||
import { sha256 } from "js-sha256";
|
||||
import { AdminPasswordChangedEvent } from "../../events";
|
||||
import { OlDropdown } from "../components/oldropdown";
|
||||
import { OlCheckbox } from "../components/olcheckbox";
|
||||
|
||||
export function AdminModal(props: { open: boolean }) {
|
||||
const [configs, setConfigs] = useState({} as { groups: { [key: string]: string[] }; users: { [key: string]: { password: string; roles: string[] } } });
|
||||
const [newUserName, setNewUserName] = useState("");
|
||||
const [newGroupName, setNewGroupName] = useState("");
|
||||
const [adminPassword, setAdminPassword] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
AdminPasswordChangedEvent.on((password) => {
|
||||
setAdminPassword(password);
|
||||
|
||||
var hash = sha256.create();
|
||||
|
||||
const requestOptions: RequestInit = {
|
||||
method: "GET", // Specify the request method
|
||||
headers: {
|
||||
Authorization: "Basic " + btoa(`Admin:${hash.update(password).hex()}`),
|
||||
}, // Specify the content type
|
||||
};
|
||||
|
||||
fetch(`./admin/config`, requestOptions)
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
console.log(`Admin password correct`);
|
||||
return response.json();
|
||||
} else {
|
||||
throw new Error("Admin password incorrect");
|
||||
}
|
||||
})
|
||||
.then((data) => {
|
||||
setConfigs(data);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(`Error reading configuration: ${error}`);
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
const uploadNewConfig = useCallback(() => {
|
||||
var hash = sha256.create();
|
||||
|
||||
const requestOptions: RequestInit = {
|
||||
method: "PUT", // Specify the request method
|
||||
headers: {
|
||||
Authorization: "Basic " + btoa(`Admin:${hash.update(adminPassword).hex()}`),
|
||||
"Content-Type": "application/json",
|
||||
}, // Specify the content type
|
||||
body: JSON.stringify(configs),
|
||||
};
|
||||
|
||||
fetch(`./admin/config`, requestOptions)
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
console.log(`Configuration uploaded`);
|
||||
} else {
|
||||
throw new Error("Error uploading configuration");
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
getApp().setState(OlympusState.WARNING, WarningSubstate.ERROR_UPLOADING_CONFIG);
|
||||
console.error(`Error uploading configuration: ${error}`);
|
||||
});
|
||||
}, [adminPassword, configs]);
|
||||
|
||||
|
||||
return (
|
||||
<Modal open={props.open}>
|
||||
<div className="flex w-full gap-4">
|
||||
<div className="w-[40%]">
|
||||
<div className="text-white">Groups:</div>
|
||||
<div className="flex max-h-[300px] flex-col gap-1 overflow-auto p-2">
|
||||
{configs.groups &&
|
||||
Object.keys(configs.groups).map((group: any) => {
|
||||
return (
|
||||
<div
|
||||
key={group}
|
||||
className={`
|
||||
flex justify-between gap-4 text-sm text-gray-200
|
||||
`}
|
||||
>
|
||||
<div className="my-auto">{group}</div>
|
||||
<OlDropdown
|
||||
label="Enabled roles"
|
||||
className={`my-auto ml-auto min-w-48`}
|
||||
disableAutoClose={true}
|
||||
>
|
||||
{["Game master", "Blue commander", "Red commander"].map((role: any) => {
|
||||
return (
|
||||
<div key={role} className="flex gap-2 p-2">
|
||||
<OlCheckbox
|
||||
checked={configs["groups"][group].includes(role)}
|
||||
onChange={(ev) => {
|
||||
if (ev.target.checked) {
|
||||
configs["groups"][group].push(role);
|
||||
} else {
|
||||
configs["groups"][group] = configs["groups"][group].filter((r: any) => r !== role);
|
||||
}
|
||||
setConfigs({ ...configs });
|
||||
}}
|
||||
></OlCheckbox>
|
||||
{role}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</OlDropdown>
|
||||
<div
|
||||
className={`
|
||||
my-auto cursor-pointer rounded-md bg-red-600 p-2
|
||||
hover:bg-red-400
|
||||
`}
|
||||
onClick={() => {
|
||||
delete configs["users"][group];
|
||||
}}
|
||||
>
|
||||
<FaTrash className={`text-gray-50`}></FaTrash>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{(configs.groups === undefined || Object.keys(configs.groups).length === 0) && (
|
||||
<div
|
||||
className={`text-gray-400`}
|
||||
>
|
||||
No groups defined
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex justify-between gap-4">
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="new-password"
|
||||
onChange={(ev) => {
|
||||
setNewUserName(ev.currentTarget.value);
|
||||
}}
|
||||
className={`
|
||||
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="New group name"
|
||||
value={newGroupName}
|
||||
required
|
||||
/>
|
||||
<div
|
||||
className={`
|
||||
my-auto cursor-pointer rounded-md border-[1px] border-white
|
||||
bg-transparent p-2
|
||||
hover:bg-gray-800
|
||||
`}
|
||||
onClick={() => {
|
||||
if (newGroupName === "") return;
|
||||
configs["groups"][newGroupName] = [];
|
||||
setConfigs({ ...configs });
|
||||
setNewGroupName("");
|
||||
}}
|
||||
>
|
||||
<FaPlus className={`text-gray-50`} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-[58%] flex-col gap-2">
|
||||
<div className="text-white">Users:</div>
|
||||
<div className={`flex max-h-[300px] flex-col gap-1 overflow-auto p-2`}>
|
||||
{configs.users &&
|
||||
Object.keys(configs.users).map((user: any) => {
|
||||
return (
|
||||
<div
|
||||
key={user.id}
|
||||
className={`
|
||||
flex justify-between gap-2 text-sm text-gray-200
|
||||
`}
|
||||
>
|
||||
<div className="my-auto">{user}</div>
|
||||
|
||||
<OlDropdown
|
||||
label="Enabled roles"
|
||||
className={`my-auto ml-auto min-w-48`}
|
||||
disableAutoClose={true}
|
||||
>
|
||||
{["Game master", "Blue commander", "Red commander"].map((role: any) => {
|
||||
return (
|
||||
<div key={role} className="flex gap-2 p-2">
|
||||
<OlCheckbox
|
||||
checked={configs["users"][user].roles.includes(role)}
|
||||
onChange={(ev) => {
|
||||
if (ev.target.checked) {
|
||||
configs["users"][user].roles.push(role);
|
||||
} else {
|
||||
configs["users"][user].roles = configs["users"][user].roles.filter((r: any) => r !== role);
|
||||
}
|
||||
setConfigs({ ...configs });
|
||||
}}
|
||||
></OlCheckbox>
|
||||
{role}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</OlDropdown>
|
||||
|
||||
<input
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
onChange={(ev) => {
|
||||
var hash = sha256.create();
|
||||
configs["users"][user].password = hash.update(ev.currentTarget.value).hex();
|
||||
setConfigs({ ...configs });
|
||||
}}
|
||||
className={`
|
||||
max-w-44 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="Change password"
|
||||
required
|
||||
/>
|
||||
<div
|
||||
className={`
|
||||
my-auto cursor-pointer rounded-md bg-red-600 p-2
|
||||
hover:bg-red-400
|
||||
`}
|
||||
onClick={() => {
|
||||
delete configs["users"][user];
|
||||
setConfigs({ ...configs });
|
||||
}}
|
||||
>
|
||||
<FaTrash className={`text-gray-50`}></FaTrash>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{(configs.users === undefined || Object.keys(configs.users).length === 0) && (
|
||||
<div
|
||||
className={`text-gray-400`}
|
||||
>
|
||||
No users defined
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex justify-between gap-4">
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="new-password"
|
||||
onChange={(ev) => {
|
||||
setNewUserName(ev.currentTarget.value);
|
||||
}}
|
||||
className={`
|
||||
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="New user name"
|
||||
value={newUserName}
|
||||
required
|
||||
/>
|
||||
<div
|
||||
className={`
|
||||
my-auto cursor-pointer rounded-md border-[1px] border-white
|
||||
bg-transparent p-2
|
||||
hover:bg-gray-800
|
||||
`}
|
||||
onClick={() => {
|
||||
if (newUserName === "") return;
|
||||
configs["users"][newUserName] = { password: "", roles: [] };
|
||||
setConfigs({ ...configs });
|
||||
setNewUserName("");
|
||||
}}
|
||||
>
|
||||
<FaPlus className={`text-gray-50`} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-auto flex justify-between">
|
||||
<div className="my-auto flex gap-4 text-sm text-gray-400">
|
||||
<div className="my-auto">Reset all user preferences, use with caution</div>
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => getApp().resetAllProfiles()}
|
||||
className={`
|
||||
flex content-center items-center gap-2 text-nowrap rounded-sm
|
||||
border-[1px] bg-blue-700 px-5 py-2.5 text-sm font-medium
|
||||
text-white
|
||||
dark:border-red-600 dark:bg-red-800 dark:text-gray-400
|
||||
dark:hover:bg-red-700 dark:focus:ring-blue-800
|
||||
focus:outline-none focus:ring-4 focus:ring-blue-300
|
||||
hover:bg-red-800
|
||||
`}
|
||||
>
|
||||
Reset profiles
|
||||
<FaTrash />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
uploadNewConfig();
|
||||
getApp().setState(OlympusState.IDLE)}
|
||||
}
|
||||
className={`
|
||||
my-auto flex content-center items-center gap-2 rounded-sm
|
||||
bg-blue-700 px-5 py-2.5 text-sm font-medium text-white
|
||||
dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800
|
||||
focus:outline-none focus:ring-4 focus:ring-blue-300
|
||||
hover:bg-blue-800
|
||||
`}
|
||||
>
|
||||
Apply changes
|
||||
<FontAwesomeIcon className={`my-auto`} icon={faArrowRight} />
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@ -73,6 +73,15 @@ export function WarningModal(props: { open: boolean }) {
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
case WarningSubstate.ERROR_UPLOADING_CONFIG:
|
||||
warningText = (
|
||||
<div className="flex flex-col gap-2 text-gray-400">
|
||||
<span>An error has occurred uploading the admin configuration.</span>
|
||||
<span></span>
|
||||
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@ -302,7 +302,7 @@ export function DrawingMenu(props: { open: boolean; onClose: () => void }) {
|
||||
>
|
||||
Automatic IADS generation
|
||||
</div>
|
||||
<OlDropdown className="" label="Units types">
|
||||
<OlDropdown className="" label="Units types" disableAutoClose={true}>
|
||||
{types.map((type, idx) => {
|
||||
if (!(type in typesSelection)) {
|
||||
typesSelection[type] = true;
|
||||
@ -323,7 +323,7 @@ export function DrawingMenu(props: { open: boolean; onClose: () => void }) {
|
||||
);
|
||||
})}
|
||||
</OlDropdown>
|
||||
<OlDropdown className="" label="Units eras">
|
||||
<OlDropdown className="" label="Units eras" disableAutoClose={true}>
|
||||
{eras.map((era) => {
|
||||
if (!(era in erasSelection)) {
|
||||
erasSelection[era] = true;
|
||||
@ -344,7 +344,7 @@ export function DrawingMenu(props: { open: boolean; onClose: () => void }) {
|
||||
);
|
||||
})}
|
||||
</OlDropdown>
|
||||
<OlDropdown className="" label="Units ranges">
|
||||
<OlDropdown className="" label="Units ranges" disableAutoClose={true}>
|
||||
{["Short range", "Medium range", "Long range"].map((range) => {
|
||||
if (!(range in rangesSelection)) {
|
||||
rangesSelection[range] = true;
|
||||
|
||||
@ -9,15 +9,18 @@ import { BindShortcutRequestEvent, MapOptionsChangedEvent, ShortcutsChangedEvent
|
||||
import { OlAccordion } from "../components/olaccordion";
|
||||
import { Shortcut } from "../../shortcut/shortcut";
|
||||
import { OlSearchBar } from "../components/olsearchbar";
|
||||
import { FaTrash, FaXmark } from "react-icons/fa6";
|
||||
import { FaTrash, FaUserGroup, FaXmark } from "react-icons/fa6";
|
||||
import { OlCoalitionToggle } from "../components/olcoalitiontoggle";
|
||||
import { FaQuestionCircle } from "react-icons/fa";
|
||||
import { FaCog, FaKey, FaPlus, FaQuestionCircle } from "react-icons/fa";
|
||||
import { sha256 } from "js-sha256";
|
||||
import { OlDropdown, OlDropdownItem } from "../components/oldropdown";
|
||||
|
||||
const enum Accordion {
|
||||
NONE,
|
||||
BINDINGS,
|
||||
MAP_OPTIONS,
|
||||
CAMERA_PLUGIN,
|
||||
ADMIN,
|
||||
}
|
||||
|
||||
export function OptionsMenu(props: { open: boolean; onClose: () => void; children?: JSX.Element | JSX.Element[] }) {
|
||||
@ -25,6 +28,31 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre
|
||||
const [shortcuts, setShortcuts] = useState({} as { [key: string]: Shortcut });
|
||||
const [openAccordion, setOpenAccordion] = useState(Accordion.NONE);
|
||||
const [filterString, setFilterString] = useState("");
|
||||
const [admin, setAdmin] = useState(false);
|
||||
const [password, setPassword] = useState("");
|
||||
|
||||
const checkPassword = (password: string) => {
|
||||
var hash = sha256.create();
|
||||
|
||||
const requestOptions: RequestInit = {
|
||||
method: "GET", // Specify the request method
|
||||
headers: {
|
||||
Authorization: "Basic " + btoa(`Admin:${hash.update(password).hex()}`),
|
||||
}, // Specify the content type
|
||||
};
|
||||
|
||||
fetch(`./admin/config`, requestOptions)
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
console.log(`Admin password correct`);
|
||||
getApp().setAdminPassword(password);
|
||||
getApp().setState(OlympusState.ADMIN)
|
||||
return response.json();
|
||||
} else {
|
||||
throw new Error("Admin password incorrect");
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
MapOptionsChangedEvent.on((mapOptions) => setMapOptions({ ...mapOptions }));
|
||||
@ -186,18 +214,14 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex content-center gap-4">
|
||||
<OlCoalitionToggle onClick={() => {}} coalition={mapOptions.AWACSCoalition} />
|
||||
<span className="my-auto">Coalition of unit bullseye info</span>
|
||||
</div>
|
||||
<div className="flex gap-1 text-sm text-gray-400">
|
||||
<FaQuestionCircle className={`my-auto w-8`} />{" "}
|
||||
<div
|
||||
className={`my-auto ml-2`}
|
||||
>
|
||||
Change the coalition of the bullseye to use to provide bullseye information in the unit tooltip.
|
||||
<div className="flex content-center gap-4">
|
||||
<OlCoalitionToggle onClick={() => {}} coalition={mapOptions.AWACSCoalition} />
|
||||
<span className="my-auto">Coalition of unit bullseye info</span>
|
||||
</div>
|
||||
<div className="flex gap-1 text-sm text-gray-400">
|
||||
<FaQuestionCircle className={`my-auto w-8`} />{" "}
|
||||
<div className={`my-auto ml-2`}>Change the coalition of the bullseye to use to provide bullseye information in the unit tooltip.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</OlAccordion>
|
||||
@ -207,12 +231,6 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre
|
||||
open={openAccordion === Accordion.CAMERA_PLUGIN}
|
||||
title="Camera plugin options"
|
||||
>
|
||||
<hr
|
||||
className={`
|
||||
m-2 my-1 w-auto border-[1px] bg-gray-700
|
||||
dark:border-olympus-500
|
||||
`}
|
||||
></hr>
|
||||
<div
|
||||
className={`
|
||||
flex flex-col content-center items-start justify-between gap-2 p-2
|
||||
@ -270,38 +288,65 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre
|
||||
</div>
|
||||
</OlAccordion>
|
||||
|
||||
<div className="mt-auto flex">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => getApp().resetProfile()}
|
||||
className={`
|
||||
flex w-full content-center items-center justify-center gap-2
|
||||
rounded-sm border-[1px] bg-blue-700 px-5 py-2.5 text-sm font-medium
|
||||
text-white
|
||||
dark:border-red-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
|
||||
`}
|
||||
>
|
||||
Reset all settings
|
||||
<FaXmark />
|
||||
</button>
|
||||
|
||||
<div className="mt-auto flex flex-col gap-2 p-2">
|
||||
<div className="flex content-center justify-between gap-4">
|
||||
<label
|
||||
className={`
|
||||
text-gray-800 text-md my-auto text-nowrap
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
Admin password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
onChange={(ev) => {
|
||||
setPassword(ev.currentTarget.value);
|
||||
}}
|
||||
className={`
|
||||
max-w-44 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 password"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => getApp().resetProfile()}
|
||||
onClick={() => checkPassword(password)}
|
||||
className={`
|
||||
mb-2 me-2 flex content-center items-center gap-2 rounded-sm
|
||||
flex content-center items-center justify-center gap-2 rounded-sm
|
||||
border-[1px] bg-blue-700 px-5 py-2.5 text-sm font-medium
|
||||
text-white
|
||||
dark:border-red-600 dark:bg-gray-800 dark:text-gray-400
|
||||
dark:border-white 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
|
||||
`}
|
||||
>
|
||||
Reset profile
|
||||
<FaXmark />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => getApp().resetAllProfiles()}
|
||||
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-red-600 dark:bg-red-800 dark:text-gray-400
|
||||
dark:hover:bg-red-700 dark:focus:ring-blue-800
|
||||
focus:outline-none focus:ring-4 focus:ring-blue-300
|
||||
hover:bg-red-800
|
||||
`}
|
||||
>
|
||||
Reset all profiles
|
||||
<FaTrash />
|
||||
Open advanced settings menu
|
||||
<FaCog />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -32,6 +32,7 @@ import { ServerOverlay } from "./serveroverlay";
|
||||
import { ImportExportModal } from "./modals/importexportmodal";
|
||||
import { WarningModal } from "./modals/warningmodal";
|
||||
import { TrainingModal } from "./modals/trainingmodal";
|
||||
import { AdminModal } from "./modals/adminmodal";
|
||||
|
||||
export function UI() {
|
||||
const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED);
|
||||
@ -75,6 +76,7 @@ export function UI() {
|
||||
<LoginModal open={appState === OlympusState.LOGIN} />
|
||||
<WarningModal open={appState === OlympusState.WARNING} />
|
||||
<TrainingModal open={appState === OlympusState.TRAINING} />
|
||||
<AdminModal open={appState === OlympusState.ADMIN} />
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@ -4,7 +4,8 @@
|
||||
"version": "{{OLYMPUS_VERSION_NUMBER}}",
|
||||
"scripts": {
|
||||
"build-release": "call ./scripts/build-release.bat",
|
||||
"server": "electron . --server",
|
||||
"server-electron": "electron . --server",
|
||||
"server": "node ./build/www.js",
|
||||
"client": "electron .",
|
||||
"tsc": "tsc"
|
||||
},
|
||||
|
||||
@ -51,6 +51,7 @@ module.exports = function (configLocation, viteProxy) {
|
||||
/* Config specific routers */
|
||||
const elevationRouter = require("./routes/api/elevation")(configLocation);
|
||||
const resourcesRouter = require("./routes/resources")(configLocation);
|
||||
const adminRouter = require("./routes/admin")(configLocation);
|
||||
|
||||
/* Default routers */
|
||||
const airbasesRouter = require("./routes/api/airbases");
|
||||
@ -113,6 +114,9 @@ module.exports = function (configLocation, viteProxy) {
|
||||
"Blue commander": config["authentication"]["blueCommanderPassword"],
|
||||
"Red commander": config["authentication"]["redCommanderPassword"],
|
||||
};
|
||||
if (config["authentication"]["adminPassword"]) {
|
||||
defaultUsers["Admin"] = config["authentication"]["adminPassword"];
|
||||
}
|
||||
let users = {};
|
||||
Object.keys(usersConfig).forEach(
|
||||
(user) => (users[user] = usersConfig[user].password)
|
||||
@ -122,7 +126,9 @@ module.exports = function (configLocation, viteProxy) {
|
||||
});
|
||||
|
||||
/* Define middleware */
|
||||
app.use(logger("dev"));
|
||||
app.use(logger('dev', {
|
||||
skip: function (req, res) { return res.statusCode < 400 }
|
||||
}));
|
||||
|
||||
/* Authorization middleware */
|
||||
if (
|
||||
@ -238,6 +244,9 @@ module.exports = function (configLocation, viteProxy) {
|
||||
app.use("/api/speech", speechRouter);
|
||||
app.use("/resources", resourcesRouter);
|
||||
|
||||
app.use("/admin", auth);
|
||||
app.use("/admin", adminRouter);
|
||||
|
||||
/* Set default index */
|
||||
if (viteProxy) {
|
||||
app.use(
|
||||
|
||||
100
frontend/server/src/routes/admin.ts
Normal file
100
frontend/server/src/routes/admin.ts
Normal file
@ -0,0 +1,100 @@
|
||||
import express = require("express");
|
||||
import fs = require("fs");
|
||||
import path = require("path");
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
module.exports = function (configLocation) {
|
||||
router.get("/config", function (req, res, next) {
|
||||
if (req.auth?.user === "Admin") {
|
||||
/* 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);
|
||||
}
|
||||
|
||||
res.send({ users: usersConfig, groups: groupsConfig });
|
||||
res.end();
|
||||
} else {
|
||||
res.sendStatus(401);
|
||||
}
|
||||
});
|
||||
|
||||
router.put("/config", function (req, res, next) {
|
||||
if (req.auth?.user === "Admin") {
|
||||
/* Create a backup folder for the configuration files */
|
||||
let backupFolder = path.join(path.dirname(configLocation), "Olympus Configs Backup");
|
||||
if (!fs.existsSync(backupFolder)) {
|
||||
fs.mkdirSync(backupFolder);
|
||||
}
|
||||
|
||||
/* Make a backup of the existing files */
|
||||
let timestamp = new Date().toISOString().replace(/:/g, "-");
|
||||
fs.copyFileSync(
|
||||
path.join(path.dirname(configLocation), "olympusUsers.json"),
|
||||
path.join(
|
||||
path.dirname(configLocation),
|
||||
"Olympus Configs Backup",
|
||||
"olympusUsers.json." + timestamp
|
||||
)
|
||||
);
|
||||
fs.copyFileSync(
|
||||
path.join(path.dirname(configLocation), "olympusGroups.json"),
|
||||
path.join(
|
||||
path.dirname(configLocation),
|
||||
"Olympus Configs Backup",
|
||||
"olympusGroups.json." + timestamp
|
||||
)
|
||||
);
|
||||
|
||||
/* Save the users configuration file */
|
||||
let usersConfig = req.body.users;
|
||||
|
||||
if (usersConfig) {
|
||||
fs.writeFileSync(
|
||||
path.join(path.dirname(configLocation), "olympusUsers.json"),
|
||||
JSON.stringify(usersConfig, null, 2)
|
||||
);
|
||||
}
|
||||
|
||||
/* Save the groups configuration file */
|
||||
let groupsConfig = req.body.groups;
|
||||
|
||||
if (groupsConfig) {
|
||||
fs.writeFileSync(
|
||||
path.join(path.dirname(configLocation), "olympusGroups.json"),
|
||||
JSON.stringify(groupsConfig, null, 2)
|
||||
);
|
||||
}
|
||||
|
||||
res.send({ users: usersConfig, groups: groupsConfig });
|
||||
res.end();
|
||||
} else {
|
||||
res.sendStatus(401);
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
};
|
||||
@ -19,19 +19,25 @@
|
||||
<span>Game Master Password<img src="./icons/circle-info-solid.svg"
|
||||
title="This password is used to access Olympus as Game Master with full privileges.">
|
||||
</span>
|
||||
<input type="password" minlength="8" onchange="signal('onGameMasterPasswordChanged', this.value)" placeholder="<%= !activeInstance["installed"] || activeInstance["gameMasterPasswordEdited"]? '': 'Keep old password'%>">
|
||||
<input type="password" class="unique" minlength="8" onchange="signal('onGameMasterPasswordChanged', this.value)" placeholder="<%= !activeInstance["installed"] || activeInstance["gameMasterPasswordEdited"]? '': 'Keep old password'%>">
|
||||
</div>
|
||||
<div class="input-group blue-commander">
|
||||
<span>Blue Commander Password<img src="./icons/circle-info-solid.svg"
|
||||
title="This password is used to access Olympus as blue coalition Commander.">
|
||||
</span>
|
||||
<input type="password" minlength="8" onchange="signal('onBlueCommanderPasswordChanged', this.value)" placeholder="<%= !activeInstance["installed"] || activeInstance["blueCommanderPasswordEdited"]? '': 'Keep old password'%>">
|
||||
<input type="password" class="unique" minlength="8" onchange="signal('onBlueCommanderPasswordChanged', this.value)" placeholder="<%= !activeInstance["installed"] || activeInstance["blueCommanderPasswordEdited"]? '': 'Keep old password'%>">
|
||||
</div>
|
||||
<div class="input-group red-commander">
|
||||
<span>Red Commander Password<img src="./icons/circle-info-solid.svg"
|
||||
title="This password is used to access Olympus as red coalition Commander.">
|
||||
</span>
|
||||
<input type="password" minlength="8" onchange="signal('onRedCommanderPasswordChanged', this.value)" placeholder="<%= !activeInstance["installed"] || activeInstance["redCommanderPasswordEdited"]? '': 'Keep old password'%>">
|
||||
<input type="password" class="unique" minlength="8" onchange="signal('onRedCommanderPasswordChanged', this.value)" placeholder="<%= !activeInstance["installed"] || activeInstance["redCommanderPasswordEdited"]? '': 'Keep old password'%>">
|
||||
</div>
|
||||
<div class="input-group admin-password">
|
||||
<span>Admin Password<img src="./icons/circle-info-solid.svg"
|
||||
title="This password is used to set global Olympus configurations, like user access privileges.">
|
||||
</span>
|
||||
<input type="password" minlength="8" onchange="signal('onAdminPasswordChanged', this.value)" placeholder="<%= !activeInstance["installed"] || activeInstance["adminPasswordEdited"]? '': 'Keep old password'%>">
|
||||
</div>
|
||||
<div class="<%= activeInstance["installed"]? '': 'hide' %>" style="color: var(--offwhite); font-size: var(--normal); color: var(--lightgray);">
|
||||
Note: to keep the old passwords, click <b>Next</b> without editing any value.
|
||||
|
||||
@ -1,5 +1,19 @@
|
||||
<style>
|
||||
.wizard-page #passwords-page .wizard-inputs {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.wizard-page #passwords-page .wizard-inputs>div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 10px;
|
||||
}
|
||||
|
||||
.wizard-page #passwords-page .wizard-inputs {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
</style>
|
||||
<div id="passwords-page">
|
||||
<div class="instructions">
|
||||
@ -15,33 +29,43 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="wizard-inputs">
|
||||
<div>
|
||||
<div class="input-group game-master">
|
||||
<span>Game Master Password<img src="./icons/circle-info-solid.svg"
|
||||
title="This password is used to access Olympus as Game Master with full privileges.">
|
||||
</span>
|
||||
<input type="password" minlength="8" onchange="signal('onGameMasterPasswordChanged', this.value)" placeholder="<%= state === 'INSTALL' || activeInstance["gameMasterPasswordEdited"]? '': 'Keep old password'%>">
|
||||
<input type="password" class="unique" minlength="8" onchange="signal('onGameMasterPasswordChanged', this.value)" placeholder="<%= state === 'INSTALL' || activeInstance["gameMasterPasswordEdited"]? '': 'Keep old password'%>">
|
||||
</div>
|
||||
<div class="input-group blue-commander">
|
||||
<span>Blue Commander Password<img src="./icons/circle-info-solid.svg"
|
||||
title="This password is used to access Olympus as blue coalition Commander.">
|
||||
</span>
|
||||
<input type="password" minlength="8" onchange="signal('onBlueCommanderPasswordChanged', this.value)" placeholder="<%= state === 'INSTALL' || activeInstance["blueCommanderPasswordEdited"]? '': 'Keep old password'%>">
|
||||
<input type="password" class="unique" minlength="8" onchange="signal('onBlueCommanderPasswordChanged', this.value)" placeholder="<%= state === 'INSTALL' || activeInstance["blueCommanderPasswordEdited"]? '': 'Keep old password'%>">
|
||||
</div>
|
||||
<div class="input-group red-commander">
|
||||
<span>Red Commander Password<img src="./icons/circle-info-solid.svg"
|
||||
title="This password is used to access Olympus as red coalition Commander.">
|
||||
</span>
|
||||
<input type="password" minlength="8" onchange="signal('onRedCommanderPasswordChanged', this.value)" placeholder="<%= state === 'INSTALL' || activeInstance["redCommanderPasswordEdited"]? '': 'Keep old password'%>">
|
||||
<input type="password" class="unique" minlength="8" onchange="signal('onRedCommanderPasswordChanged', this.value)" placeholder="<%= state === 'INSTALL' || activeInstance["redCommanderPasswordEdited"]? '': 'Keep old password'%>">
|
||||
</div>
|
||||
<div class="<%= state !== 'INSTALL'? '': 'hide' %>" style="color: var(--offwhite); font-size: var(--normal); color: var(--lightgray);">
|
||||
Note: to keep the old passwords, click <b>Next</b> without editing any value.
|
||||
</div>
|
||||
<div class="input-group autoconnect">
|
||||
<span onclick="signal('onEnableAutoconnectClicked')">
|
||||
<div class="checkbox checked"></div> Autoconnect when local
|
||||
<div class="checkbox <%= activeInstance['installationType'] === 'multiplayer'? '': 'checked' %>"></div> Autoconnect when local
|
||||
<img src="./icons/circle-info-solid.svg"
|
||||
title="Autoconnect as Game Master when running Olympus on the same computer as DCS.">
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="input-group admin-password <%= activeInstance['installationType'] === 'multiplayer'? '': 'hide' %>">
|
||||
<span>Admin Password<img src="./icons/circle-info-solid.svg"
|
||||
title="This password is used to set global Olympus configurations, like user access privileges.">
|
||||
</span>
|
||||
<input type="password" minlength="8" onchange="signal('onAdminPasswordChanged', this.value)" placeholder="<%= state === 'INSTALL' || activeInstance["adminPasswordEdited"]? '': 'Keep old password'%>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -136,6 +136,7 @@ class DCSInstance {
|
||||
blueCommanderPassword = "";
|
||||
redCommanderPassword = "";
|
||||
gameMasterPasswordHash = "";
|
||||
adminPassword = "";
|
||||
installed = false;
|
||||
error = false;
|
||||
webserverOnline = false;
|
||||
@ -149,6 +150,7 @@ class DCSInstance {
|
||||
gameMasterPasswordEdited = false;
|
||||
blueCommanderPasswordEdited = false;
|
||||
redCommanderPasswordEdited = false;
|
||||
adminPasswordEdited = false;
|
||||
autoconnectWhenLocal = false;
|
||||
SRSPort = 5002;
|
||||
|
||||
@ -196,6 +198,7 @@ class DCSInstance {
|
||||
this.gameMasterPasswordEdited = false;
|
||||
this.blueCommanderPasswordEdited = false;
|
||||
this.redCommanderPasswordEdited = false;
|
||||
this.adminPasswordEdited = false;
|
||||
|
||||
} catch (err) {
|
||||
showErrorPopup(`<div class='main-message'>A critical error has occurred while reading your Olympus configuration file. </div><div class='sub-message'> Please manually reinstall Olympus in ${this.folder} using either the installation Wizard or the Expert view. </div>`)
|
||||
@ -277,7 +280,7 @@ class DCSInstance {
|
||||
|
||||
/** Set Blue Commander password
|
||||
*
|
||||
* @param {String} newAddress The new Blue Commander password to set
|
||||
* @param {String} newPassword The new Blue Commander password to set
|
||||
*/
|
||||
setBlueCommanderPassword(newPassword) {
|
||||
this.blueCommanderPassword = newPassword;
|
||||
@ -286,13 +289,22 @@ class DCSInstance {
|
||||
|
||||
/** Set Red Commander password
|
||||
*
|
||||
* @param {String} newAddress The new Red Commander password to set
|
||||
* @param {String} newPassword The new Red Commander password to set
|
||||
*/
|
||||
setRedCommanderPassword(newPassword) {
|
||||
this.redCommanderPassword = newPassword;
|
||||
this.redCommanderPasswordEdited = true;
|
||||
}
|
||||
|
||||
/** Set Admin password
|
||||
*
|
||||
* @param {String} newPassword The new Admin password to set
|
||||
*/
|
||||
setAdminPassword(newPassword) {
|
||||
this.adminPassword = newPassword;
|
||||
this.adminPasswordEdited = true;
|
||||
}
|
||||
|
||||
/** Checks if any password has been edited by the user
|
||||
*
|
||||
* @returns true if any password was edited
|
||||
@ -306,7 +318,10 @@ class DCSInstance {
|
||||
* @returns true if all the password have been set
|
||||
*/
|
||||
arePasswordsSet() {
|
||||
return !(getManager().getActiveInstance().gameMasterPassword === '' || getManager().getActiveInstance().blueCommanderPassword === '' || getManager().getActiveInstance().redCommanderPassword === '');
|
||||
if (getManager().getActiveInstance().installationType === "singleplayer")
|
||||
return !(getManager().getActiveInstance().gameMasterPassword === '' || getManager().getActiveInstance().blueCommanderPassword === '' || getManager().getActiveInstance().redCommanderPassword === '');
|
||||
else
|
||||
return !(getManager().getActiveInstance().gameMasterPassword === '' || getManager().getActiveInstance().blueCommanderPassword === '' || getManager().getActiveInstance().redCommanderPassword === '' || getManager().getActiveInstance().adminPassword === '');
|
||||
}
|
||||
|
||||
/** Checks if all the passwords are different
|
||||
|
||||
@ -181,6 +181,9 @@ async function applyConfiguration(folder, instance) {
|
||||
if (instance.redCommanderPassword !== "")
|
||||
config["authentication"]["redCommanderPassword"] = sha256(instance.redCommanderPassword);
|
||||
|
||||
if (instance.adminPassword !== "")
|
||||
config["authentication"]["adminPassword"] = sha256(instance.adminPassword);
|
||||
|
||||
await fsp.writeFile(path.join(folder, "Config", "olympus.json"), JSON.stringify(config, null, 4));
|
||||
logger.log(`Config succesfully applied in ${folder}`)
|
||||
} else {
|
||||
|
||||
@ -475,7 +475,7 @@ class Manager {
|
||||
}
|
||||
|
||||
async onGameMasterPasswordChanged(value) {
|
||||
for (let input of this.activePage.getElement().querySelectorAll("input[type='password']")) {
|
||||
for (let input of this.activePage.getElement().querySelectorAll("input[type='password'].unique")) {
|
||||
input.placeholder = "";
|
||||
}
|
||||
|
||||
@ -486,7 +486,7 @@ class Manager {
|
||||
}
|
||||
|
||||
async onBlueCommanderPasswordChanged(value) {
|
||||
for (let input of this.activePage.getElement().querySelectorAll("input[type='password']")) {
|
||||
for (let input of this.activePage.getElement().querySelectorAll("input[type='password'].unique")) {
|
||||
input.placeholder = "";
|
||||
}
|
||||
|
||||
@ -497,7 +497,7 @@ class Manager {
|
||||
}
|
||||
|
||||
async onRedCommanderPasswordChanged(value) {
|
||||
for (let input of this.activePage.getElement().querySelectorAll("input[type='password']")) {
|
||||
for (let input of this.activePage.getElement().querySelectorAll("input[type='password'].unique")) {
|
||||
input.placeholder = "";
|
||||
}
|
||||
|
||||
@ -507,6 +507,13 @@ class Manager {
|
||||
showErrorPopup(`<div class='main-message'>A critical error occurred! </div><div class='sub-message'> Check ${this.getLogLocation()} for more info. </div>`);
|
||||
}
|
||||
|
||||
async onAdminPasswordChanged(value) {
|
||||
if (this.getActiveInstance())
|
||||
this.getActiveInstance().setAdminPassword(value);
|
||||
else
|
||||
showErrorPopup(`<div class='main-message'>A critical error occurred! </div><div class='sub-message'> Check ${this.getLogLocation()} for more info. </div>`);
|
||||
}
|
||||
|
||||
/* When the frontend port input value is changed */
|
||||
async onFrontendPortChanged(value) {
|
||||
this.setPort('frontend', Number(value));
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user