From 57b74bd1b17086f8f2e189b065dea99bda6e0aaf Mon Sep 17 00:00:00 2001 From: Pax1601 Date: Tue, 9 May 2023 15:41:04 +0200 Subject: [PATCH] Added frontend code for authentication --- client/app.js | 7 ++ client/package-lock.json | 19 ++++ client/package.json | 1 + client/public/stylesheets/layout.css | 7 +- client/public/stylesheets/olympus.css | 81 ++++++++++++-- client/src/featureswitches.ts | 14 +-- client/src/index.ts | 145 ++++++++------------------ client/src/map/map.ts | 2 +- client/src/server/server.ts | 135 ++++++++++++++++++++---- client/views/dialogs.ejs | 19 ++-- client/views/index.ejs | 2 + client/views/navbar.ejs | 7 +- 12 files changed, 286 insertions(+), 153 deletions(-) diff --git a/client/app.js b/client/app.js index 7c2ad473..9b65e625 100644 --- a/client/app.js +++ b/client/app.js @@ -3,6 +3,7 @@ var path = require('path'); var cookieParser = require('cookie-parser'); var logger = require('morgan'); var fs = require('fs'); +var basicAuth = require('express-basic-auth') var atcRouter = require('./routes/api/atc'); var indexRouter = require('./routes/index'); @@ -11,6 +12,10 @@ var usersRouter = require('./routes/users'); var app = express(); +app.use('/demo', basicAuth({ + users: { 'admin': 'socks' } +})) + app.use(logger('dev')); app.use(express.json()); app.use(express.urlencoded({ extended: false })); @@ -38,3 +43,5 @@ app.get('/demo/bullseyes', (req, res) => demoDataGenerator.bullseyes(req, res)); app.get('/demo/airbases', (req, res) => demoDataGenerator.airbases(req, res)); app.get('/demo/mission', (req, res) => demoDataGenerator.mission(req, res)); + + diff --git a/client/package-lock.json b/client/package-lock.json index e7858b9a..2f89a883 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -31,6 +31,7 @@ "browserify": "^17.0.0", "concurrently": "^7.6.0", "esmify": "^2.1.1", + "express-basic-auth": "^1.2.1", "nodemon": "^2.0.20", "sortablejs": "^1.15.0", "tsify": "^5.0.4", @@ -3283,6 +3284,15 @@ "node": ">= 0.10.0" } }, + "node_modules/express-basic-auth": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/express-basic-auth/-/express-basic-auth-1.2.1.tgz", + "integrity": "sha512-L6YQ1wQ/mNjVLAmK3AG1RK6VkokA1BIY6wmiH304Xtt/cLTps40EusZsU1Uop+v9lTDPxdtzbFmdXfFO3KEnwA==", + "dev": true, + "dependencies": { + "basic-auth": "^2.0.1" + } + }, "node_modules/express/node_modules/cookie": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", @@ -8148,6 +8158,15 @@ } } }, + "express-basic-auth": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/express-basic-auth/-/express-basic-auth-1.2.1.tgz", + "integrity": "sha512-L6YQ1wQ/mNjVLAmK3AG1RK6VkokA1BIY6wmiH304Xtt/cLTps40EusZsU1Uop+v9lTDPxdtzbFmdXfFO3KEnwA==", + "dev": true, + "requires": { + "basic-auth": "^2.0.1" + } + }, "fast-safe-stringify": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", diff --git a/client/package.json b/client/package.json index 40823578..47fe7cd0 100644 --- a/client/package.json +++ b/client/package.json @@ -33,6 +33,7 @@ "browserify": "^17.0.0", "concurrently": "^7.6.0", "esmify": "^2.1.1", + "express-basic-auth": "^1.2.1", "nodemon": "^2.0.20", "sortablejs": "^1.15.0", "tsify": "^5.0.4", diff --git a/client/public/stylesheets/layout.css b/client/public/stylesheets/layout.css index ff8e8a54..5c60d1bb 100644 --- a/client/public/stylesheets/layout.css +++ b/client/public/stylesheets/layout.css @@ -16,12 +16,13 @@ #olympus-toolbar-summary { background-image: url("/images/icon-round.png"); - background-position: 25px 20px; + background-position: 20px 22px; background-repeat: no-repeat; - background-size: 36px 36px; + background-size: 45px 45px; display: flex; flex-direction: column; - text-indent: 44px; + text-indent: 60px; + padding: 20px; } dl.ol-data-grid { diff --git a/client/public/stylesheets/olympus.css b/client/public/stylesheets/olympus.css index 1d2aed0b..d7905e9b 100644 --- a/client/public/stylesheets/olympus.css +++ b/client/public/stylesheets/olympus.css @@ -251,13 +251,14 @@ form>div { text-align: left; white-space: nowrap; width: 100%; - padding: 10px; + padding: 5px; border-radius: var(--border-radius-sm); } .ol-select>.ol-select-options>div a:hover, .ol-select>.ol-select-options>div button:hover { background-color: #FFF3; + text-decoration: none; } .ol-panel-list { @@ -725,21 +726,21 @@ body[data-hide-navyunit] #unit-visibility-control-navyunit { #splash-screen { background-image: url("/images/splash/splash_pic_ship.png"); background-position: 100% 50%; - background-size: 320px; + background-size: 60%; border-radius: var(--border-radius-lg); - display: none; overflow: hidden; - width: 700px; + width: 1200px; + z-index: 99999; } #splash-content { background-color: var(--background-steel); display: flex; flex-direction: column; - padding: 20px; + padding: 30px; position: relative; row-gap: 10px; - width: 55%; + width: 50%; z-index: 10; } @@ -747,7 +748,7 @@ body[data-hide-navyunit] #unit-visibility-control-navyunit { background-color: var(--background-steel); content: ""; display: block; - height: 250px; + height: 800px; position: absolute; right: 0; top: 0; @@ -775,20 +776,84 @@ body[data-hide-navyunit] #unit-visibility-control-navyunit { line-height: 25px; white-space: nowrap; width: fit-content; + padding: 2px; } #splash-content .app-version { font-size: 11px; } -#splash-content #legal-stuff h4 { +#splash-content #legal-stuff h5 { text-transform: uppercase; } #splash-content #legal-stuff p { font-size: 10px; + color:#FFF7; + width: 120%; +} + +#splash-content.ol-dialog-content { + margin: 0px; } .feature-splashScreen #splash-screen { display: flex; +} + +#gray-out { + position: fixed; + height: 100%; + width: 100%; + left: 0px; + top: 0px; + z-index: 9999; + background-color: #000A; +} + +#authentication-form { + display: flex; + align-items: end; + column-gap: 10px; + margin: 10px 0px; + flex-direction: row; +} + +#authentication-form>div { + display: flex; + align-items: start; + row-gap: 4px; + flex-direction: column; +} + +#authentication-form>div>input { + height: 35px; + border-radius: var(--border-radius-sm); + border: 0px solid transparent; + width: 200px; +} + +#splash-content a { + color: #FFFB; + font-weight: bold; +} + +#connection-status { + margin-bottom: 5px; +} + +#connection-status[data-status="connecting"]::before { + content: "Connecting..."; + animation: blinker 1s linear infinite; +} + +#connection-status[data-status="failed"]::before { + content: "Incorrect username/password!"; + color: var(--primary-red); +} + +@keyframes blinker { + 50% { + opacity: 0; + } } \ No newline at end of file diff --git a/client/src/featureswitches.ts b/client/src/featureswitches.ts index ed52d8eb..c5776683 100644 --- a/client/src/featureswitches.ts +++ b/client/src/featureswitches.ts @@ -90,7 +90,7 @@ export class FeatureSwitches { }), new FeatureSwitch({ - "defaultEnabled": false, + "defaultEnabled": true, "label": "Show splash screen", "masterSwitch": true, "name": "splashScreen" @@ -116,36 +116,24 @@ export class FeatureSwitches { #testSwitches() { - for ( const featureSwitch of this.#featureSwitches ) { - if ( featureSwitch.isEnabled() ) { - if ( typeof featureSwitch.onEnabled === "function" ) { featureSwitch.onEnabled(); } - } else { - document.querySelectorAll( "[data-feature-switch='" + featureSwitch.name + "']" ).forEach( el => { - if ( featureSwitch.removeArtifactsIfDisabled === false ) { el.remove(); } else { el.classList.add( "hide" ); } - }); - } - document.body.classList.toggle( "feature-" + featureSwitch.name, featureSwitch.isEnabled() ); - } - } - savePreferences() { let preferences:any = {}; diff --git a/client/src/index.ts b/client/src/index.ts index d7843625..5e41f88b 100644 --- a/client/src/index.ts +++ b/client/src/index.ts @@ -9,7 +9,7 @@ import { AIC } from "./aic/aic"; import { ATC } from "./atc/atc"; import { FeatureSwitches } from "./featureswitches"; import { LogPanel } from "./panels/logpanel"; -import { getAirbases, getBullseye as getBullseyes, getConfig, getMission, getUnits, setAddress, toggleDemoEnabled } from "./server/server"; +import { getAirbases, getBullseye, getConfig, getFreezed, getMission, getUnits, setAddress, setCredentials, setFreezed, startUpdate, toggleDemoEnabled } from "./server/server"; import { UnitDataTable } from "./units/unitdatatable"; import { keyEventWasInInput } from "./other/utils"; import { Popup } from "./popups/popup"; @@ -31,11 +31,8 @@ var logPanel: LogPanel; var infoPopup: Popup; -var connected: boolean = false; -var paused: boolean = false; var activeCoalition: string = "blue"; -var sessionHash: string | null = null; var unitDataTable: UnitDataTable; var featureSwitches; @@ -49,15 +46,18 @@ function setup() { missionHandler = new MissionHandler(); /* Panels */ - unitInfoPanel = new UnitInfoPanel("unit-info-panel"); - unitControlPanel = new UnitControlPanel("unit-control-panel"); - connectionStatusPanel = new ConnectionStatusPanel("connection-status-panel"); - mouseInfoPanel = new MouseInfoPanel("mouse-info-panel"); + unitInfoPanel = new UnitInfoPanel("unit-info-panel"); + unitControlPanel = new UnitControlPanel("unit-control-panel"); + connectionStatusPanel = new ConnectionStatusPanel("connection-status-panel"); + mouseInfoPanel = new MouseInfoPanel("mouse-info-panel"); //logPanel = new LogPanel("log-panel"); /* Popups */ infoPopup = new Popup("info-popup"); + /* Controls */ + new Dropdown("app-icon", () => { }); + /* Unit data table */ unitDataTable = new UnitDataTable("unit-data-table"); @@ -65,7 +65,6 @@ function setup() { let aicFeatureSwitch = featureSwitches.getSwitch("aic"); if (aicFeatureSwitch?.isEnabled()) { aic = new AIC(); - // TODO: add back buttons } /* ATC */ @@ -75,82 +74,23 @@ function setup() { atc.startUpdates(); } - new Dropdown( "app-icon", () => {} ); - /* Setup event handlers */ setupEvents(); - getConfig(readConfig) + /* Load the config file */ + getConfig(readConfig); } -function readConfig(config: any) -{ - if (config && config["server"] != undefined && config["server"]["address"] != undefined && config["server"]["port"] != undefined) - { +function readConfig(config: any) { + if (config && config["server"] != undefined && config["server"]["address"] != undefined && config["server"]["port"] != undefined) { const address = config["server"]["address"]; const port = config["server"]["port"]; if (typeof address === 'string' && typeof port == 'number') - setAddress(address == "*"? window.location.hostname: address, port); - - /* On the first connection, force request of full data */ - getAirbases((data: AirbasesData) => getMissionData()?.update(data)); - getBullseyes((data: BullseyesData) => getMissionData()?.update(data)); - getMission((data: any) => {getMissionData()?.update(data)}); - getUnits((data: UnitsData) => getUnitsManager()?.update(data), true /* Does a full refresh */); - - /* Start periodically requesting updates */ - startPeriodicUpdate(); + setAddress(address == "*" ? window.location.hostname : address, port); } else { throw new Error('Could not read configuration file!'); - } -} - -function startPeriodicUpdate() { - requestUpdate(); - requestRefresh(); -} - -function requestUpdate() { - /* Main update rate = 250ms is minimum time, equal to server update time. */ - getUnits((data: UnitsData) => { - if (!getPaused()){ - getUnitsManager()?.update(data); - checkSessionHash(data.sessionHash); - } - }, false); - window.setTimeout(() => requestUpdate(), getConnected() ? 250 : 1000); - - getConnectionStatusPanel()?.update(getConnected()); -} - -function requestRefresh() { - /* Main refresh rate = 5000ms. */ - getUnits((data: UnitsData) => { - if (!getPaused()){ - getUnitsManager()?.update(data); - getAirbases((data: AirbasesData) => getMissionData()?.update(data)); - getBullseyes((data: BullseyesData) => getMissionData()?.update(data)); - getMission((data: any) => { - getMissionData()?.update(data) - }); - - // Update the list of existing units - getUnitDataTable()?.update(); - - checkSessionHash(data.sessionHash); - } - }, true); - window.setTimeout(() => requestRefresh(), 5000); -} - -function checkSessionHash(newSessionHash: string) { - if (sessionHash != null) { - if (newSessionHash != sessionHash) - location.reload(); } - else - sessionHash = newSessionHash; } function setupEvents() { @@ -164,7 +104,7 @@ function setupEvents() { } const triggerElement = target.closest("[data-on-click]"); - + if (triggerElement instanceof HTMLElement) { const eventName: string = triggerElement.dataset.onClick || ""; let params = JSON.parse(triggerElement.dataset.onClickParams || "{}"); @@ -181,7 +121,7 @@ function setupEvents() { /* Keyup events */ document.addEventListener("keyup", ev => { - if ( keyEventWasInInput( ev ) ) { + if (keyEventWasInInput(ev)) { return; } switch (ev.code) { @@ -195,13 +135,13 @@ function setupEvents() { unitDataTable.toggle(); break case "Space": - setPaused(!getPaused()); + setFreezed(!getFreezed()); break; case "KeyW": case "KeyA": case "KeyS": case "KeyD": - case "ArrowLeft": + case "ArrowLeft": case "ArrowRight": case "ArrowUp": case "ArrowDown": @@ -212,7 +152,7 @@ function setupEvents() { /* Keydown events */ document.addEventListener("keydown", ev => { - if ( keyEventWasInInput( ev ) ) { + if (keyEventWasInInput(ev)) { return; } switch (ev.code) { @@ -220,7 +160,7 @@ function setupEvents() { case "KeyA": case "KeyS": case "KeyD": - case "ArrowLeft": + case "ArrowLeft": case "ArrowRight": case "ArrowUp": case "ArrowDown": @@ -229,15 +169,31 @@ function setupEvents() { } }); - document.addEventListener( "closeDialog", (ev: CustomEventInit) => { - ev.detail._element.closest( ".ol-dialog" ).classList.add( "hide" ); + document.addEventListener("closeDialog", (ev: CustomEventInit) => { + ev.detail._element.closest(".ol-dialog").classList.add("hide"); }); - document.addEventListener( "toggleElements", (ev: CustomEventInit) => { - document.querySelectorAll( ev.detail.selector ).forEach( el => { - el.classList.toggle( "hide" ); + document.addEventListener("toggleElements", (ev: CustomEventInit) => { + document.querySelectorAll(ev.detail.selector).forEach(el => { + el.classList.toggle("hide"); }) }); + + document.addEventListener("tryConnection", () => { + const form = document.querySelector("#splash-content")?.querySelector("#authentication-form"); + const username = ( (form?.querySelector("#username"))).value; + const password = ( (form?.querySelector("#password"))).value; + setCredentials(username, btoa("admin" + ":" + password)); + + /* Start periodically requesting updates */ + startUpdate(); + + setConnectionStatus("connecting"); + }) + + document.addEventListener("reloadPage", () => { + location.reload(); + }) } export function getMap() { @@ -285,23 +241,10 @@ export function getActiveCoalition() { return activeCoalition; } -export function setConnected(newConnected: boolean) { - if (connected != newConnected) - newConnected? getInfoPopup().setText("Connected to DCS Olympus server"): getInfoPopup().setText("Disconnected from DCS Olympus server"); - connected = newConnected; -} - -export function getConnected() { - return connected; -} - -export function setPaused(newPaused: boolean) { - paused = newPaused; - paused? getInfoPopup().setText("Paused"): getInfoPopup().setText("Unpaused"); -} - -export function getPaused() { - return paused; +export function setConnectionStatus(status: string) { + const el = document.querySelector("#connection-status") as HTMLElement; + if (el) + el.dataset["status"] = status; } export function getInfoPopup() { diff --git a/client/src/map/map.ts b/client/src/map/map.ts index c7bfd727..a531e33a 100644 --- a/client/src/map/map.ts +++ b/client/src/map/map.ts @@ -316,7 +316,7 @@ export class Map extends L.Map { } this.setView(bounds.getCenter(), 8); - this.setMaxBounds(bounds); + //this.setMaxBounds(bounds); if (this.#miniMap) this.#miniMap.remove(); diff --git a/client/src/server/server.ts b/client/src/server/server.ts index c1cd1420..92c5b2d2 100644 --- a/client/src/server/server.ts +++ b/client/src/server/server.ts @@ -1,7 +1,10 @@ import * as L from 'leaflet' -import { setConnected } from '..'; +import { getConnectionStatusPanel, getInfoPopup, getMissionData, getUnitDataTable, getUnitsManager, setConnectionStatus } from '..'; import { SpawnOptions } from '../controls/mapcontextmenu'; +var connected: boolean = false; +var freezed: boolean = false; + var REST_ADDRESS = "http://localhost:30000/olympus"; var DEMO_ADDRESS = window.location.href + "demo"; const UNITS_URI = "units"; @@ -10,29 +13,46 @@ const AIRBASES_URI = "airbases"; const BULLSEYE_URI = "bullseyes"; const MISSION_URI = "mission"; +var username = ""; +var credentials = ""; + +var sessionHash: string | null = null; var lastUpdateTime = 0; var demoEnabled = false; -export function toggleDemoEnabled() -{ +export function toggleDemoEnabled() { demoEnabled = !demoEnabled; } -export function GET(callback: CallableFunction, uri: string, options?: string){ +export function setCredentials(newUsername: string, newCredentials: string) { + username = newUsername; + credentials = newCredentials; +} + +export function GET(callback: CallableFunction, uri: string, options?: string) { var xmlHttp = new XMLHttpRequest(); xmlHttp.open("GET", `${demoEnabled? DEMO_ADDRESS: REST_ADDRESS}/${uri}${options? options: ''}`, true); + if (credentials) + xmlHttp.setRequestHeader("Authorization", "Basic " + credentials); xmlHttp.onload = function (e) { - var data = JSON.parse(xmlHttp.responseText); - if (uri !== UNITS_URI || parseInt(data.time) > lastUpdateTime) - { - callback(data); - lastUpdateTime = parseInt(data.time); - if (isNaN(lastUpdateTime)) - lastUpdateTime = 0; - setConnected(true); + if (xmlHttp.status == 200) { + var data = JSON.parse(xmlHttp.responseText); + if (uri !== UNITS_URI || parseInt(data.time) > lastUpdateTime) + { + callback(data); + lastUpdateTime = parseInt(data.time); + if (isNaN(lastUpdateTime)) + lastUpdateTime = 0; + setConnected(true); + } + } else if (xmlHttp.status == 401) { + console.error("Incorrect username/password"); + setConnectionStatus("failed"); + } else { + setConnected(false); } }; - xmlHttp.onerror = function () { + xmlHttp.onerror = function (res) { console.error("An error occurred during the XMLHttpRequest"); setConnected(false); }; @@ -40,13 +60,15 @@ export function GET(callback: CallableFunction, uri: string, options?: string){ } export function POST(request: object, callback: CallableFunction){ - var xhr = new XMLHttpRequest(); - xhr.open("PUT", demoEnabled? DEMO_ADDRESS: REST_ADDRESS); - xhr.setRequestHeader("Content-Type", "application/json"); - xhr.onreadystatechange = () => { + var xmlHttp = new XMLHttpRequest(); + xmlHttp.open("PUT", demoEnabled? DEMO_ADDRESS: REST_ADDRESS); + xmlHttp.setRequestHeader("Content-Type", "application/json"); + if (credentials) + xmlHttp.setRequestHeader("Authorization", "Basic " + credentials); + xmlHttp.onreadystatechange = () => { callback(); }; - xhr.send(JSON.stringify(request)); + xmlHttp.send(JSON.stringify(request)); } export function getConfig(callback: CallableFunction) { @@ -208,4 +230,81 @@ export function setAdvacedOptions(ID: number, isTanker: boolean, isAWACS: boolea var data = { "setAdvancedOptions": command }; POST(data, () => { }); +} + +export function startUpdate() { + /* On the first connection, force request of full data */ + getAirbases((data: AirbasesData) => getMissionData()?.update(data)); + getBullseye((data: BullseyesData) => getMissionData()?.update(data)); + getMission((data: any) => { getMissionData()?.update(data) }); + getUnits((data: UnitsData) => getUnitsManager()?.update(data), true /* Does a full refresh */); + + requestUpdate(); + requestRefresh(); +} + +export function requestUpdate() { + /* Main update rate = 250ms is minimum time, equal to server update time. */ + getUnits((data: UnitsData) => { + if (!getFreezed()) { + getUnitsManager()?.update(data); + checkSessionHash(data.sessionHash); + } + }, false); + window.setTimeout(() => requestUpdate(), getConnected() ? 250 : 1000); + + getConnectionStatusPanel()?.update(getConnected()); +} + +export function requestRefresh() { + /* Main refresh rate = 5000ms. */ + getUnits((data: UnitsData) => { + if (!getFreezed()) { + getUnitsManager()?.update(data); + getAirbases((data: AirbasesData) => getMissionData()?.update(data)); + getBullseye((data: BullseyesData) => getMissionData()?.update(data)); + getMission((data: any) => { + getMissionData()?.update(data) + }); + + // Update the list of existing units + getUnitDataTable()?.update(); + + checkSessionHash(data.sessionHash); + } + }, true); + window.setTimeout(() => requestRefresh(), 5000); +} + +export function checkSessionHash(newSessionHash: string) { + if (sessionHash != null) { + if (newSessionHash != sessionHash) + location.reload(); + } + else + sessionHash = newSessionHash; +} + +export function setConnected(newConnected: boolean) { + if (connected != newConnected) + newConnected ? getInfoPopup().setText("Connected to DCS Olympus server") : getInfoPopup().setText("Disconnected from DCS Olympus server"); + connected = newConnected; + + if (connected) { + document.querySelector("#splash-screen")?.classList.add("hide"); + document.querySelector("#gray-out")?.classList.add("hide"); + } +} + +export function getConnected() { + return connected; +} + +export function setFreezed(newFreezed: boolean) { + freezed = newFreezed; + freezed ? getInfoPopup().setText("Freezed") : getInfoPopup().setText("Unfreezed"); +} + +export function getFreezed() { + return freezed; } \ No newline at end of file diff --git a/client/views/dialogs.ejs b/client/views/dialogs.ejs index 042c05be..31183517 100644 --- a/client/views/dialogs.ejs +++ b/client/views/dialogs.ejs @@ -1,20 +1,25 @@ -
- +
-

DCS Olympus

Dynamic Unit Command

Version v0.2.0
- +

+ +
diff --git a/client/views/index.ejs b/client/views/index.ejs index c477210c..ae159a9d 100644 --- a/client/views/index.ejs +++ b/client/views/index.ejs @@ -34,6 +34,8 @@ <%- include('dialogs.ejs') %> <%- include('unitdatatable.ejs') %> <%- include('popups.ejs') %> + +
<% /* %> <%- include('log.ejs') %> diff --git a/client/views/navbar.ejs b/client/views/navbar.ejs index 2ddf7c8d..5362d56f 100644 --- a/client/views/navbar.ejs +++ b/client/views/navbar.ejs @@ -6,8 +6,8 @@
-

Olympus

-
v0.2.0
+

DCS Olympus

+
version v0.2.0
Discord @@ -15,6 +15,9 @@ +