diff --git a/server/ai/app.js b/server/ai/app.ts similarity index 98% rename from server/ai/app.js rename to server/ai/app.ts index 855fd35..81b5bc9 100644 --- a/server/ai/app.js +++ b/server/ai/app.ts @@ -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); }; diff --git a/server/ai/longest-road.js b/server/ai/longest-road.ts similarity index 85% rename from server/ai/longest-road.js rename to server/ai/longest-road.ts index 317fc8c..434dbbf 100644 --- a/server/ai/longest-road.js +++ b/server/ai/longest-road.ts @@ -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; \ No newline at end of file +export default calculateRoadLengths; \ No newline at end of file diff --git a/server/app.js b/server/app.ts similarity index 80% rename from server/app.js rename to server/app.ts index 17c61f5..21099f5 100755 --- a/server/app.js +++ b/server/app.ts @@ -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 }; diff --git a/server/basepath.js b/server/basepath.ts similarity index 91% rename from server/basepath.js rename to server/basepath.ts index 207bf08..37f621b 100755 --- a/server/basepath.js +++ b/server/basepath.ts @@ -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 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; diff --git a/server/console-line.js b/server/console-line.js deleted file mode 100755 index a6f33b9..0000000 --- a/server/console-line.js +++ /dev/null @@ -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); - }; - })(); - }); -} diff --git a/server/console-line.ts b/server/console-line.ts new file mode 100755 index 0000000..4edc689 --- /dev/null +++ b/server/console-line.ts @@ -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); + }; + })(); + }); +} diff --git a/server/db/games.js b/server/db/games.js deleted file mode 100755 index b487f54..0000000 --- a/server/db/games.js +++ /dev/null @@ -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(); diff --git a/server/db/users.js b/server/db/users.js deleted file mode 100755 index 00e5dae..0000000 --- a/server/db/users.js +++ /dev/null @@ -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(); diff --git a/server/lib/mail.js b/server/lib/mail.ts similarity index 73% rename from server/lib/mail.js rename to server/lib/mail.ts index 3cc10c2..3ab6e8b 100644 --- a/server/lib/mail.js +++ b/server/lib/mail.ts @@ -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(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(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(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 } diff --git a/server/mail.js b/server/mail.ts similarity index 60% rename from server/mail.js rename to server/mail.ts index 87f741e..28786fc 100755 --- a/server/mail.js +++ b/server/mail.ts @@ -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 { + let envelope: any = { subject: subject, from: 'Ketr.Ketran ', 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 }; diff --git a/server/package.json b/server/package.json index 09f4585..f234a4a 100644 --- a/server/package.json +++ b/server/package.json @@ -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", diff --git a/server/routes/basepath.js b/server/routes/basepath.ts similarity index 61% rename from server/routes/basepath.js rename to server/routes/basepath.ts index eba896d..504ec2c 100644 --- a/server/routes/basepath.js +++ b/server/routes/basepath.ts @@ -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 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( / 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( /