feat: added admin password and admin modal

This commit is contained in:
Davide Passoni 2025-03-10 17:16:02 +01:00
parent 9a7af84cd4
commit 386d5298a2
16 changed files with 645 additions and 61 deletions

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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