Added frontend code for authentication

This commit is contained in:
Pax1601 2023-05-09 15:41:04 +02:00
parent 865be6283c
commit 57b74bd1b1
12 changed files with 286 additions and 153 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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, <number>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, <number>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 = (<HTMLInputElement> (form?.querySelector("#username"))).value;
const password = (<HTMLInputElement> (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() {

View File

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

View File

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

View File

@ -1,20 +1,25 @@
<div id="splash-screen" class="ol-dialog" data-on-click="closeDialog" oncontextmenu="return false;">
<div id="splash-screen" class="ol-dialog" oncontextmenu="return false;">
<div id="splash-content" class="ol-dialog-content">
<div id="app-summary">
<h2>DCS Olympus</h2>
<h4>Dynamic Unit Command</h4>
<div class="app-version">Version <span class="app-version-number">v0.2.0</span></div>
</div>
<div id="legal-stuff">
<h4>Disclaimer</h4>
<p>We ain't no friends with no Eagle Dynamics.</p>
<div id="authentication-form">
<div><h5>Username</h5> <input type="text" id="username" name="username" required autocomplete="username" placeholder="Enter username..."></div>
<div><h5>Password</h5> <input type="password" id="password" name="password" minlength="8" required autocomplete="current-password" placeholder="Enter password..."></div>
<button id="connection-button" class="ol-button-apply" data-on-click="tryConnection">Connect</button>
</div>
</div>
<h5 id="connection-status"><br></h5>
<div id="legal-stuff">
<h5>DISCLAIMER</h5>
<p> DCS Olympus (the "MATERIAL" or "Software") is provided completely free to users subject to the terms of the <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/">CC BY-NC-SA 4.0 Licence</a> except where such terms conflict with this disclaimer, in which case, the terms of this disclaimer shall prevail.
The authors and/or copyright holders of the Software have not received any financial benefit in connection with the Software. In any event, the Software is provided “as is”, without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose and non-infringement. In no event shall the authors and/or copyright holders be liable for any claim, damages or other liability, whether in an action of contract, tort or otherwise, arising from, out of or in connection with the Software or the use or other dealings in the Software. Any party making use of the Software in any manner agrees to be bound by the terms set out in this disclaimer. THIS MATERIAL IS NOT MADE OR SUPPORTED BY EAGLE DYNAMICS SA.
</div>
</div>
</div>
<div id="advanced-settings-dialog" class="ol-panel ol-dialog olympus-dialog-close hide" oncontextmenu="return false;">

View File

@ -34,6 +34,8 @@
<%- include('dialogs.ejs') %>
<%- include('unitdatatable.ejs') %>
<%- include('popups.ejs') %>
<div id="gray-out"></div>
<% /* %>
<%- include('log.ejs') %>

View File

@ -6,8 +6,8 @@
</div>
<div class="ol-select-options">
<div id="olympus-toolbar-summary">
<h3>Olympus</h3>
<div class="accent-green app-version-number">v0.2.0</div>
<h3>DCS Olympus</h3>
<div class="accent-green app-version-number">version v0.2.0</div>
</div>
<div>
<a href="https://www.discord.com" target="_blank">Discord</a>
@ -15,6 +15,9 @@
<div>
<a href="https://github.com/Pax1601/DCSOlympus" target="_blank">Github</a>
</div>
<div data-on-click="reloadPage">
<a href="" target="_blank" data-on-click="reloadPage">Restart Olyumpus</a>
</div>
</div>
</div>