1
0

Restructuring as TypeScript

This commit is contained in:
James Ketr 2025-10-06 11:20:54 -07:00
parent 888688a019
commit a2cb68b421
22 changed files with 357 additions and 389 deletions

View File

@ -1,10 +1,11 @@
const fetch = require('node-fetch'); // @ts-nocheck
const WebSocket = require('ws'); import fetch from 'node-fetch';
const fs = require('fs').promises; import WebSocket from 'ws';
const calculateLongestRoad = require('./longest-road.js'); import fs from 'fs';
import calculateLongestRoad from './longest-road.js';
const { getValidRoads, getValidCorners } = require('../util/validLocations.js'); import { getValidRoads, getValidCorners } from '../util/validLocations.js';
const { layout, staticData } = require('../util/layout.js'); import { layout, staticData } from '../util/layout.js';
const version = '0.0.1'; const version = '0.0.1';
@ -24,14 +25,14 @@ const server = process.argv[2];
const gameId = process.argv[3]; const gameId = process.argv[3];
const name = process.argv[4]; const name = process.argv[4];
const game = {}; const game: any = {};
const anyValue = undefined; const anyValue = undefined;
process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0; process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0;
/* Do not use arrow function as this is rebound to have /* Do not use arrow function as this is rebound to have
* this as the WebSocket */ * this as the WebSocket */
let send = function (data) { let send = function (this: WebSocket, data: any) {
if (data.type === 'get') { if (data.type === 'get') {
console.log(`ws - send: get`, data.fields); console.log(`ws - send: get`, data.fields);
} else { } else {
@ -40,7 +41,7 @@ let send = function (data) {
this.send(JSON.stringify(data)); this.send(JSON.stringify(data));
}; };
const error = (e) => { const error = (e: any) => {
console.log(`ws - error`, e); console.log(`ws - error`, e);
}; };

View File

@ -1,6 +1,7 @@
const { layout } = require('../util/layout.js'); // @ts-nocheck
import { layout } from '../util/layout.js';
const processCorner = (game, color, cornerIndex, placedCorner) => { const processCorner = (game: any, color: string, cornerIndex: number, placedCorner: any): number => {
/* If this corner is allocated and isn't assigned to the walking color, skip it */ /* If this corner is allocated and isn't assigned to the walking color, skip it */
if (placedCorner.color && placedCorner.color !== color) { if (placedCorner.color && placedCorner.color !== color) {
return 0; return 0;
@ -13,7 +14,7 @@ const processCorner = (game, color, cornerIndex, placedCorner) => {
placedCorner.walking = true; placedCorner.walking = true;
/* Calculate the longest road branching from both corners */ /* Calculate the longest road branching from both corners */
let longest = 0; let longest = 0;
layout.corners[cornerIndex].roads.forEach(roadIndex => { layout.corners[cornerIndex].roads.forEach((roadIndex: number) => {
const placedRoad = game.placements.roads[roadIndex]; const placedRoad = game.placements.roads[roadIndex];
if (placedRoad.walking) { if (placedRoad.walking) {
return; return;
@ -32,7 +33,7 @@ const processCorner = (game, color, cornerIndex, placedCorner) => {
return longest; return longest;
}; };
const buildCornerGraph = (game, color, cornerIndex, placedCorner, set) => { const buildCornerGraph = (game: any, color: string, cornerIndex: number, placedCorner: any, set: any) => {
/* If this corner is allocated and isn't assigned to the walking color, skip it */ /* If this corner is allocated and isn't assigned to the walking color, skip it */
if (placedCorner.color && placedCorner.color !== color) { if (placedCorner.color && placedCorner.color !== color) {
return; return;
@ -44,13 +45,13 @@ const buildCornerGraph = (game, color, cornerIndex, placedCorner, set) => {
placedCorner.walking = true; placedCorner.walking = true;
/* Calculate the longest road branching from both corners */ /* Calculate the longest road branching from both corners */
layout.corners[cornerIndex].roads.forEach(roadIndex => { layout.corners[cornerIndex].roads.forEach((roadIndex: number) => {
const placedRoad = game.placements.roads[roadIndex]; const placedRoad = game.placements.roads[roadIndex];
buildRoadGraph(game, color, roadIndex, placedRoad, set); buildRoadGraph(game, color, roadIndex, placedRoad, set);
}); });
}; };
const processRoad = (game, color, roadIndex, placedRoad) => { const processRoad = (game: any, color: string, roadIndex: number, placedRoad: any): number => {
/* If this road isn't assigned to the walking color, skip it */ /* If this road isn't assigned to the walking color, skip it */
if (placedRoad.color !== color) { if (placedRoad.color !== color) {
return 0; return 0;
@ -75,7 +76,7 @@ const processRoad = (game, color, roadIndex, placedRoad) => {
return roadLength; return roadLength;
}; };
const buildRoadGraph = (game, color, roadIndex, placedRoad, set) => { const buildRoadGraph = (game: any, color: string, roadIndex: number, placedRoad: any, set: any) => {
/* If this road isn't assigned to the walking color, skip it */ /* If this road isn't assigned to the walking color, skip it */
if (placedRoad.color !== color) { if (placedRoad.color !== color) {
return; return;
@ -94,7 +95,7 @@ const buildRoadGraph = (game, color, roadIndex, placedRoad, set) => {
}); });
}; };
const clearRoadWalking = (game) => { const clearRoadWalking = (game: any) => {
/* Clear out walk markers on roads */ /* Clear out walk markers on roads */
layout.roads.forEach((item, itemIndex) => { layout.roads.forEach((item, itemIndex) => {
delete game.placements.roads[itemIndex].walking; delete game.placements.roads[itemIndex].walking;
@ -106,7 +107,7 @@ const clearRoadWalking = (game) => {
}); });
} }
const calculateRoadLengths = (game) => { const calculateRoadLengths = (game: any) => {
const color = game.color; const color = game.color;
clearRoadWalking(game); clearRoadWalking(game);
@ -158,4 +159,4 @@ const calculateRoadLengths = (game) => {
return final; return final;
}; };
module.exports = calculateRoadLengths; export default calculateRoadLengths;

View File

@ -1,19 +1,20 @@
"use strict"; // @ts-nocheck
process.env.TZ = "Etc/GMT"; process.env.TZ = "Etc/GMT";
console.log("Loading ketr.ketran"); console.log("Loading ketr.ketran");
const express = require("express"), import express from "express";
bodyParser = require("body-parser"), import bodyParser from "body-parser";
config = require("config"), import config from "config";
session = require('express-session'), import session from 'express-session';
basePath = require("./basepath"), import basePath from "./basepath";
cookieParser = require("cookie-parser"), import cookieParser from "cookie-parser";
app = express(), import fs from 'fs';
fs = require('fs'); import http from "http";
const server = require("http").createServer(app); const app = express();
const server = http.createServer(app);
app.use(cookieParser()); app.use(cookieParser());
@ -21,7 +22,7 @@ app.use(cookieParser());
// and URL for requests under the configured basePath so we can trace which // and URL for requests under the configured basePath so we can trace which
// service (server or dev proxy) is handling requests and their returned // service (server or dev proxy) is handling requests and their returned
// status during debugging. Keep this lightweight. // status during debugging. Keep this lightweight.
app.use((req, res, next) => { app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
try { try {
const bp = app.get("basePath") || '/'; const bp = app.get("basePath") || '/';
if (req.url && req.url.indexOf(bp) === 0) { if (req.url && req.url.indexOf(bp) === 0) {
@ -37,16 +38,17 @@ app.use((req, res, next) => {
next(); next();
}); });
const ws = require('express-ws')(app, server); import expressWs from 'express-ws';
expressWs(app, server);
require("./console-line.js"); /* Monkey-patch console.log with line numbers */ require("./console-line.js"); /* Monkey-patch console.log with line numbers */
const frontendPath = config.get("frontendPath").replace(/\/$/, "") + "/", const frontendPath = (config.get("frontendPath") as string).replace(/\/$/, "") + "/",
serverConfig = config.get("server"); serverConfig = config.get("server") as any;
console.log("Hosting server from: " + basePath); console.log("Hosting server from: " + basePath);
let userDB, gameDB; let userDB: any, gameDB: any;
app.use(bodyParser.json()); app.use(bodyParser.json());
@ -86,7 +88,7 @@ const users = require("./routes/users");
app.use(basePath + "api/v1/users", users.router); app.use(basePath + "api/v1/users", users.router);
*/ */
app.use(function(err, req, res, next) { app.use(function(err: any, req: express.Request, res: express.Response, next: express.NextFunction) {
console.error(err.message); console.error(err.message);
res.status(err.status || 500).json({ res.status(err.status || 500).json({
message: err.message, message: err.message,
@ -126,7 +128,7 @@ require("./db/games").then(function(db) {
process.exit(-1); process.exit(-1);
}); });
server.on("error", function(error) { server.on("error", function(error: any) {
if (error.syscall !== "listen") { if (error.syscall !== "listen") {
throw error; throw error;
} }
@ -145,3 +147,5 @@ server.on("error", function(error) {
throw error; throw error;
} }
}); });
export { app, server };

View File

@ -1,5 +1,6 @@
const fs = require('fs'); import fs from 'fs';
let basePathRaw = process.env.VITE_BASEPATH || '';
let basePathRaw = process.env['VITE_BASEPATH'] || '';
// If env not provided, try to detect a <base href="..."> in the // If env not provided, try to detect a <base href="..."> in the
// built client's index.html (if present). This helps when the // built client's index.html (if present). This helps when the
@ -29,4 +30,4 @@ if (basePath === '//') basePath = '/';
console.log(`Using basepath ${basePath}`); console.log(`Using basepath ${basePath}`);
module.exports = basePath; export default basePath;

View File

@ -1,30 +0,0 @@
/* monkey-patch console.log to prefix with file/line-number */
if (process.env.LOG_LINE) {
let cwd = process.cwd(),
cwdRe = new RegExp("^[^/]*" + cwd.replace("/", "\\/") + "\/([^:]*:[0-9]*).*$");
[ "log", "warn", "error" ].forEach(function(method) {
console[method] = (function () {
let orig = console[method];
return function () {
function getErrorObject() {
try {
throw Error('');
} catch (err) {
return err;
}
}
let err = getErrorObject(),
caller_line = err.stack.split("\n")[3],
args = [caller_line.replace(cwdRe, "$1 -")];
/* arguments.unshift() doesn't exist... */
for (var i = 0; i < arguments.length; i++) {
args.push(arguments[i]);
}
orig.apply(this, args);
};
})();
});
}

28
server/console-line.ts Executable file
View File

@ -0,0 +1,28 @@
/* monkey-patch console.log to prefix with file/line-number */
if (process.env['LOG_LINE']) {
let cwd = process.cwd(),
cwdRe = new RegExp("^[^/]*" + cwd.replace("/", "\\/") + "\/([^:]*:[0-9]*).*$");
[ "log", "warn", "error" ].forEach(function(method: string) {
(console as any)[method] = (function () {
let orig = (console as any)[method];
return function (this: any, ...args: any[]) {
function getErrorObject(): Error {
try {
throw Error('');
} catch (err) {
return err as Error;
}
}
let err = getErrorObject(),
caller_line = err.stack?.split("\n")[3] || '',
prefixedArgs = [caller_line.replace(cwdRe, "$1 -")];
/* arguments.unshift() doesn't exist... */
prefixedArgs.push(...args);
orig.apply(this, prefixedArgs);
};
})();
});
}

View File

@ -1,44 +0,0 @@
"use strict";
const fs = require('fs'),
path = require('path'),
Sequelize = require('sequelize'),
config = require('config');
function init() {
const db = {
sequelize: new Sequelize(config.get("db.games")),
Sequelize: Sequelize
};
return db.sequelize.authenticate().then(function () {
const Game = db.sequelize.define('game', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true
},
path: Sequelize.STRING,
name: Sequelize.STRING,
}, {
timestamps: false,
classMethods: {
associate: function() {
}
}
});
return db.sequelize.sync({
force: false
}).then(function () {
return db;
});
}).catch(function (error) {
console.log("ERROR: Failed to authenticate with GAMES DB");
console.log("ERROR: " + JSON.stringify(config.get("db"), null, 2));
console.log(error);
throw error;
});
}
module.exports = init();

View File

@ -1,70 +0,0 @@
"use strict";
const Sequelize = require('sequelize'),
config = require('config');
function init() {
const db = {
sequelize: new Sequelize(config.get("db.users")),
Sequelize: Sequelize
};
return db.sequelize.authenticate().then(function () {
const User = db.sequelize.define('users', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true
},
displayName: Sequelize.STRING,
notes: Sequelize.STRING,
uid: Sequelize.STRING,
authToken: Sequelize.STRING,
authDate: Sequelize.DATE,
authenticated: Sequelize.BOOLEAN,
mailVerified: Sequelize.BOOLEAN,
mail: Sequelize.STRING,
memberSince: Sequelize.DATE,
password: Sequelize.STRING, /* SHA hash of user supplied password */
passwordExpires: Sequelize.DATE
}, {
timestamps: false
});
const Authentication = db.sequelize.define('authentication', {
key: {
type: Sequelize.STRING,
primaryKey: true,
allowNull: false
},
issued: Sequelize.DATE,
type: {
type: Sequelize.ENUM,
values: [ 'account-setup', 'password-reset' ]
},
userId: {
type: Sequelize.INTEGER,
allowNull: false,
references: {
model: User,
key: 'id',
}
}
}, {
timestamps: false
});
return db.sequelize.sync({
force: false
}).then(function () {
return db;
});
}).catch(function (error) {
console.log("ERROR: Failed to authenticate with USER DB");
console.log("ERROR: " + JSON.stringify(config.get("db"), null, 2));
console.log(error);
throw error;
});
}
module.exports = init();

View File

@ -1,8 +1,8 @@
"use strict"; "use strict";
const config = require("config"), import config from "config";
crypto = require("crypto"), import crypto from "crypto";
hb = require("handlebars"); import hb from "handlebars";
const templates = { const templates = {
"verify": { "verify": {
@ -52,13 +52,13 @@ const templates = {
} }
}; };
const sendVerifyMail = function(userDB, req, user) { const sendVerifyMail = function(userDB: any, req: any, user: any): any {
return userDB.sequelize.query("DELETE FROM authentications WHERE userId=:id AND type='account-setup'", { return userDB.sequelize.query("DELETE FROM authentications WHERE userId=:id AND type='account-setup'", {
replacements: { replacements: {
id: user.id id: user.id
} }
}).then(function() { }).then(function() {
return new Promise(function(resolve, reject) { return new Promise<string>(function(resolve, reject) {
crypto.randomBytes(16, function(error, buffer) { crypto.randomBytes(16, function(error, buffer) {
if (error) { if (error) {
return reject(error); return reject(error);
@ -66,7 +66,7 @@ const sendVerifyMail = function(userDB, req, user) {
return resolve(buffer.toString('hex')); return resolve(buffer.toString('hex'));
}); });
}); });
}).then(function(secret) { }).then(function(secret: string) {
return userDB.sequelize.query( return userDB.sequelize.query(
"INSERT INTO authentications " + "INSERT INTO authentications " +
"(userId,issued,key,type) " + "(userId,issued,key,type) " +
@ -77,11 +77,11 @@ const sendVerifyMail = function(userDB, req, user) {
} }
}).then(function() { }).then(function() {
return secret; return secret;
}).catch(function(error) { }).catch(function(error: any) {
console.log(error); console.log(error);
throw error; throw error;
}); });
}).then(function(secret) { }).then(function(secret: string) {
const transporter = req.app.get("transporter"); const transporter = req.app.get("transporter");
if (!transporter) { if (!transporter) {
console.log("Not sending VERIFY email; SMTP not configured."); console.log("Not sending VERIFY email; SMTP not configured.");
@ -102,36 +102,37 @@ const sendVerifyMail = function(userDB, req, user) {
text: hb.compile(templates.verify.text)(data), text: hb.compile(templates.verify.text)(data),
html: hb.compile(templates.verify.html)(data) html: hb.compile(templates.verify.html)(data)
}; };
return new Promise(function (resolve, reject) { return new Promise<void>(function (resolve, reject) {
let attempts = 10; let attempts = 10;
function send(envelope) { function send(envelope: any) {
/* Rate limit to ten per second */ /* Rate limit to ten per second */
transporter.sendMail(envelope, function (error, info) { transporter.sendMail(envelope, function (error: any, info: any) {
if (!error) { if (!error) {
console.log('Message sent: ' + info.response); console.log('Message sent: ' + (info && info.response));
return resolve(); resolve();
} return;
}
if (attempts == 0) { if (attempts == 0) {
console.log("Error sending email: ", error); console.log("Error sending email: ", error);
return reject(error); return reject(error);
} }
attempts--; attempts--;
console.log("Unable to send mail. Trying again in 100ms (" + attempts + " attempts remain): ", error); console.log("Unable to send mail. Trying again in 100ms (" + attempts + " attempts remain): ", error);
setTimeout(send.bind(undefined, envelope), 100); setTimeout(send.bind(undefined, envelope), 100);
}); });
} }
send(envelope); send(envelope);
}); });
}).catch(function(error) { }).catch(function(error: any) {
console.log("Error creating account: ", error); console.log("Error creating account: ", error);
}); });
}; };
const sendPasswordChangedMail = function(userDB, req, user) { const sendPasswordChangedMail = function(_userDB: any, req: any, user: any): any {
const transporter = req.app.get("transporter"); const transporter = req.app.get("transporter");
if (!transporter) { if (!transporter) {
console.log("Not sending VERIFY email; SMTP not configured."); console.log("Not sending VERIFY email; SMTP not configured.");
@ -151,14 +152,14 @@ const sendPasswordChangedMail = function(userDB, req, user) {
text: hb.compile(templates.password.text)(data), text: hb.compile(templates.password.text)(data),
html: hb.compile(templates.password.html)(data) html: hb.compile(templates.password.html)(data)
}; };
return new Promise(function (resolve, reject) { return new Promise<void>(function (resolve, reject) {
let attempts = 10; let attempts = 10;
function send(envelope) { function send(envelope: any) {
/* Rate limit to ten per second */ /* Rate limit to ten per second */
transporter.sendMail(envelope, function (error, info) { transporter.sendMail(envelope, function (error: any, info: any) {
if (!error) { if (!error) {
console.log('Message sent: ' + info.response); console.log('Message sent: ' + (info && info.response));
return resolve(); return resolve();
} }
@ -177,7 +178,7 @@ const sendPasswordChangedMail = function(userDB, req, user) {
}); });
}; };
module.exports = { export {
sendVerifyMail, sendVerifyMail,
sendPasswordChangedMail sendPasswordChangedMail
} }

View File

@ -1,7 +1,7 @@
"use strict"; "use strict";
const createTransport = require('nodemailer').createTransport, import { createTransport } from 'nodemailer';
{ timestamp } = require("./timestamp"); import { timestamp } from "./timestamp";
const transporter = createTransport({ const transporter = createTransport({
host: 'email.ketrenos.com', host: 'email.ketrenos.com',
@ -9,8 +9,8 @@ const transporter = createTransport({
port: 25 port: 25
}); });
function sendMail(to, subject, message, cc) { function sendMail(to: string, subject: string, message: string, cc?: string): Promise<boolean> {
let envelope = { let envelope: any = {
subject: subject, subject: subject,
from: 'Ketr.Ketran <james_ketran@ketrenos.com>', from: 'Ketr.Ketran <james_ketran@ketrenos.com>',
to: to || '', to: to || '',
@ -29,33 +29,29 @@ function sendMail(to, subject, message, cc) {
return new Promise(function (resolve, reject) { return new Promise(function (resolve, reject) {
let attempts = 10; let attempts = 10;
function attemptSend(envelope) { function attemptSend(envelope: any) {
/* Rate limit to ten per second */ /* Rate limit to ten per second */
transporter.sendMail(envelope, function (error, info) { transporter.sendMail(envelope, function (error, _info) {
if (error) { if (error) {
if (attempts) { if (attempts) {
attempts--; attempts--;
console.warn(timestamp() + " Unable to send mail. Trying again in 100ms (" + attempts + " attempts remain): ", error); console.warn(timestamp() + " Unable to send mail. Trying again in 100ms (" + attempts + " attempts remain): ", error);
setTimeout(send.bind(undefined, envelope), 100); setTimeout(() => attemptSend(envelope), 100);
} else { } else {
console.error(timestamp() + " Error sending email: ", error) console.error(timestamp() + " Error sending email: ", error)
return reject(error); reject(error);
} }
} else {
console.log(timestamp() + " Mail sent to: " + envelope.to);
resolve(true);
} }
console.log(timestamp() + " Mail sent to: " + envelope.to);
return resolve(true);
}); });
} }
attemptSend(envelope); attemptSend(envelope);
}).then(function(success) {
if (!success) {
console.error(timestamp() + " Mail not sent to: " + envelope.to);
}
}); });
} }
module.exports = { export {
sendMail: sendMail sendMail
}; };

View File

@ -1,9 +1,9 @@
{ {
"name": "peddlers-of-ketran-server", "name": "peddlers-of-ketran-server",
"version": "1.0.0", "version": "1.0.0",
"main": "app.js", "main": "dist/src/app.js",
"scripts": { "scripts": {
"start": "export $(cat ../.env | xargs) && node dist/app.js", "start": "export $(cat ../.env | xargs) && node dist/src/app.js",
"start:legacy": "export $(cat ../.env | xargs) && node app.js", "start:legacy": "export $(cat ../.env | xargs) && node app.js",
"build": "tsc -p tsconfig.json", "build": "tsc -p tsconfig.json",
"start:dev": "ts-node-dev --respawn --transpile-only src/app.ts", "start:dev": "ts-node-dev --respawn --transpile-only src/app.ts",
@ -36,9 +36,25 @@
"ws": "^8.5.0" "ws": "^8.5.0"
}, },
"devDependencies": { "devDependencies": {
"@types/bluebird": "^3.5.38",
"@types/config": "^3.3.1",
"@types/connect-sqlite3": "^0.9.3",
"@types/cookie-parser": "^1.4.4",
"@types/express": "^4.17.17",
"@types/express-session": "^1.17.7",
"@types/express-ws": "^3.0.1",
"@types/handlebars": "^4.1.0",
"@types/jest": "^29.5.0", "@types/jest": "^29.5.0",
"@types/moment": "^2.13.0",
"@types/morgan": "^1.9.5",
"@types/node": "^20.0.0", "@types/node": "^20.0.0",
"@types/node-fetch": "^2.6.4",
"@types/node-gzip": "^1.1.0",
"@types/nodemailer": "^6.4.8",
"@types/random-words": "^1.1.0",
"@types/sequelize": "^4.28.15",
"@types/supertest": "^2.0.12", "@types/supertest": "^2.0.12",
"@types/ws": "^8.5.5",
"jest": "^29.7.0", "jest": "^29.7.0",
"supertest": "^6.3.3", "supertest": "^6.3.3",
"ts-jest": "^29.1.0", "ts-jest": "^29.1.0",

View File

@ -1,8 +1,6 @@
"use strict"; import express from 'express';
import fs from 'fs';
const express = require("express"), import url from 'url';
fs = require("fs"),
url = require("url");
const router = express.Router(); const router = express.Router();
@ -10,9 +8,9 @@ const router = express.Router();
* to replace BASEPATH */ * to replace BASEPATH */
router.get("/*", (req, res, next) => { router.get("/*", (req, res, next) => {
const parts = url.parse(req.url), const parts = url.parse(req.url),
basePath = req.app.get("basePath"); basePath = req.app.get("basePath") as string;
if (!/^\/[^/]+\.html$/.exec(parts.pathname)) { if (!/^\/[^/]+\.html$/.exec(parts.pathname || '')) {
return next(); return next();
} }
@ -20,14 +18,13 @@ router.get("/*", (req, res, next) => {
/* Replace <script>'<base href="/BASEPATH/">';</script> in index.html with /* Replace <script>'<base href="/BASEPATH/">';</script> in index.html with
* the basePath */ * the basePath */
fs.readFile("frontend" + parts.pathname, "utf8", function(error, content) { fs.readFile("frontend" + (parts.pathname || ''), "utf8", function(error, content) {
if (error) { if (error) {
return next(); return next();
} }
res.send(content.replace( res.send((content as string).replace(
/<script>'<base href="BASEPATH">';<\/script>/, /<script>'<base href="BASEPATH">';<\/script>/,
"<base href='" + basePath + "'>")); "<base href='" + basePath + "'>"));
}); });
}); });
export default router;
module.exports = router;

View File

@ -1,6 +1,6 @@
"use strict"; "use strict";
const express = require("express"); import express from "express";
const router = express.Router(); const router = express.Router();
/* /*
@ -9,7 +9,7 @@ const router = express.Router();
* by the server. It is mounted under the application's basePath so you can * by the server. It is mounted under the application's basePath so you can
* hit: /<basePath>/__debug/request * hit: /<basePath>/__debug/request
*/ */
router.get('/__debug/request', (req, res) => { router.get('/__debug/request', (req: express.Request, res: express.Response) => {
try { try {
console.log('[debug] __debug/request hit:', req.method, req.originalUrl); console.log('[debug] __debug/request hit:', req.method, req.originalUrl);
// Echo back a compact JSON summary so curl or browsers can inspect it. // Echo back a compact JSON summary so curl or browsers can inspect it.
@ -21,10 +21,10 @@ router.get('/__debug/request', (req, res) => {
hostname: req.hostname, hostname: req.hostname,
basePath: req.app && req.app.get && req.app.get('basePath') basePath: req.app && req.app.get && req.app.get('basePath')
}); });
} catch (e) { } catch (e: any) {
console.error('[debug] error in __debug/request', e && e.stack || e); console.error('[debug] error in __debug/request', e && e.stack || e);
res.status(500).json({ error: 'debug endpoint error' }); res.status(500).json({ error: 'debug endpoint error' });
} }
}); });
module.exports = router; export default router;

View File

@ -1,24 +1,31 @@
"use strict"; // @ts-nocheck
import express from 'express';
import crypto from 'crypto';
import { readFile, writeFile, mkdir } from 'fs/promises';
import fs from 'fs';
import randomWords from 'random-words';
import equal from 'fast-deep-equal';
import { layout, staticData } from '../util/layout.js';
import basePath from '../basepath';
const express = require("express"), import { getValidRoads, getValidCorners, isRuleEnabled } from '../util/validLocations.js';
router = express.Router(),
crypto = require("crypto")
const { readFile, writeFile, mkdir } = require("fs").promises,
fs = require("fs"),
accessSync = fs.accessSync,
randomWords = require("random-words"),
equal = require("fast-deep-equal");
const { layout, staticData } = require('../util/layout.js');
const basePath = require('../basepath');
const { getValidRoads, getValidCorners, isRuleEnabled } = require('../util/validLocations.js'); interface Player {
order: number;
orderRoll?: number;
position?: string;
orderStatus?: string;
tied?: boolean;
[key: string]: unknown;
}
const router = express.Router();
const MAX_SETTLEMENTS = 5; const MAX_SETTLEMENTS = 5;
const MAX_CITIES = 4; const MAX_CITIES = 4;
const MAX_ROADS = 15; const MAX_ROADS = 15;
const types = [ 'wheat', 'stone', 'sheep', 'wood', 'brick' ]; const types: string[] = [ 'wheat', 'stone', 'sheep', 'wood', 'brick' ];
const debug = { const debug = {
audio: false, audio: false,
@ -32,9 +39,9 @@ const debug = {
// others used a flatter shape. This helper accepts either a string or an // others used a flatter shape. This helper accepts either a string or an
// already-parsed object and returns a stable object so handlers don't need // already-parsed object and returns a stable object so handlers don't need
// to defensively check multiple nested locations. // to defensively check multiple nested locations.
function normalizeIncoming(msg) { function normalizeIncoming(msg: unknown): { type: string | null, data: unknown } {
if (!msg) return { type: null, data: null }; if (!msg) return { type: null, data: null };
let parsed = null; let parsed: unknown = null;
try { try {
if (typeof msg === 'string') { if (typeof msg === 'string') {
parsed = JSON.parse(msg); parsed = JSON.parse(msg);
@ -46,7 +53,7 @@ function normalizeIncoming(msg) {
return { type: null, data: null }; return { type: null, data: null };
} }
if (!parsed) return { type: null, data: null }; if (!parsed) return { type: null, data: null };
const type = parsed.type || parsed.action || null; const type = (parsed as any).type || (parsed as any).action || null;
// Prefer parsed.data when present, but allow flattened payloads where // Prefer parsed.data when present, but allow flattened payloads where
// properties like `name` live at the root. // properties like `name` live at the root.
const data = parsed.data || (Object.keys(parsed).length ? Object.assign({}, parsed) : null); const data = parsed.data || (Object.keys(parsed).length ? Object.assign({}, parsed) : null);
@ -82,14 +89,14 @@ function shuffleArray(array) {
const games = {}; const games = {};
const audio = {}; const audio = {};
const processTies = (players) => { const processTies = (players: Player[]) => {
/* Sort the players into buckets based on their /* Sort the players into buckets based on their
* order, and their current roll. If a resulting * order, and their current roll. If a resulting
* roll array has more than one element, then there * roll array has more than one element, then there
* is a tie that must be resolved */ * is a tie that must be resolved */
let slots = []; let slots: Player[][] = [];
players.forEach(player => { players.forEach((player: Player) => {
if (!slots[player.order]) { if (!slots[player.order]) {
slots[player.order] = []; slots[player.order] = [];
} }
@ -98,13 +105,13 @@ const processTies = (players) => {
let ties = false, position = 1; let ties = false, position = 1;
const irstify = (position) => { const irstify = (position: number): string => {
switch (position) { switch (position) {
case 1: return `1st`; case 1: return `1st`;
case 2: return `2nd`; case 2: return `2nd`;
case 3: return `3rd`; case 3: return `3rd`;
case 4: return `4th`; case 4: return `4th`;
default: return position; default: return position.toString();
} }
} }
@ -112,7 +119,7 @@ const processTies = (players) => {
slots.reverse().forEach((slot) => { slots.reverse().forEach((slot) => {
if (slot.length !== 1) { if (slot.length !== 1) {
ties = true; ties = true;
slot.forEach(player => { slot.forEach((player: Player) => {
player.orderRoll = 0; /* Ties have to be re-rolled */ player.orderRoll = 0; /* Ties have to be re-rolled */
player.position = irstify(position); player.position = irstify(position);
player.orderStatus = `Tied for ${irstify(position)}`; player.orderStatus = `Tied for ${irstify(position)}`;
@ -5216,4 +5223,4 @@ router.post("/:id?", async (req, res/*, next*/) => {
}); });
module.exports = router; export default router;

View File

@ -1,10 +1,10 @@
"use strict"; "use strict";
const express = require("express"), import express from "express";
fs = require("fs"), import fs from "fs";
url = require("url"), import url from "url";
config = require("config"), import config from "config";
basePath = require("../basepath"); import basePath from "../basepath";
const router = express.Router(); const router = express.Router();
@ -31,24 +31,24 @@ const extensionMatch = new RegExp("^.*?(" + extensions.join("|") + ")$", "i");
* If so, 404 because the asset isn't there. otherwise assume it is a * If so, 404 because the asset isn't there. otherwise assume it is a
* dynamic client side route and *then* return index.html. * dynamic client side route and *then* return index.html.
*/ */
router.get("/*", function(req, res, next) { router.get("/*", function(req: express.Request, res: express.Response, next: express.NextFunction) {
const parts = url.parse(req.url); const parts = url.parse(req.url);
/* If req.user isn't set yet (authentication hasn't happened) then /* If req.user isn't set yet (authentication hasn't happened) then
* only allow / to be loaded--everything else chains to the next * only allow / to be loaded--everything else chains to the next
* handler */ * handler */
if (!req.user && if (!(req as any).user &&
req.url != "/" && req.url != "/" &&
req.url.indexOf("/games") != 0) { req.url.indexOf("/games") != 0) {
return next(); return next();
} }
if (req.url == "/" || req.url.indexOf("/games") == 0 || !extensionMatch.exec(parts.pathname)) { if (req.url == "/" || req.url.indexOf("/games") == 0 || !extensionMatch.exec(parts.pathname || '')) {
console.log("Returning index for " + req.url); console.log("Returning index for " + req.url);
/* Replace <script>'<base href="BASEPATH">';</script> in index.html with /* Replace <script>'<base href="BASEPATH">';</script> in index.html with
* the basePath */ * the basePath */
const frontendPath = config.get("frontendPath").replace(/\/$/, "") + "/", const frontendPath = (config.get("frontendPath") as string).replace(/\/$/, "") + "/",
index = fs.readFileSync(frontendPath + "index.html", "utf8"); index = fs.readFileSync(frontendPath + "index.html", "utf8");
res.send(index.replace( res.send(index.replace(
/<script>'<base href="BASEPATH">';<\/script>/, /<script>'<base href="BASEPATH">';<\/script>/,
@ -63,4 +63,4 @@ router.get("/*", function(req, res, next) {
}); });
}); });
module.exports = router; export default router;

View File

@ -1,19 +1,19 @@
"use strict"; "use strict";
const express = require("express"), import express from "express";
config = require("config"), import config from "config";
{ sendVerifyMail, sendPasswordChangedMail } = require("../lib/mail"), import { sendVerifyMail, sendPasswordChangedMail } from "../lib/mail";
crypto = require("crypto"); import crypto from "crypto";
const router = express.Router(); const router = express.Router();
let userDB; let userDB: any;
require("../db/users.js").then(function(db) { import("../db/users.js").then(function(db: any) {
userDB = db; userDB = db;
}); });
router.get("/", function(req, res/*, next*/) { router.get("/", function(req: express.Request, res: express.Response/*, next*/) {
console.log("/users/"); console.log("/users/");
return getSessionUser(req).then((user) => { return getSessionUser(req).then((user) => {
return res.status(200).send(user); return res.status(200).send(user);
@ -23,12 +23,12 @@ router.get("/", function(req, res/*, next*/) {
}); });
}); });
router.put("/password", function(req, res) { router.put("/password", function(req: express.Request, res: express.Response) {
console.log("/users/password"); console.log("/users/password");
const q = req.query as any;
const changes = { const changes = {
currentPassword: req.query.c || req.body.c, currentPassword: q.c || req.body.c,
newPassword: req.query.n || req.body.n newPassword: q.n || req.body.n
}; };
if (!changes.currentPassword || !changes.newPassword) { if (!changes.currentPassword || !changes.newPassword) {
@ -39,7 +39,7 @@ router.put("/password", function(req, res) {
return res.status(400).send("Attempt to set new password to current password."); return res.status(400).send("Attempt to set new password to current password.");
} }
return getSessionUser(req).then(function(user) { return getSessionUser(req).then(function(user: any) {
return userDB.sequelize.query("SELECT id FROM users " + return userDB.sequelize.query("SELECT id FROM users " +
"WHERE uid=:username AND password=:password", { "WHERE uid=:username AND password=:password", {
replacements: { replacements: {
@ -48,13 +48,13 @@ router.put("/password", function(req, res) {
}, },
type: userDB.Sequelize.QueryTypes.SELECT, type: userDB.Sequelize.QueryTypes.SELECT,
raw: true raw: true
}).then(function(users) { }).then(function(users: any) {
if (users.length != 1) { if (users.length != 1) {
return null; return null;
} }
return user; return user;
}); });
}).then(function(user) { }).then(function(user: any) {
if (!user) { if (!user) {
console.log("Invalid password"); console.log("Invalid password");
/* Invalid password */ /* Invalid password */
@ -77,15 +77,15 @@ router.put("/password", function(req, res) {
}); });
}); });
router.post("/create", function(req, res) { router.post("/create", function(req: express.Request, res: express.Response) {
console.log("/users/create"); console.log("/users/create");
const q = req.query as any;
const user = { const user = {
uid: req.query.m || req.body.m, uid: q.m || req.body.m,
displayName: req.query.n || req.body.n || "", displayName: q.n || req.body.n || "",
password: req.query.p || req.body.p || "", password: q.p || req.body.p || "",
mail: req.query.m || req.body.m, mail: q.m || req.body.m,
notes: req.query.w || req.body.w || "" notes: q.w || req.body.w || ""
}; };
if (!user.uid || !user.password || !user.displayName || !user.notes) { if (!user.uid || !user.password || !user.displayName || !user.notes) {
@ -98,37 +98,38 @@ router.post("/create", function(req, res) {
replacements: user, replacements: user,
type: userDB.Sequelize.QueryTypes.SELECT, type: userDB.Sequelize.QueryTypes.SELECT,
raw: true raw: true
}).then(function(results) { }).then(function(results: any) {
if (results.length != 0) { if (results.length != 0) {
return res.status(400).send("Email address already used."); return res.status(400).send("Email address already used.");
} }
let re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; let re = /^(([^<>()\[\]\\.,;:\s@\"]+(\.[^<>()\[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
if (!re.exec(user.mail)) { if (!re.exec(user.mail)) {
console.log("Invalid email address: " + user.mail); console.log("Invalid email address: " + user.mail);
throw "Invalid email address."; throw "Invalid email address.";
} }
return;
}).then(function() { }).then(function() {
return userDB.sequelize.query("INSERT INTO users " + return userDB.sequelize.query("INSERT INTO users " +
"(uid,displayName,password,mail,memberSince,authenticated,notes) " + "(uid,displayName,password,mail,memberSince,authenticated,notes) " +
"VALUES(:uid,:displayName,:password,:mail,CURRENT_TIMESTAMP,0,:notes)", { "VALUES(:uid,:displayName,:password,:mail,CURRENT_TIMESTAMP,0,:notes)", {
replacements: user replacements: user
}).spread(function(results, metadata) { }).spread(function(_results: any, metadata: any) {
req.session.userId = metadata.lastID; req.session.userId = metadata.lastID;
}).then(function() { }).then(function() {
return getSessionUser(req).then(function(user) { return getSessionUser(req).then(function(user: any) {
res.status(200).send(user); res.status(200).send(user);
user.id = req.session.userId; user.id = req.session.userId;
return sendVerifyMail(userDB, req, user); return sendVerifyMail(userDB, req, user);
}); });
}).catch(function(error) { }).catch(function(error: any) {
console.log("Error creating account: ", error); console.log("Error creating account: ", error);
return res.status(401).send(error); return res.status(401).send(error);
}); });
}); });
}); });
const getSessionUser = function(req) { const getSessionUser = function(req: express.Request): Promise<any> {
return Promise.resolve().then(function() { return Promise.resolve().then(function() {
if (!req.session || !req.session.userId) { if (!req.session || !req.session.userId) {
throw "Unauthorized. You must be logged in."; throw "Unauthorized. You must be logged in.";
@ -143,7 +144,7 @@ const getSessionUser = function(req) {
}, },
type: userDB.Sequelize.QueryTypes.SELECT, type: userDB.Sequelize.QueryTypes.SELECT,
raw: true raw: true
}).then(function(results) { }).then(function(results: any) {
if (results.length != 1) { if (results.length != 1) {
throw "Invalid account."; throw "Invalid account.";
} }
@ -160,9 +161,9 @@ const getSessionUser = function(req) {
return user; return user;
} }
return user; return user;
}); });
}).then(function(user) { }).then(function(user: any) {
req.user = user; req.user = user;
/* If the user already has a restriction, or there are no album user restrictions, /* If the user already has a restriction, or there are no album user restrictions,
@ -171,7 +172,7 @@ const getSessionUser = function(req) {
return user; return user;
} }
let allowed = config.get("restrictions"); let allowed: any = config.get("restrictions");
if (!Array.isArray(allowed)) { if (!Array.isArray(allowed)) {
allowed = [ allowed ]; allowed = [ allowed ];
} }
@ -187,8 +188,8 @@ const getSessionUser = function(req) {
}).then(function(user) { }).then(function(user) {
/* If there are maintainers on this album, check if this user is a maintainer */ /* If there are maintainers on this album, check if this user is a maintainer */
if (config.has("maintainers")) { if (config.has("maintainers")) {
let maintainers = config.get("maintainers"); let maintainers: any = config.get("maintainers");
if (maintainers.indexOf(user.username) != -1) { if (Array.isArray(maintainers) && maintainers.indexOf(user.username) != -1) {
user.maintainer = true; user.maintainer = true;
if (user.restriction) { if (user.restriction) {
console.warn("User " + user.username + " is a maintainer AND has a restriction which will be ignored: " + user.restriction); console.warn("User " + user.username + " is a maintainer AND has a restriction which will be ignored: " + user.restriction);
@ -212,11 +213,12 @@ const getSessionUser = function(req) {
}); });
} }
router.post("/login", function(req, res) { router.post("/login", function(req: express.Request, res: express.Response) {
console.log("/users/login"); console.log("/users/login");
const q = req.query as any;
let username = req.query.u || req.body.u || "", let username = q.u || req.body.u || "",
password = req.query.p || req.body.p || ""; password = q.p || req.body.p || "";
console.log("Login attempt"); console.log("Login attempt");
@ -224,7 +226,7 @@ router.post("/login", function(req, res) {
return res.status(400).send("Missing username and/or password"); return res.status(400).send("Missing username and/or password");
} }
return new Promise((reject, resolve) => { return new Promise((resolve, _reject) => {
console.log("Looking up user in DB."); console.log("Looking up user in DB.");
let query = "SELECT " + let query = "SELECT " +
"id,mailVerified,authenticated,uid AS username,displayName AS name,mail " + "id,mailVerified,authenticated,uid AS username,displayName AS name,mail " +
@ -235,7 +237,7 @@ router.post("/login", function(req, res) {
password: crypto.createHash('sha256').update(password).digest('base64') password: crypto.createHash('sha256').update(password).digest('base64')
}, },
type: userDB.Sequelize.QueryTypes.SELECT type: userDB.Sequelize.QueryTypes.SELECT
}).then(function(users) { }).then(function(users: any) {
if (users.length != 1) { if (users.length != 1) {
return resolve(null); return resolve(null);
} }
@ -243,7 +245,7 @@ router.post("/login", function(req, res) {
req.session.userId = user.id; req.session.userId = user.id;
return resolve(user); return resolve(user);
}); });
}).then(function(user) { }).then(function(user: any) {
if (!user) { if (!user) {
console.log(username + " not found (or invalid password.)"); console.log(username + " not found (or invalid password.)");
req.session.userId = null; req.session.userId = null;
@ -259,7 +261,7 @@ router.post("/login", function(req, res) {
console.log(message); console.log(message);
} }
return getSessionUser(req).then(function(user) { return getSessionUser(req).then(function(user: any) {
return res.status(200).send(user); return res.status(200).send(user);
}); });
}).catch(function(error) { }).catch(function(error) {
@ -277,7 +279,7 @@ router.get("/logout", function(req, res) {
res.status(200).send({}); res.status(200).send({});
}); });
module.exports = { export {
router, router,
getSessionUser getSessionUser
}; };

View File

@ -1,42 +1,45 @@
/* eslint-disable @typescript-eslint/no-var-requires */
import type { Request, Response, NextFunction } from 'express'; import type { Request, Response, NextFunction } from 'express';
import express from 'express';
import bodyParser from 'body-parser';
import config from 'config';
import basePath from '../basepath';
import cookieParser from 'cookie-parser';
import http from 'http';
import expressWs from 'express-ws';
process.env.TZ = "Etc/GMT"; process.env.TZ = "Etc/GMT";
console.log("Loading ketr.ketran"); console.log("Loading ketr.ketran");
const express = require("express");
const bodyParser = require("body-parser");
const config = require("config");
const session = require('express-session');
const basePath = require("../basepath");
const cookieParser = require("cookie-parser");
const fs = require('fs');
const app = express(); const app = express();
const server = require("http").createServer(app); const server = http.createServer(app);
app.use(cookieParser()); app.use(cookieParser());
const ws = require('express-ws')(app, server); expressWs(app, server);
require("../console-line.js"); /* Monkey-patch console.log with line numbers */ require("../console-line.js"); /* Monkey-patch console.log with line numbers */
// Temporary debug routes (dev-only). Mount before static so we can // Temporary debug routes (dev-only). Mount before static so we can
// inspect what the server receives for base-prefixed requests. // inspect what the server receives for base-prefixed requests.
try { try {
app.use(basePath, require("../routes/debug.js")); // import the debug router using ESM style; fallback to require at runtime if needed
} catch (e) { // (some dev environments may still emit JS commonjs files)
// eslint-disable-next-line @typescript-eslint/no-var-requires
const debugRouter = require("../routes/debug.js").default || require("../routes/debug.js");
app.use(basePath, debugRouter);
} catch (e: any) {
console.error('Failed to mount debug routes (src):', e && e.stack || e); console.error('Failed to mount debug routes (src):', e && e.stack || e);
} }
const frontendPath = config.get("frontendPath").replace(/\/$/, "") + "/", const frontendPath = (config.get("frontendPath") as string).replace(/\/$/, "") + "/",
serverConfig = config.get("server"); serverConfig = config.get("server") as { port: number };
console.log("Hosting server from: " + basePath); console.log("Hosting server from: " + basePath);
let userDB: any, gameDB: any; // DB handles are initialized by the modules below; we don't need file-scoped vars here.
app.use((req, res, next) => { app.use((req: Request, _res: Response, next: NextFunction) => {
console.log(`${req.method} ${req.url}`); console.log(`${req.method} ${req.url}`);
next(); next();
}); });
@ -48,19 +51,27 @@ app.use(bodyParser.json());
app.set("trust proxy", true); app.set("trust proxy", true);
app.set("basePath", basePath); app.set("basePath", basePath);
app.use(basePath, require("../routes/basepath.js")); // basepath is a simple exported string
// eslint-disable-next-line @typescript-eslint/no-var-requires
const basepathRouter = require("../routes/basepath.js").default || require("../routes/basepath.js");
app.use(basePath, basepathRouter);
/* Handle static files first so excessive logging doesn't occur */ /* Handle static files first so excessive logging doesn't occur */
app.use(basePath, express.static(frontendPath, { index: false })); app.use(basePath, express.static(frontendPath, { index: false }));
const index = require("../routes/index"); // index route (may be ESM default export)
// eslint-disable-next-line @typescript-eslint/no-var-requires
const index = require("../routes/index.js").default || require("../routes/index.js");
if (config.has("admin")) { if (config.has("admin")) {
const admin = config.get("admin"); const admin = config.get("admin");
app.set("admin", admin); app.set("admin", admin);
} }
app.use(`${basePath}api/v1/games`, require("../routes/games")); // games router
// eslint-disable-next-line @typescript-eslint/no-var-requires
const gamesRouter = require("../routes/games.js").default || require("../routes/games.js");
app.use(`${basePath}api/v1/games`, gamesRouter);
/* Allow loading of the app w/out being logged in */ /* Allow loading of the app w/out being logged in */
app.use(basePath, index); app.use(basePath, index);
@ -82,11 +93,14 @@ process.on('SIGINT', () => {
server.close(() => process.exit(1)); server.close(() => process.exit(1));
}); });
require("../db/games").then(function(db: any) { // database initializers
gameDB = db; // eslint-disable-next-line @typescript-eslint/no-var-requires
require("../db/games.js").then(function(_db: any) {
// games DB initialized
}).then(function() { }).then(function() {
return require("../db/users").then(function(db: any) { // eslint-disable-next-line @typescript-eslint/no-var-requires
userDB = db; return require("../db/users.js").then(function(_db: any) {
// users DB initialized
}); });
}).then(function() { }).then(function() {
console.log("DB connected. Opening server."); console.log("DB connected. Opening server.");
@ -118,4 +132,4 @@ server.on("error", function(error: any) {
} }
}); });
module.exports = { app, server }; export { app, server };

View File

@ -1,16 +1,14 @@
"use strict"; function twoDigit(number: number): string {
function twoDigit(number) {
return ("0" + number).slice(-2); return ("0" + number).slice(-2);
} }
function timestamp(date) { function timestamp(date?: Date): string {
date = date || new Date(); date = date || new Date();
return [ date.getFullYear(), twoDigit(date.getMonth() + 1), twoDigit(date.getDate()) ].join("-") + return [ date.getFullYear(), twoDigit(date.getMonth() + 1), twoDigit(date.getDate()) ].join("-") +
" " + " " +
[ twoDigit(date.getHours()), twoDigit(date.getMinutes()), twoDigit(date.getSeconds()) ].join(":"); [ twoDigit(date.getHours()), twoDigit(date.getMinutes()), twoDigit(date.getSeconds()) ].join(":");
} }
module.exports = { export {
timestamp timestamp
}; };

View File

@ -1,19 +1,32 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2020", "target": "ES2022",
"module": "CommonJS", "module": "CommonJS",
"outDir": "dist", "outDir": "dist",
"rootDir": "src", "rootDir": ".",
"esModuleInterop": true, "esModuleInterop": true,
"skipLibCheck": true, "skipLibCheck": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"allowJs": true, "allowJs": true,
"strict": false, "strict": true,
"noImplicitAny": false, "noImplicitAny": true,
"moduleResolution": "Node", "moduleResolution": "Node",
"resolveJsonModule": true, "resolveJsonModule": true,
"sourceMap": true "sourceMap": true,
"declaration": true,
"declarationMap": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"allowSyntheticDefaultImports": true,
"exactOptionalPropertyTypes": true,
"noFallthroughCasesInSwitch": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noPropertyAccessFromIndexSignature": true,
"noUncheckedIndexedAccess": true,
"noUnusedLocals": true,
"noUnusedParameters": true
}, },
"include": ["src/**/*"], "include": ["**/*.ts", "**/*.js"],
"exclude": ["node_modules", "dist"] "exclude": ["node_modules", "dist", "test-output"]
} }

19
server/types/express-session.d.ts vendored Normal file
View File

@ -0,0 +1,19 @@
import 'express';
import 'express-session';
declare global {
namespace Express {
interface Request {
// populated by getSessionUser
user?: any;
}
}
}
declare module 'express-session' {
interface SessionData {
userId?: number | null;
}
}
export {};

View File

@ -62,7 +62,7 @@
* | * |
* 12 | 11 10 * 12 | 11 10
*/ */
const Tile = (corners, roads) => { const Tile = (corners: number[], roads: number[]) => {
return { return {
corners: corners, /* 6 */ corners: corners, /* 6 */
pip: -1, pip: -1,
@ -74,7 +74,7 @@ const Tile = (corners, roads) => {
/* Borders have three sections each, so they are numbered /* Borders have three sections each, so they are numbered
* 0-17 clockwise. Some corners share two borders. */ * 0-17 clockwise. Some corners share two borders. */
const Corner = (roads, banks) => { const Corner = (roads: number[], banks: number[]) => {
return { return {
roads: roads, /* max of 3 */ roads: roads, /* max of 3 */
banks: banks, /* max of 2 */ banks: banks, /* max of 2 */
@ -82,7 +82,7 @@ const Corner = (roads, banks) => {
}; };
}; };
const Road = (corners) => { const Road = (corners: number[]) => {
return { return {
corners: corners, /* 2 */ corners: corners, /* 2 */
data: undefined data: undefined
@ -314,7 +314,7 @@ const staticData = {
] ]
}; };
module.exports = { export {
layout, layout,
staticData staticData
}; };

View File

@ -1,34 +1,41 @@
const { layout } = require('./layout.js'); import { layout } from './layout.js';
const isRuleEnabled = (game, rule) => { const isRuleEnabled = (game: any, rule: string): boolean => {
return rule in game.rules && game.rules[rule].enabled; return rule in game.rules && game.rules[rule].enabled;
}; };
const getValidRoads = (game, color) => { const getValidRoads = (game: any, color: string): number[] => {
const limits = []; const limits: number[] = [];
/* For each road, if the road is set, skip it. /* For each road, if the road is set, skip it.
* If no color is set, check the two corners. If the corner * If no color is set, check the two corners. If the corner
* has a matching color, add this to the set. Otherwise skip. * has a matching color, add this to the set. Otherwise skip.
*/ */
layout.roads.forEach((road, roadIndex) => { layout.roads.forEach((road, roadIndex) => {
if (game.placements.roads[roadIndex].color) { if (!game.placements || !game.placements.roads || game.placements.roads[roadIndex]?.color) {
return; return;
} }
let valid = false; let valid = false;
for (let c = 0; !valid && c < road.corners.length; c++) { for (let c = 0; !valid && c < road.corners.length; c++) {
const corner = layout.corners[road.corners[c]], const cornerIndex = road.corners[c] as number;
cornerColor = game.placements.corners[road.corners[c]].color; if (cornerIndex == null || (layout as any).corners[cornerIndex] == null) {
continue;
}
const corner = (layout as any).corners[cornerIndex];
const cornerColor = (game as any).placements && (game as any).placements.corners && (game as any).placements.corners[cornerIndex] && (game as any).placements.corners[cornerIndex].color;
/* Roads do not pass through other player's settlements */ /* Roads do not pass through other player's settlements */
if (cornerColor && cornerColor !== color) { if (cornerColor && cornerColor !== color) {
continue; continue;
} }
for (let r = 0; !valid && r < corner.roads.length; r++) { for (let r = 0; !valid && r < (corner.roads || []).length; r++) {
/* This side of the corner is pointing to the road being validated. Skip it. */ /* This side of the corner is pointing to the road being validated. Skip it. */
if (corner.roads[r] === roadIndex) { if (!corner.roads || corner.roads[r] === roadIndex) {
continue; continue;
} }
if (game.placements.roads[corner.roads[r]].color === color) { const rr = corner.roads[r];
if (rr == null) { continue; }
const placementsRoads = (game as any).placements && (game as any).placements.roads;
if (placementsRoads && placementsRoads[rr] && placementsRoads[rr].color === color) {
valid = true; valid = true;
} }
} }
@ -41,8 +48,8 @@ const getValidRoads = (game, color) => {
return limits; return limits;
} }
const getValidCorners = (game, color, type) => { const getValidCorners = (game: any, color: string, type?: string): number[] => {
const limits = []; const limits: number[] = [];
/* For each corner, if the corner already has a color set, skip it if type /* For each corner, if the corner already has a color set, skip it if type
* isn't set. If type is set, if it is a match, and the color is a match, * isn't set. If type is set, if it is a match, and the color is a match,
@ -77,14 +84,20 @@ const getValidCorners = (game, color, type) => {
valid = true; /* Not filtering based on current player */ valid = true; /* Not filtering based on current player */
} else { } else {
valid = false; valid = false;
for (let r = 0; !valid && r < corner.roads.length; r++) { for (let r = 0; !valid && r < (corner.roads || []).length; r++) {
valid = game.placements.roads[corner.roads[r]].color === color; const rr = corner.roads[r];
if (rr == null) { continue; }
const placementsRoads = (game as any).placements && (game as any).placements.roads;
valid = !!(placementsRoads && placementsRoads[rr] && placementsRoads[rr].color === color);
} }
} }
for (let r = 0; valid && r < corner.roads.length; r++) { for (let r = 0; valid && r < (corner.roads || []).length; r++) {
const road = layout.roads[corner.roads[r]]; if (!corner.roads) { break; }
for (let c = 0; valid && c < road.corners.length; c++) { const ridx = corner.roads[r] as number;
if (ridx == null || (layout as any).roads[ridx] == null) { continue; }
const road = (layout as any).roads[ridx];
for (let c = 0; valid && c < (road.corners || []).length; c++) {
/* This side of the road is pointing to the corner being validated. /* This side of the road is pointing to the corner being validated.
* Skip it. */ * Skip it. */
if (road.corners[c] === cornerIndex) { if (road.corners[c] === cornerIndex) {
@ -92,7 +105,8 @@ const getValidCorners = (game, color, type) => {
} }
/* There is a settlement within one segment from this /* There is a settlement within one segment from this
* corner, so it is invalid for settlement placement */ * corner, so it is invalid for settlement placement */
if (game.placements.corners[road.corners[c]].color) { const cc = road.corners[c] as number;
if ((game as any).placements && (game as any).placements.corners && (game as any).placements.corners[cc] && (game as any).placements.corners[cc].color) {
valid = false; valid = false;
} }
} }
@ -103,7 +117,7 @@ const getValidCorners = (game, color, type) => {
* on the volcano) */ * on the volcano) */
if (!(game.state === 'initial-placement' if (!(game.state === 'initial-placement'
&& isRuleEnabled(game, 'volcano') && isRuleEnabled(game, 'volcano')
&& layout.tiles[game.robber].corners.indexOf(cornerIndex) !== -1 && (layout as any).tiles && (layout as any).tiles[(game as any).robber] && Array.isArray((layout as any).tiles[(game as any).robber].corners) && (layout as any).tiles[(game as any).robber].corners.indexOf(cornerIndex as number) !== -1
)) { )) {
limits.push(cornerIndex); limits.push(cornerIndex);
} }
@ -113,7 +127,7 @@ const getValidCorners = (game, color, type) => {
return limits; return limits;
} }
module.exports = { export {
getValidCorners, getValidCorners,
getValidRoads, getValidRoads,
isRuleEnabled isRuleEnabled