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');
const WebSocket = require('ws');
const fs = require('fs').promises;
const calculateLongestRoad = require('./longest-road.js');
// @ts-nocheck
import fetch from 'node-fetch';
import WebSocket from 'ws';
import fs from 'fs';
import calculateLongestRoad from './longest-road.js';
const { getValidRoads, getValidCorners } = require('../util/validLocations.js');
const { layout, staticData } = require('../util/layout.js');
import { getValidRoads, getValidCorners } from '../util/validLocations.js';
import { layout, staticData } from '../util/layout.js';
const version = '0.0.1';
@ -24,14 +25,14 @@ const server = process.argv[2];
const gameId = process.argv[3];
const name = process.argv[4];
const game = {};
const game: any = {};
const anyValue = undefined;
process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0;
/* Do not use arrow function as this is rebound to have
* this as the WebSocket */
let send = function (data) {
let send = function (this: WebSocket, data: any) {
if (data.type === 'get') {
console.log(`ws - send: get`, data.fields);
} else {
@ -40,7 +41,7 @@ let send = function (data) {
this.send(JSON.stringify(data));
};
const error = (e) => {
const error = (e: any) => {
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 (placedCorner.color && placedCorner.color !== color) {
return 0;
@ -13,7 +14,7 @@ const processCorner = (game, color, cornerIndex, placedCorner) => {
placedCorner.walking = true;
/* Calculate the longest road branching from both corners */
let longest = 0;
layout.corners[cornerIndex].roads.forEach(roadIndex => {
layout.corners[cornerIndex].roads.forEach((roadIndex: number) => {
const placedRoad = game.placements.roads[roadIndex];
if (placedRoad.walking) {
return;
@ -32,7 +33,7 @@ const processCorner = (game, color, cornerIndex, placedCorner) => {
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 (placedCorner.color && placedCorner.color !== color) {
return;
@ -44,13 +45,13 @@ const buildCornerGraph = (game, color, cornerIndex, placedCorner, set) => {
placedCorner.walking = true;
/* 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];
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 (placedRoad.color !== color) {
return 0;
@ -75,7 +76,7 @@ const processRoad = (game, color, roadIndex, placedRoad) => {
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 (placedRoad.color !== color) {
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 */
layout.roads.forEach((item, itemIndex) => {
delete game.placements.roads[itemIndex].walking;
@ -106,7 +107,7 @@ const clearRoadWalking = (game) => {
});
}
const calculateRoadLengths = (game) => {
const calculateRoadLengths = (game: any) => {
const color = game.color;
clearRoadWalking(game);
@ -158,4 +159,4 @@ const calculateRoadLengths = (game) => {
return final;
};
module.exports = calculateRoadLengths;
export default calculateRoadLengths;

View File

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

View File

@ -1,5 +1,6 @@
const fs = require('fs');
let basePathRaw = process.env.VITE_BASEPATH || '';
import fs from 'fs';
let basePathRaw = process.env['VITE_BASEPATH'] || '';
// If env not provided, try to detect a <base href="..."> in the
// built client's index.html (if present). This helps when the
@ -29,4 +30,4 @@ if (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";
const config = require("config"),
crypto = require("crypto"),
hb = require("handlebars");
import config from "config";
import crypto from "crypto";
import hb from "handlebars";
const templates = {
"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'", {
replacements: {
id: user.id
}
}).then(function() {
return new Promise(function(resolve, reject) {
return new Promise<string>(function(resolve, reject) {
crypto.randomBytes(16, function(error, buffer) {
if (error) {
return reject(error);
@ -66,7 +66,7 @@ const sendVerifyMail = function(userDB, req, user) {
return resolve(buffer.toString('hex'));
});
});
}).then(function(secret) {
}).then(function(secret: string) {
return userDB.sequelize.query(
"INSERT INTO authentications " +
"(userId,issued,key,type) " +
@ -77,11 +77,11 @@ const sendVerifyMail = function(userDB, req, user) {
}
}).then(function() {
return secret;
}).catch(function(error) {
}).catch(function(error: any) {
console.log(error);
throw error;
});
}).then(function(secret) {
}).then(function(secret: string) {
const transporter = req.app.get("transporter");
if (!transporter) {
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),
html: hb.compile(templates.verify.html)(data)
};
return new Promise(function (resolve, reject) {
return new Promise<void>(function (resolve, reject) {
let attempts = 10;
function send(envelope) {
/* Rate limit to ten per second */
transporter.sendMail(envelope, function (error, info) {
if (!error) {
console.log('Message sent: ' + info.response);
return resolve();
}
function send(envelope: any) {
/* Rate limit to ten per second */
transporter.sendMail(envelope, function (error: any, info: any) {
if (!error) {
console.log('Message sent: ' + (info && info.response));
resolve();
return;
}
if (attempts == 0) {
console.log("Error sending email: ", error);
return reject(error);
}
if (attempts == 0) {
console.log("Error sending email: ", error);
return reject(error);
}
attempts--;
console.log("Unable to send mail. Trying again in 100ms (" + attempts + " attempts remain): ", error);
setTimeout(send.bind(undefined, envelope), 100);
});
}
attempts--;
console.log("Unable to send mail. Trying again in 100ms (" + attempts + " attempts remain): ", error);
setTimeout(send.bind(undefined, envelope), 100);
});
}
send(envelope);
});
}).catch(function(error) {
}).catch(function(error: any) {
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");
if (!transporter) {
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),
html: hb.compile(templates.password.html)(data)
};
return new Promise(function (resolve, reject) {
return new Promise<void>(function (resolve, reject) {
let attempts = 10;
function send(envelope) {
function send(envelope: any) {
/* Rate limit to ten per second */
transporter.sendMail(envelope, function (error, info) {
transporter.sendMail(envelope, function (error: any, info: any) {
if (!error) {
console.log('Message sent: ' + info.response);
console.log('Message sent: ' + (info && info.response));
return resolve();
}
@ -177,7 +178,7 @@ const sendPasswordChangedMail = function(userDB, req, user) {
});
};
module.exports = {
export {
sendVerifyMail,
sendPasswordChangedMail
}

View File

@ -1,7 +1,7 @@
"use strict";
const createTransport = require('nodemailer').createTransport,
{ timestamp } = require("./timestamp");
import { createTransport } from 'nodemailer';
import { timestamp } from "./timestamp";
const transporter = createTransport({
host: 'email.ketrenos.com',
@ -9,8 +9,8 @@ const transporter = createTransport({
port: 25
});
function sendMail(to, subject, message, cc) {
let envelope = {
function sendMail(to: string, subject: string, message: string, cc?: string): Promise<boolean> {
let envelope: any = {
subject: subject,
from: 'Ketr.Ketran <james_ketran@ketrenos.com>',
to: to || '',
@ -29,33 +29,29 @@ function sendMail(to, subject, message, cc) {
return new Promise(function (resolve, reject) {
let attempts = 10;
function attemptSend(envelope) {
function attemptSend(envelope: any) {
/* Rate limit to ten per second */
transporter.sendMail(envelope, function (error, info) {
transporter.sendMail(envelope, function (error, _info) {
if (error) {
if (attempts) {
attempts--;
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 {
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);
}).then(function(success) {
if (!success) {
console.error(timestamp() + " Mail not sent to: " + envelope.to);
}
});
}
module.exports = {
sendMail: sendMail
export {
sendMail
};

View File

@ -1,9 +1,9 @@
{
"name": "peddlers-of-ketran-server",
"version": "1.0.0",
"main": "app.js",
"main": "dist/src/app.js",
"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",
"build": "tsc -p tsconfig.json",
"start:dev": "ts-node-dev --respawn --transpile-only src/app.ts",
@ -36,9 +36,25 @@
"ws": "^8.5.0"
},
"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/moment": "^2.13.0",
"@types/morgan": "^1.9.5",
"@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/ws": "^8.5.5",
"jest": "^29.7.0",
"supertest": "^6.3.3",
"ts-jest": "^29.1.0",

View File

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

View File

@ -1,6 +1,6 @@
"use strict";
const express = require("express");
import express from "express";
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
* hit: /<basePath>/__debug/request
*/
router.get('/__debug/request', (req, res) => {
router.get('/__debug/request', (req: express.Request, res: express.Response) => {
try {
console.log('[debug] __debug/request hit:', req.method, req.originalUrl);
// 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,
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);
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"),
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');
import { getValidRoads, getValidCorners, isRuleEnabled } from '../util/validLocations.js';
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_CITIES = 4;
const MAX_ROADS = 15;
const types = [ 'wheat', 'stone', 'sheep', 'wood', 'brick' ];
const types: string[] = [ 'wheat', 'stone', 'sheep', 'wood', 'brick' ];
const debug = {
audio: false,
@ -32,9 +39,9 @@ const debug = {
// 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
// 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 };
let parsed = null;
let parsed: unknown = null;
try {
if (typeof msg === 'string') {
parsed = JSON.parse(msg);
@ -46,7 +53,7 @@ function normalizeIncoming(msg) {
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
// properties like `name` live at the root.
const data = parsed.data || (Object.keys(parsed).length ? Object.assign({}, parsed) : null);
@ -82,14 +89,14 @@ function shuffleArray(array) {
const games = {};
const audio = {};
const processTies = (players) => {
const processTies = (players: Player[]) => {
/* Sort the players into buckets based on their
* order, and their current roll. If a resulting
* roll array has more than one element, then there
* is a tie that must be resolved */
let slots = [];
players.forEach(player => {
let slots: Player[][] = [];
players.forEach((player: Player) => {
if (!slots[player.order]) {
slots[player.order] = [];
}
@ -98,13 +105,13 @@ const processTies = (players) => {
let ties = false, position = 1;
const irstify = (position) => {
const irstify = (position: number): string => {
switch (position) {
case 1: return `1st`;
case 2: return `2nd`;
case 3: return `3rd`;
case 4: return `4th`;
default: return position;
default: return position.toString();
}
}
@ -112,7 +119,7 @@ const processTies = (players) => {
slots.reverse().forEach((slot) => {
if (slot.length !== 1) {
ties = true;
slot.forEach(player => {
slot.forEach((player: Player) => {
player.orderRoll = 0; /* Ties have to be re-rolled */
player.position = 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";
const express = require("express"),
fs = require("fs"),
url = require("url"),
config = require("config"),
basePath = require("../basepath");
import express from "express";
import fs from "fs";
import url from "url";
import config from "config";
import basePath from "../basepath";
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
* 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);
/* If req.user isn't set yet (authentication hasn't happened) then
* only allow / to be loaded--everything else chains to the next
* handler */
if (!req.user &&
if (!(req as any).user &&
req.url != "/" &&
req.url.indexOf("/games") != 0) {
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);
/* Replace <script>'<base href="BASEPATH">';</script> in index.html with
* the basePath */
const frontendPath = config.get("frontendPath").replace(/\/$/, "") + "/",
const frontendPath = (config.get("frontendPath") as string).replace(/\/$/, "") + "/",
index = fs.readFileSync(frontendPath + "index.html", "utf8");
res.send(index.replace(
/<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";
const express = require("express"),
config = require("config"),
{ sendVerifyMail, sendPasswordChangedMail } = require("../lib/mail"),
crypto = require("crypto");
import express from "express";
import config from "config";
import { sendVerifyMail, sendPasswordChangedMail } from "../lib/mail";
import crypto from "crypto";
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;
});
router.get("/", function(req, res/*, next*/) {
router.get("/", function(req: express.Request, res: express.Response/*, next*/) {
console.log("/users/");
return getSessionUser(req).then((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");
const q = req.query as any;
const changes = {
currentPassword: req.query.c || req.body.c,
newPassword: req.query.n || req.body.n
currentPassword: q.c || req.body.c,
newPassword: q.n || req.body.n
};
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 getSessionUser(req).then(function(user) {
return getSessionUser(req).then(function(user: any) {
return userDB.sequelize.query("SELECT id FROM users " +
"WHERE uid=:username AND password=:password", {
replacements: {
@ -48,13 +48,13 @@ router.put("/password", function(req, res) {
},
type: userDB.Sequelize.QueryTypes.SELECT,
raw: true
}).then(function(users) {
}).then(function(users: any) {
if (users.length != 1) {
return null;
}
return user;
});
}).then(function(user) {
}).then(function(user: any) {
if (!user) {
console.log("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");
const q = req.query as any;
const user = {
uid: req.query.m || req.body.m,
displayName: req.query.n || req.body.n || "",
password: req.query.p || req.body.p || "",
mail: req.query.m || req.body.m,
notes: req.query.w || req.body.w || ""
uid: q.m || req.body.m,
displayName: q.n || req.body.n || "",
password: q.p || req.body.p || "",
mail: q.m || req.body.m,
notes: q.w || req.body.w || ""
};
if (!user.uid || !user.password || !user.displayName || !user.notes) {
@ -98,37 +98,38 @@ router.post("/create", function(req, res) {
replacements: user,
type: userDB.Sequelize.QueryTypes.SELECT,
raw: true
}).then(function(results) {
}).then(function(results: any) {
if (results.length != 0) {
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)) {
console.log("Invalid email address: " + user.mail);
throw "Invalid email address.";
}
return;
}).then(function() {
return userDB.sequelize.query("INSERT INTO users " +
"(uid,displayName,password,mail,memberSince,authenticated,notes) " +
"VALUES(:uid,:displayName,:password,:mail,CURRENT_TIMESTAMP,0,:notes)", {
replacements: user
}).spread(function(results, metadata) {
}).spread(function(_results: any, metadata: any) {
req.session.userId = metadata.lastID;
}).then(function() {
return getSessionUser(req).then(function(user) {
return getSessionUser(req).then(function(user: any) {
res.status(200).send(user);
user.id = req.session.userId;
return sendVerifyMail(userDB, req, user);
});
}).catch(function(error) {
}).catch(function(error: any) {
console.log("Error creating account: ", error);
return res.status(401).send(error);
});
});
});
const getSessionUser = function(req) {
const getSessionUser = function(req: express.Request): Promise<any> {
return Promise.resolve().then(function() {
if (!req.session || !req.session.userId) {
throw "Unauthorized. You must be logged in.";
@ -143,7 +144,7 @@ const getSessionUser = function(req) {
},
type: userDB.Sequelize.QueryTypes.SELECT,
raw: true
}).then(function(results) {
}).then(function(results: any) {
if (results.length != 1) {
throw "Invalid account.";
}
@ -160,9 +161,9 @@ const getSessionUser = function(req) {
return user;
}
return user;
return user;
});
}).then(function(user) {
}).then(function(user: any) {
req.user = user;
/* If the user already has a restriction, or there are no album user restrictions,
@ -171,7 +172,7 @@ const getSessionUser = function(req) {
return user;
}
let allowed = config.get("restrictions");
let allowed: any = config.get("restrictions");
if (!Array.isArray(allowed)) {
allowed = [ allowed ];
}
@ -187,8 +188,8 @@ const getSessionUser = function(req) {
}).then(function(user) {
/* If there are maintainers on this album, check if this user is a maintainer */
if (config.has("maintainers")) {
let maintainers = config.get("maintainers");
if (maintainers.indexOf(user.username) != -1) {
let maintainers: any = config.get("maintainers");
if (Array.isArray(maintainers) && maintainers.indexOf(user.username) != -1) {
user.maintainer = true;
if (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");
const q = req.query as any;
let username = req.query.u || req.body.u || "",
password = req.query.p || req.body.p || "";
let username = q.u || req.body.u || "",
password = q.p || req.body.p || "";
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 new Promise((reject, resolve) => {
return new Promise((resolve, _reject) => {
console.log("Looking up user in DB.");
let query = "SELECT " +
"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')
},
type: userDB.Sequelize.QueryTypes.SELECT
}).then(function(users) {
}).then(function(users: any) {
if (users.length != 1) {
return resolve(null);
}
@ -243,7 +245,7 @@ router.post("/login", function(req, res) {
req.session.userId = user.id;
return resolve(user);
});
}).then(function(user) {
}).then(function(user: any) {
if (!user) {
console.log(username + " not found (or invalid password.)");
req.session.userId = null;
@ -259,7 +261,7 @@ router.post("/login", function(req, res) {
console.log(message);
}
return getSessionUser(req).then(function(user) {
return getSessionUser(req).then(function(user: any) {
return res.status(200).send(user);
});
}).catch(function(error) {
@ -277,7 +279,7 @@ router.get("/logout", function(req, res) {
res.status(200).send({});
});
module.exports = {
export {
router,
getSessionUser
};

View File

@ -1,42 +1,45 @@
/* eslint-disable @typescript-eslint/no-var-requires */
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";
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 server = require("http").createServer(app);
const server = http.createServer(app);
app.use(cookieParser());
const ws = require('express-ws')(app, server);
expressWs(app, server);
require("../console-line.js"); /* Monkey-patch console.log with line numbers */
// Temporary debug routes (dev-only). Mount before static so we can
// inspect what the server receives for base-prefixed requests.
try {
app.use(basePath, require("../routes/debug.js"));
} catch (e) {
// import the debug router using ESM style; fallback to require at runtime if needed
// (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);
}
const frontendPath = config.get("frontendPath").replace(/\/$/, "") + "/",
serverConfig = config.get("server");
const frontendPath = (config.get("frontendPath") as string).replace(/\/$/, "") + "/",
serverConfig = config.get("server") as { port: number };
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}`);
next();
});
@ -48,19 +51,27 @@ app.use(bodyParser.json());
app.set("trust proxy", true);
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 */
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")) {
const admin = config.get("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 */
app.use(basePath, index);
@ -82,11 +93,14 @@ process.on('SIGINT', () => {
server.close(() => process.exit(1));
});
require("../db/games").then(function(db: any) {
gameDB = db;
// database initializers
// eslint-disable-next-line @typescript-eslint/no-var-requires
require("../db/games.js").then(function(_db: any) {
// games DB initialized
}).then(function() {
return require("../db/users").then(function(db: any) {
userDB = db;
// eslint-disable-next-line @typescript-eslint/no-var-requires
return require("../db/users.js").then(function(_db: any) {
// users DB initialized
});
}).then(function() {
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) {
function twoDigit(number: number): string {
return ("0" + number).slice(-2);
}
function timestamp(date) {
function timestamp(date?: Date): string {
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(":");
}
module.exports = {
export {
timestamp
};

View File

@ -1,19 +1,32 @@
{
"compilerOptions": {
"target": "ES2020",
"target": "ES2022",
"module": "CommonJS",
"outDir": "dist",
"rootDir": "src",
"rootDir": ".",
"esModuleInterop": true,
"skipLibCheck": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"allowJs": true,
"strict": false,
"noImplicitAny": false,
"allowJs": true,
"strict": true,
"noImplicitAny": true,
"moduleResolution": "Node",
"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/**/*"],
"exclude": ["node_modules", "dist"]
"include": ["**/*.ts", "**/*.js"],
"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
*/
const Tile = (corners, roads) => {
const Tile = (corners: number[], roads: number[]) => {
return {
corners: corners, /* 6 */
pip: -1,
@ -74,7 +74,7 @@ const Tile = (corners, roads) => {
/* Borders have three sections each, so they are numbered
* 0-17 clockwise. Some corners share two borders. */
const Corner = (roads, banks) => {
const Corner = (roads: number[], banks: number[]) => {
return {
roads: roads, /* max of 3 */
banks: banks, /* max of 2 */
@ -82,7 +82,7 @@ const Corner = (roads, banks) => {
};
};
const Road = (corners) => {
const Road = (corners: number[]) => {
return {
corners: corners, /* 2 */
data: undefined
@ -314,7 +314,7 @@ const staticData = {
]
};
module.exports = {
export {
layout,
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;
};
const getValidRoads = (game, color) => {
const limits = [];
const getValidRoads = (game: any, color: string): number[] => {
const limits: number[] = [];
/* For each road, if the road is set, skip it.
* If no color is set, check the two corners. If the corner
* has a matching color, add this to the set. Otherwise skip.
*/
layout.roads.forEach((road, roadIndex) => {
if (game.placements.roads[roadIndex].color) {
if (!game.placements || !game.placements.roads || game.placements.roads[roadIndex]?.color) {
return;
}
let valid = false;
for (let c = 0; !valid && c < road.corners.length; c++) {
const corner = layout.corners[road.corners[c]],
cornerColor = game.placements.corners[road.corners[c]].color;
for (let c = 0; !valid && c < road.corners.length; c++) {
const cornerIndex = road.corners[c] as number;
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 */
if (cornerColor && cornerColor !== color) {
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. */
if (corner.roads[r] === roadIndex) {
if (!corner.roads || corner.roads[r] === roadIndex) {
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;
}
}
@ -41,8 +48,8 @@ const getValidRoads = (game, color) => {
return limits;
}
const getValidCorners = (game, color, type) => {
const limits = [];
const getValidCorners = (game: any, color: string, type?: string): number[] => {
const limits: number[] = [];
/* 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,
@ -77,14 +84,20 @@ const getValidCorners = (game, color, type) => {
valid = true; /* Not filtering based on current player */
} else {
valid = false;
for (let r = 0; !valid && r < corner.roads.length; r++) {
valid = game.placements.roads[corner.roads[r]].color === color;
for (let r = 0; !valid && r < (corner.roads || []).length; r++) {
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++) {
const road = layout.roads[corner.roads[r]];
for (let c = 0; valid && c < road.corners.length; c++) {
for (let r = 0; valid && r < (corner.roads || []).length; r++) {
if (!corner.roads) { break; }
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.
* Skip it. */
if (road.corners[c] === cornerIndex) {
@ -92,7 +105,8 @@ const getValidCorners = (game, color, type) => {
}
/* There is a settlement within one segment from this
* 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;
}
}
@ -103,7 +117,7 @@ const getValidCorners = (game, color, type) => {
* on the volcano) */
if (!(game.state === 'initial-placement'
&& 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);
}
@ -113,7 +127,7 @@ const getValidCorners = (game, color, type) => {
return limits;
}
module.exports = {
export {
getValidCorners,
getValidRoads,
isRuleEnabled