diff --git a/client/index.html b/client/index.html index a85ecc941..b6cfbe6a3 100644 --- a/client/index.html +++ b/client/index.html @@ -8,7 +8,8 @@ - + + diff --git a/client/package-lock.json b/client/package-lock.json index 137215b14..6a9792914 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -33,7 +33,9 @@ }, "devDependencies": { "@vitejs/plugin-vue": "^5.1.4", + "@vue/tsconfig": "^0.5.1", "less": "^3.13.1", + "typescript": "^5.6.3", "vite": "^5.4.8" } }, @@ -1220,6 +1222,13 @@ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.31.tgz", "integrity": "sha512-Yp3wtJk//8cO4NItOPpi3QkLExAr/aLBGZMmTtW9WpdwBCJpRM6zj9WgWktXAl8IDIozwNMByT45JP3tO3ACWA==" }, + "node_modules/@vue/tsconfig": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.5.1.tgz", + "integrity": "sha512-VcZK7MvpjuTPx2w6blwnwZAu5/LgBUtejFOi3pPGQFXQN5Ela03FUtd2Qtg4yWGGissVL0dr6Ro1LfOFh+PCuQ==", + "dev": true, + "license": "MIT" + }, "node_modules/after": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", @@ -2171,6 +2180,20 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/url": { "version": "0.11.3", "resolved": "https://registry.npmjs.org/url/-/url-0.11.3.tgz", diff --git a/client/package.json b/client/package.json index 2668a4dfe..e61b0c790 100644 --- a/client/package.json +++ b/client/package.json @@ -7,7 +7,8 @@ "dev": "vite", "dev:network": "vite --host", "build": "vite build", - "serve": "vite preview" + "serve": "vite preview", + "check": "tsc --noEmit" }, "dependencies": { "@pixi/graphics-extras": "^7.4.0", @@ -35,7 +36,9 @@ }, "devDependencies": { "@vitejs/plugin-vue": "^5.1.4", + "@vue/tsconfig": "^0.5.1", "less": "^3.13.1", + "typescript": "^5.6.3", "vite": "^5.4.8" } } diff --git a/client/public/favicon_dark.ico b/client/public/favicon_dark.ico new file mode 100644 index 000000000..0a7beec91 Binary files /dev/null and b/client/public/favicon_dark.ico differ diff --git a/client/src/App.vue b/client/src/App.vue index 7a8a4f859..b6bb14292 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -1,18 +1,12 @@ - diff --git a/client/src/views/components/ViewCollapsePanel.vue b/client/src/views/components/ViewCollapsePanel.vue index 01ff4f1d0..ff12b1f62 100644 --- a/client/src/views/components/ViewCollapsePanel.vue +++ b/client/src/views/components/ViewCollapsePanel.vue @@ -34,6 +34,7 @@ export default { methods: { toggle () { this.isCollapsed = !this.isCollapsed; + this.$emit('onToggle', this.isCollapsed); } }, computed: { diff --git a/client/src/views/components/ViewContainer.vue b/client/src/views/components/ViewContainer.vue index cb0a3e312..9cda6382b 100644 --- a/client/src/views/components/ViewContainer.vue +++ b/client/src/views/components/ViewContainer.vue @@ -49,6 +49,11 @@ export default { components: { 'logo': LogoVue, 'view-container-top-bar': ViewContainerTopBarVue + }, + async mounted() { + if (!this.$store.state.userId) { + await this.$store.dispatch('verify') + } } } diff --git a/client/src/views/components/modal/ConfirmationDialog.vue b/client/src/views/components/modal/ConfirmationDialog.vue index 2fb3f6125..69da9a654 100644 --- a/client/src/views/components/modal/ConfirmationDialog.vue +++ b/client/src/views/components/modal/ConfirmationDialog.vue @@ -1,5 +1,5 @@ - diff --git a/client/src/views/game/GamePlayerControl.vue b/client/src/views/game/GamePlayerControl.vue new file mode 100644 index 000000000..150402529 --- /dev/null +++ b/client/src/views/game/GamePlayerControl.vue @@ -0,0 +1,51 @@ + + + \ No newline at end of file diff --git a/client/src/views/game/components/intel/Intel.vue b/client/src/views/game/components/intel/Intel.vue index 8dc92c360..695084e45 100644 --- a/client/src/views/game/components/intel/Intel.vue +++ b/client/src/views/game/components/intel/Intel.vue @@ -145,7 +145,7 @@ export default { alias: p.alias, shape: p.shape, defeated: p.defeated, - colour: isCurrentPlayer ? '#FFFFFF' : GameHelper.getFriendlyColour(p.colour.value) + colour: isCurrentPlayer ? '#FFFFFF' : this.$store.getters.getColourForPlayer(p._id).value } }) diff --git a/client/src/views/game/components/menu/HamburgerMenu.vue b/client/src/views/game/components/menu/HamburgerMenu.vue index ac98f8107..a0f2f4bce 100644 --- a/client/src/views/game/components/menu/HamburgerMenu.vue +++ b/client/src/views/game/components/menu/HamburgerMenu.vue @@ -38,7 +38,7 @@ Notes Spectators - Intel + Intel Options How to Play My Games diff --git a/client/src/views/game/components/star/StarIcon.vue b/client/src/views/game/components/star/StarIcon.vue index b29bbbb4c..2c0913afb 100644 --- a/client/src/views/game/components/star/StarIcon.vue +++ b/client/src/views/game/components/star/StarIcon.vue @@ -39,7 +39,7 @@ export default { return new URL(`../../../../assets/map-objects/128x128_star_scannable_binary.svg`, import.meta.url).href; } else if (this.isNebula) { - return `mask-image: url(${new URL(`../../../../assets/nebula/neb0-starless-bright.png`, import.meta.url)});`; + return `mask-image: url(${new URL(`../../../../assets/nebula/neb0-starless-bright.png`, import.meta.url)}); -webkit-mask-image: url(${new URL(`../../../../assets/nebula/neb0-starless-bright.png`, import.meta.url)});`; } else if (this.isBlackHole) { return new URL(`../../../../assets/map-objects/128x128_star_black_hole.svg`, import.meta.url).href; @@ -48,7 +48,7 @@ export default { return new URL(`../../../../assets/stars/128x128_star_pulsar.svg`, import.meta.url).href; } else if (this.isWormHole) { - return `mask-image: url(${new URL(`../../../../assets/stars/vortex.png`, import.meta.url).href});`; + return `mask-image: url(${new URL(`../../../../assets/stars/vortex.png`, import.meta.url).href}); -webkit-mask-image: url(${new URL(`../../../../assets/stars/vortex.png`, import.meta.url).href});`; } else { return new URL(`../../../../assets/map-objects/128x128_star_scannable.svg`, import.meta.url).href; @@ -63,13 +63,13 @@ export default { width: 15px; height: 15px; } - .star-svg:deep(.star) { + .star-svg .star { fill: currentColor; } - .star-svg:deep(.pulsar) { + .star-svg .pulsar { stroke: currentColor; } - .star-svg:deep(.black-hole) { + .star-svg .black-hole { fill: transparent; stroke: currentColor; } @@ -80,6 +80,9 @@ export default { mask-repeat: no-repeat; mask-position: center; mask-size: 100%; + -webkit-mask-repeat: no-repeat; + -webkit-mask-position: center; + -webkit-mask-size: 100%; } .nebulaIcon, .wormHoleIcon { diff --git a/client/tsconfig.json b/client/tsconfig.json new file mode 100644 index 000000000..b47377074 --- /dev/null +++ b/client/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json" +} \ No newline at end of file diff --git a/server/api/controllers/auth.ts b/server/api/controllers/auth.ts index 208a69a38..aee718ff1 100644 --- a/server/api/controllers/auth.ts +++ b/server/api/controllers/auth.ts @@ -1,6 +1,9 @@ import { DependencyContainer } from '../../services/types/DependencyContainer'; +import {logger} from "../../utils/logging"; const axios = require('axios'); +const log = logger("Auth Controller"); + export default (container: DependencyContainer) => { return { login: async (req, res, next) => { @@ -92,7 +95,7 @@ export default (container: DependencyContainer) => { } catch (error) { // NOTE: An unauthorized token will not throw an error; // it will return a 401 Unauthorized response in the try block above - console.error(error); + log.error(error); } } diff --git a/server/api/controllers/carrier.ts b/server/api/controllers/carrier.ts index b97fb95ee..d544bf6c3 100644 --- a/server/api/controllers/carrier.ts +++ b/server/api/controllers/carrier.ts @@ -1,11 +1,11 @@ import { DependencyContainer } from '../../services/types/DependencyContainer'; -import { mapToCarrierCalculateCombatRequest, mapToCarrierLoopWaypointsRequest, mapToCarrierRenameCarrierRequest, mapToCarrierSaveWaypointsRequest, mapToCarrierTransferShipsRequest } from '../requests/carrier'; +import { mapToCarrierCalculateCombatRequest, parseCarrierLoopWaypointsRequest, mapToCarrierRenameCarrierRequest, parseCarierSaveWaypointsRequest, parseCarrierTransferShipsRequest } from '../requests/carrier'; export default (container: DependencyContainer) => { return { saveWaypoints: async (req, res, next) => { try { - const reqObj = mapToCarrierSaveWaypointsRequest(req.body); + const reqObj = parseCarierSaveWaypointsRequest(req.body); let report = await container.waypointService.saveWaypoints( req.game, @@ -22,7 +22,7 @@ export default (container: DependencyContainer) => { }, loopWaypoints: async (req, res, next) => { try { - const reqObj = mapToCarrierLoopWaypointsRequest(req.body); + const reqObj = parseCarrierLoopWaypointsRequest(req.body); await container.waypointService.loopWaypoints( req.game, @@ -38,7 +38,7 @@ export default (container: DependencyContainer) => { }, transferShips: async (req, res, next) => { try { - const reqObj = mapToCarrierTransferShipsRequest(req.body); + const reqObj = parseCarrierTransferShipsRequest(req.body); await container.shipTransferService.transfer( req.game, diff --git a/server/api/controllers/game.ts b/server/api/controllers/game.ts index ddc5abbfd..5b9fd816b 100644 --- a/server/api/controllers/game.ts +++ b/server/api/controllers/game.ts @@ -1,6 +1,9 @@ import ValidationError from '../../errors/validation'; import { DependencyContainer } from '../../services/types/DependencyContainer'; -import { mapToGameConcedeDefeatRequest, mapToGameJoinGameRequest, mapToGameSaveNotesRequest } from '../requests/game'; +import { mapToGameConcedeDefeatRequest, mapToGameJoinGameRequest, mapToGameSaveNotesRequest, parseKickPlayerRequest } from '../requests/game'; +import {logger} from "../../utils/logging"; + +const log = logger("Game Controller"); export default (container: DependencyContainer) => { return { @@ -53,7 +56,7 @@ export default (container: DependencyContainer) => { res.status(201).json(game._id); return next(); } catch (err) { - console.error(err); + log.error(err); return next(err); } }, @@ -412,6 +415,18 @@ export default (container: DependencyContainer) => { return next(err); } }, + kickPlayer: async (req, res, next) => { + try { + const params = parseKickPlayerRequest(req.body); + + await container.gameService.kickPlayer(req.game, req.session.userId, params.playerId); + + res.sendStatus(200); + return next(); + } catch (err) { + return next(err); + } + }, getPlayerUser: async (req, res, next) => { try { let user = await container.gameService.getPlayerUser( diff --git a/server/api/controllers/shop.ts b/server/api/controllers/shop.ts index d4d018f1d..309917d75 100644 --- a/server/api/controllers/shop.ts +++ b/server/api/controllers/shop.ts @@ -1,8 +1,11 @@ import ValidationError from '../../errors/validation'; import { DependencyContainer } from '../../services/types/DependencyContainer'; +import {logger} from "../../utils/logging"; const COST_PER_TOKEN = 1; +const log = logger("Shop Controller"); + export default (container: DependencyContainer) => { return { purchase: async (req, res, next) => { @@ -53,7 +56,7 @@ export default (container: DependencyContainer) => { res.redirect(`${container.config.clientUrl}/#/shop/paymentcomplete?credits=${result.galacticTokens}`); return next(); } catch (err) { - console.error(err); + log.error(err); res.redirect(`${container.config.clientUrl}/#/shop/paymentfailed`); return next(); diff --git a/server/api/controllers/star.ts b/server/api/controllers/star.ts index b0f6fad1d..19774f374 100644 --- a/server/api/controllers/star.ts +++ b/server/api/controllers/star.ts @@ -1,5 +1,5 @@ import { DependencyContainer } from '../../services/types/DependencyContainer'; -import { mapToStarAbandonStarRequest, mapToStarBuildCarrierRequest, mapToStarDestroyInfrastructureRequest, mapToStarSetBulkIgnoreAllStatusRequest, mapToStarToggleBulkIgnoreStatusRequest, mapToStarUpgradeInfrastructureBulkRequest, mapToScheduledStarUpgradeInfrastructureBulkRequest, mapToScheduledStarUpgradeToggleRepeat, mapToScheduledStarUpgradeTrash, mapToStarUpgradeInfrastructureRequest, StarUpgradeInfrastructureRequest } from '../requests/star'; +import { mapToStarAbandonStarRequest, mapToStarBuildCarrierRequest, mapToStarDestroyInfrastructureRequest, mapToStarSetBulkIgnoreAllStatusRequest, mapToStarToggleBulkIgnoreStatusRequest, mapToStarUpgradeInfrastructureBulkRequest, mapToScheduledStarUpgradeInfrastructureBulkRequest, mapToStarUpgradeInfrastructureRequest, StarUpgradeInfrastructureRequest, parseScheduledStarUpgradeToggleRepeat, parseScheduledStarUpgradeTrashRepeat } from '../requests/star'; export default (container: DependencyContainer) => { return { @@ -103,12 +103,12 @@ export default (container: DependencyContainer) => { }, toggleBulkRepeat: async (req, res, next) => { try { - const reqObj = mapToScheduledStarUpgradeToggleRepeat(req.body); + const reqObj = parseScheduledStarUpgradeToggleRepeat(req.body); let summary = await container.scheduleBuyService.toggleBulkRepeat( req.game, req.player, - reqObj._id); + reqObj.actionId); res.status(200).json(summary); return next(); @@ -118,12 +118,12 @@ export default (container: DependencyContainer) => { }, trashBulk: async (req, res, next) => { try { - const reqObj = mapToScheduledStarUpgradeTrash(req.body); + const reqObj = parseScheduledStarUpgradeTrashRepeat(req.body); await container.scheduleBuyService.trashAction( req.game, req.player, - reqObj._id + reqObj.actionId ) res.sendStatus(200); return next(); diff --git a/server/api/controllers/user.ts b/server/api/controllers/user.ts index 740588f5b..2cbdd0126 100644 --- a/server/api/controllers/user.ts +++ b/server/api/controllers/user.ts @@ -1,6 +1,9 @@ import ValidationError from '../../errors/validation'; import { DependencyContainer } from '../../services/types/DependencyContainer'; import { mapToUserCreateUserRequest, mapToUserRequestPasswordResetRequest, mapToUserRequestUsernameRequest, mapToUserResetPasswordResetRequest, mapToUserUpdateEmailPreferenceRequest, mapToUserUpdateEmailRequest, mapToUserUpdatePasswordRequest, mapToUserUpdateUsernameRequest } from '../requests/user'; +import {logger} from "../../utils/logging"; + +const log = logger("User Controller"); export default (container: DependencyContainer) => { return { @@ -231,7 +234,7 @@ export default (container: DependencyContainer) => { try { await container.emailService.sendTemplate(reqObj.email, container.emailService.TEMPLATES.RESET_PASSWORD, [token]); } catch (emailError) { - console.error(emailError); + log.error(emailError); res.sendStatus(500); return next(emailError); } @@ -262,7 +265,7 @@ export default (container: DependencyContainer) => { try { await container.emailService.sendTemplate(reqObj.email, container.emailService.TEMPLATES.FORGOT_USERNAME, [username]); } catch (emailError) { - console.error(emailError); + log.error(emailError); res.sendStatus(500); return next(emailError); diff --git a/server/api/express.ts b/server/api/express.ts index f10ef1257..1bcf9caa2 100644 --- a/server/api/express.ts +++ b/server/api/express.ts @@ -10,6 +10,9 @@ import { DependencyContainer } from '../services/types/DependencyContainer'; import registerRoutes from './routes'; import {SingleRouter} from "./singleRoute"; import Middleware from "./middleware"; +import {logger} from "../utils/logging"; + +const log = logger("express"); export default async (config: Config, app, container: DependencyContainer) => { const idempotencyKeyCache: Map = new Map(); @@ -27,7 +30,7 @@ export default async (config: Config, app, container: DependencyContainer) => { // Catch session store errors sessionStorage.on('error', function(err) { - console.error(err); + log.error(err); }); // --------------- @@ -121,7 +124,7 @@ export default async (config: Config, app, container: DependencyContainer) => { app.use(middleware.core.handleError); - console.log('Express intialized.'); + log.info('Express intialized.'); return { app, diff --git a/server/api/index.ts b/server/api/index.ts index c462e9a85..955350584 100644 --- a/server/api/index.ts +++ b/server/api/index.ts @@ -1,3 +1,5 @@ +import {logger, onReady, setupLogging} from "../utils/logging"; + const express = require('express'); const http = require('http'); import config from '../config'; @@ -7,10 +9,13 @@ import socketLoader from './sockets'; import containerLoader from '../services'; let mongo; +Error.stackTraceLimit = 1000; -console.log(`Node ${process.version}`); +setupLogging(); -Error.stackTraceLimit = 1000; +const log = logger(); + +log.info(`Node ${process.version}`); async function startServer() { mongo = await mongooseLoader(config, {}); @@ -28,11 +33,11 @@ async function startServer() { server.listen(config.port, (err) => { if (err) { - console.error(err); + log.error(err); return; } - console.log(`Server is running on port ${config.port}.`); + log.info(`Server is running on port ${config.port}.`); }); await container.discordService.initialize(); @@ -40,15 +45,15 @@ async function startServer() { } process.on('SIGINT', async () => { - console.log('Shutting down...'); + log.info('Shutting down...'); - console.log('Disconnecting from MongoDB...'); + log.info('Disconnecting from MongoDB...'); await mongo.disconnect(); - console.log('MongoDB disconnected.'); + log.info('MongoDB disconnected.'); - console.log('Shutdown complete.'); - - process.exit(); + log.info('Shutdown complete.'); + + onReady(() => process.exit()); }); startServer(); diff --git a/server/api/middleware/core.ts b/server/api/middleware/core.ts index 741f4714e..9c4915fac 100644 --- a/server/api/middleware/core.ts +++ b/server/api/middleware/core.ts @@ -2,6 +2,9 @@ import { NextFunction, Request, Response } from 'express'; import { ExpressJoiError } from 'express-joi-validation'; import ValidationError from '../../errors/validation'; import { DependencyContainer } from '../../services/types/DependencyContainer'; +import {logger} from "../../utils/logging"; + +const log = logger("Core Middleware"); export interface CoreMiddleware { handleError(err: any, req: Request, res: Response, next: NextFunction); @@ -21,7 +24,10 @@ export const middleware = (container: DependencyContainer): CoreMiddleware => { errors = [errors]; } - console.error(errors); + log.error({ + userId: req.session?.userId, + errors + }); res.status(err.statusCode).json({ errors }); @@ -40,7 +46,10 @@ export const middleware = (container: DependencyContainer): CoreMiddleware => { return; } - console.error(err.stack); + log.error({ + userId: req.session?.userId, + error: err.stack + }); res.status(500).json({ errors: ['Something broke. If the problem persists, please contact a developer.'] diff --git a/server/api/middleware/playerMutex.ts b/server/api/middleware/playerMutex.ts index 88dbbe65e..2161d411e 100644 --- a/server/api/middleware/playerMutex.ts +++ b/server/api/middleware/playerMutex.ts @@ -2,6 +2,9 @@ import { NextFunction, Request, Response } from 'express'; import ValidationError from "../../errors/validation"; import { DBObjectId } from "../../services/types/DBObjectId"; import { DependencyContainer } from "../../services/types/DependencyContainer"; +import {logger} from "../../utils/logging"; + +const log = logger("Player Mutex Middleware"); export interface PlayerMutexMiddleware { wait: () => (req: Request<{ gameId?: DBObjectId }>, res: Response, next: NextFunction) => Promise; @@ -119,7 +122,10 @@ export const middleware = (container: DependencyContainer): PlayerMutexMiddlewar return next(); } catch (err) { - console.error("PlayerMutex threw: ", err); + log.error({ + error: err, + gameId: req.params.gameId, + }, "PlayerMutex threw: ", err); return next(err); } } diff --git a/server/api/requests/carrier.ts b/server/api/requests/carrier.ts index 0d3da9238..c0f7a77dc 100644 --- a/server/api/requests/carrier.ts +++ b/server/api/requests/carrier.ts @@ -1,125 +1,52 @@ import ValidationError from "../../errors/validation"; -import { CarrierWaypointActionType } from "../../services/types/CarrierWaypoint"; +import { CarrierWaypointActionType, CarrierWaypointActionTypes } from "../../services/types/CarrierWaypoint"; import { DBObjectId } from "../../services/types/DBObjectId"; +import { array, boolean, map, number, numberAdv, object, objectId, or, positiveInteger, string, stringEnumeration, Validator } from "../validate"; import { keyHasArrayValue, keyHasBooleanValue, keyHasNumberValue, keyHasObjectValue, keyHasStringValue } from "./helpers"; -export interface CarrierSaveWaypointsRequest { - waypoints: [ - { - source: DBObjectId; - destination: DBObjectId; - action: CarrierWaypointActionType; - actionShips: number; - delayTicks: number; - } - ]; - looped: boolean; +type CarrierSaveWaypoint = { + source: DBObjectId; + destination: DBObjectId; + action: CarrierWaypointActionType; + actionShips: number; + delayTicks: number; }; -export const mapToCarrierSaveWaypointsRequest = (body: any): CarrierSaveWaypointsRequest => { - let errors: string[] = []; - - if (!keyHasBooleanValue(body, 'looped')) { - errors.push('Looped is required.'); - } - - if (!keyHasArrayValue(body, 'waypoints')) { - errors.push('Waypoints is required.'); - } - - if (body.waypoints) { - for (let waypoint of body.waypoints) { - if (!keyHasStringValue(waypoint, 'source')) { - errors.push('Source is required.'); - } - - if (!keyHasStringValue(waypoint, 'destination')) { - errors.push('Destination is required.'); - } - - if (!keyHasStringValue(waypoint, 'action')) { - errors.push('Action is required.'); - } - -if (waypoint.actionShips == null) waypoint.actionShips = 0; -if (waypoint.delayTicks == null) waypoint.delayTicks = 0; - - if (!keyHasNumberValue(waypoint, 'actionShips')) { - errors.push('Action Ships is required.'); - } - - if (!keyHasNumberValue(waypoint, 'delayTicks')) { - errors.push('Delay Ticks is required.'); - } - - waypoint.actionShips = +waypoint.actionShips; - waypoint.delayTicks = +waypoint.delayTicks; - } - } - - if (errors.length) { - throw new ValidationError(errors); - } - - return { - waypoints: body.waypoints, - looped: body.looped - } +export type CarrierSaveWaypointsRequest = { + waypoints: CarrierSaveWaypoint[]; + looped: boolean; }; -export interface CarrierLoopWaypointsRequest { +export const parseCarierSaveWaypointsRequest: Validator = object({ + waypoints: array(object({ + source: objectId, + destination: objectId, + action: stringEnumeration(CarrierWaypointActionTypes), + actionShips: map((a) => a || 0, positiveInteger), + delayTicks: map((a) => a || 0, positiveInteger), + })), + looped: boolean, +}); + +export type CarrierLoopWaypointsRequest = { loop: boolean; }; -export const mapToCarrierLoopWaypointsRequest = (body: any): CarrierLoopWaypointsRequest => { - let errors: string[] = []; +export const parseCarrierLoopWaypointsRequest: Validator = object({ + loop: boolean, +}); - if (!keyHasBooleanValue(body, 'loop')) { - errors.push('Loop is required.'); - } - - if (errors.length) { - throw new ValidationError(errors); - } - - return { - loop: body.loop - } -}; - -export interface CarrierTransferShipsRequest { +export type CarrierTransferShipsRequest = { carrierShips: number; starShips: number; starId: DBObjectId; }; -export const mapToCarrierTransferShipsRequest = (body: any): CarrierTransferShipsRequest => { - let errors: string[] = []; - - if (!keyHasNumberValue(body, 'carrierShips')) { - errors.push('Carrier Ships is required.'); - } - - if (!keyHasNumberValue(body, 'starShips')) { - errors.push('Star Ships is required.'); - } - - if (!keyHasStringValue(body, 'starId')) { - errors.push('Star ID is required.'); - } - - if (errors.length) { - throw new ValidationError(errors); - } - - body.starShips = +body.starShips; - - return { - carrierShips: body.carrierShips, - starShips: body.starShips, - starId: body.starId - } -}; +export const parseCarrierTransferShipsRequest = object({ + carrierShips: positiveInteger, + starShips: positiveInteger, + starId: objectId, +}); export interface CarrierRenameCarrierRequest { name: string; diff --git a/server/api/requests/game.ts b/server/api/requests/game.ts index 7e7cbfdc2..6bf32dfdd 100644 --- a/server/api/requests/game.ts +++ b/server/api/requests/game.ts @@ -1,5 +1,6 @@ import ValidationError from "../../errors/validation"; import { DBObjectId } from "../../services/types/DBObjectId"; +import { object, Validator, objectId } from "../validate"; import { keyHasBooleanValue, keyHasNumberValue, keyHasStringValue } from "./helpers"; export interface GameCreateGameRequest { @@ -82,4 +83,12 @@ export const mapToGameConcedeDefeatRequest = (body: any): GameConcedeDefeatReque return { openSlot: body.openSlot } -} \ No newline at end of file +} + +export type KickPlayerRequest = { + playerId: DBObjectId, +} + +export const parseKickPlayerRequest: Validator = object({ + playerId: objectId +}); diff --git a/server/api/requests/star.ts b/server/api/requests/star.ts index 4b0e4be90..09fbed769 100644 --- a/server/api/requests/star.ts +++ b/server/api/requests/star.ts @@ -3,6 +3,7 @@ import ValidationError from "../../errors/validation"; import { DBObjectId } from "../../services/types/DBObjectId"; import { InfrastructureType } from "../../services/types/Star"; import { keyHasBooleanValue, keyHasNumberValue, keyHasStringValue } from "./helpers"; +import { object, objectId, Validator } from "../validate"; export interface StarUpgradeInfrastructureRequest { starId: DBObjectId; @@ -38,12 +39,12 @@ export interface ScheduledStarUpgradeInfrastructureBulkRequest { tick: number; }; -export interface ScheduledStarUpgradeToggleRepeat { - _id: DBObjectId; +export type ScheduledStarUpgradeToggleRepeat = { + actionId: DBObjectId; }; export interface ScheduledStarUpgradeTrash { - _id: DBObjectId; + actionId: DBObjectId; } export const mapToStarUpgradeInfrastructureBulkRequest = (body: any): StarUpgradeInfrastructureBulkRequest => { @@ -121,37 +122,13 @@ export const mapToScheduledStarUpgradeInfrastructureBulkRequest = (body: any): S } }; -export const mapToScheduledStarUpgradeToggleRepeat = (body: any): ScheduledStarUpgradeToggleRepeat => { - let errors: string[] = []; - - if (!keyHasStringValue(body, 'actionId')) { - errors.push('ObjectId is required.'); - } - - if (errors.length) { - throw new ValidationError(errors); - } - - return { - _id: body.actionId - } -}; +export const parseScheduledStarUpgradeToggleRepeat: Validator = object({ + actionId: objectId +}); -export const mapToScheduledStarUpgradeTrash = (body: any): ScheduledStarUpgradeTrash => { - let errors: string[] = []; - - if (!keyHasStringValue(body, 'actionId')) { - errors.push('ObjectId is required.'); - } - - if (errors.length) { - throw new ValidationError(errors); - } - - return { - _id: body.actionId - } -} +export const parseScheduledStarUpgradeTrashRepeat: Validator = object({ + actionId: objectId +}); export interface StarDestroyInfrastructureRequest { starId: DBObjectId; diff --git a/server/api/routes/games.ts b/server/api/routes/games.ts index 5347111ad..6bef3ee19 100644 --- a/server/api/routes/games.ts +++ b/server/api/routes/games.ts @@ -348,6 +348,17 @@ export default (router: SingleRouter, mw: MiddlewareContainer, validator: Expres controller.fastForward ); + router.post('/api/game/:gameId/kick', + mw.auth.authenticate(), + mw.game.loadGame({ + lean: false, + }), + mw.game.validateGameState({ + isUnlocked: true, + }), + controller.kickPlayer + ); + router.get('/api/game/:gameId/player/:playerId', mw.game.loadGame({ lean: true, diff --git a/server/api/sockets.ts b/server/api/sockets.ts index 7ec561b9d..1e22c17f1 100644 --- a/server/api/sockets.ts +++ b/server/api/sockets.ts @@ -1,9 +1,12 @@ import { Config } from "../config/types/Config"; +import {logger} from "../utils/logging"; const socketio = require('socket.io'); const cookieParser = require('cookie-parser'); const cookie = require('cookie'); +const log = logger('sockets'); + export default (config: Config, server, sessionStore) => { const io = socketio(server); @@ -79,7 +82,7 @@ export default (config: Config, server, sessionStore) => { }); }); - console.log('Sockets initialized.'); + log.info('Sockets initialized.'); return io; }; diff --git a/server/api/validate.ts b/server/api/validate.ts index 86b819f4d..ca15f585f 100644 --- a/server/api/validate.ts +++ b/server/api/validate.ts @@ -1,4 +1,5 @@ import ValidationError from "../errors/validation"; +import { DBObjectId, objectIdFromString } from "../services/types/DBObjectId"; export type Validator = (value: any) => T; @@ -134,4 +135,62 @@ export const object = (objValidator: ObjectValidator): Validator => { return n; } -} \ No newline at end of file +} + +export const stringEnumeration = (members: readonly [any, ...M]): Validator => { + return v => { + const s = string(v); + if (members.includes(s as A)) { + return s as A; + } else { + throw failed(members.join(", "), v) + } + } +} + +export const objectId: Validator = map(objectIdFromString, string); + +type NumberValidationProps = { + sign?: 'positive' | 'negative', + integer?: boolean, + range?: { + from: number, + to: number + }, +} + +export const numberAdv = (props: NumberValidationProps) => v => { + const n = number(v); + + if (props.sign) { + const sign = Math.sign(n); + if (props.sign === 'positive') { + if (sign === -1) { + throw failed('positive number', v); + } + } else if (props.sign === 'negative') { + if (sign !== -1) { + throw failed('negative number', v); + } + } + } + + if (props.integer) { + if (!Number.isInteger(n)) { + throw failed('integer', v); + } + } + + if (props.range) { + if (n < props.range.from || n > props.range.to) { + throw failed(`number between ${props.range.from} and ${props.range.to}`, v); + } + } + + return n; +} + +export const positiveInteger = numberAdv({ + integer: true, + sign: 'positive' +}); \ No newline at end of file diff --git a/server/config/index.ts b/server/config/index.ts index 7cd0a7419..008cc96ed 100644 --- a/server/config/index.ts +++ b/server/config/index.ts @@ -1,4 +1,4 @@ -import { Config } from "./types/Config"; +import {Config, LoggingType} from "./types/Config"; require('dotenv').config({path:__dirname + '/../.env'}); @@ -11,6 +11,7 @@ const config: Config = { clientUrl: process.env.CLIENT_URL, corsUrls: process.env.CORS_URLS?.split(",") || [ process.env.CLIENT_URL || "https://solaris.games" ], cacheEnabled: process.env.CACHE_ENABLED == "true", + logging: process.env.LOGGING_TYPE as LoggingType, smtp: { enabled: process.env.SMTP_ENABLED == "true", host: process.env.SMTP_HOST, diff --git a/server/config/types/Config.ts b/server/config/types/Config.ts index 787cb3f36..2585c1dfa 100644 --- a/server/config/types/Config.ts +++ b/server/config/types/Config.ts @@ -1,3 +1,5 @@ +export type LoggingType = 'pretty' | 'stdout'; + export interface Config { port?: string; sessionSecret?: string; @@ -7,6 +9,7 @@ export interface Config { clientUrl?: string; corsUrls: string[]; cacheEnabled: boolean; + logging?: LoggingType; smtp: { enabled: boolean; host?: string; diff --git a/server/db/index.ts b/server/db/index.ts index 1d078fdc2..0a8edeed1 100644 --- a/server/db/index.ts +++ b/server/db/index.ts @@ -1,3 +1,5 @@ +import {logger} from "../utils/logging"; + const mongoose = require('mongoose'); import EventModel from './models/Event'; @@ -7,6 +9,8 @@ import HistoryModel from './models/History'; import UserModel from './models/User'; import PaymentModel from './models/Payment'; +const log = logger("Database"); + export default async (config, options) => { async function unlockAgendaJobs(db) { @@ -25,14 +29,14 @@ export default async (config, options) => { $set: { nextRunAt:new Date() } }); - console.log(`Unlocked #${numUnlocked.modifiedCount} jobs.`); + log.info(`Unlocked #${numUnlocked.modifiedCount} jobs.`); } catch (e) { - console.error(e); + log.error(e); } } async function syncIndexes() { - console.log('Syncing indexes...'); + log.info('Syncing indexes...'); await EventModel.syncIndexes(); await GameModel.syncIndexes(); await GuildModel.syncIndexes(); @@ -40,12 +44,12 @@ export default async (config, options) => { await UserModel.syncIndexes(); await PaymentModel.syncIndexes(); // TODO ReportModel? - console.log('Indexes synced.'); + log.info('Indexes synced.'); } const dbConnection = mongoose.connection; - dbConnection.on('error', console.error.bind(console, 'connection error:')); + dbConnection.on('error', log.error.bind(log.error, 'connection error:')); options = options || {}; options.connectionString = options.connectionString || config.connectionString; @@ -53,7 +57,7 @@ export default async (config, options) => { options.unlockJobs = options.unlockJobs == null ? false : options.unlockJobs; options.poolSize = options.poolSize || 5; - console.log(`Connecting to database: ${options.connectionString}`); + log.info(`Connecting to database: ${options.connectionString}`); const db = await mongoose.connect(options.connectionString, { useUnifiedTopology: true, @@ -71,7 +75,7 @@ export default async (config, options) => { await unlockAgendaJobs(db); } - console.log('MongoDB intialized.'); + log.info('MongoDB intialized.'); return db; }; diff --git a/server/db/migrate.ts b/server/db/migrate.ts index dcbe89297..e49237a0e 100644 --- a/server/db/migrate.ts +++ b/server/db/migrate.ts @@ -2,16 +2,19 @@ import config from '../config'; import mongooseLoader from '.'; import fs from 'fs'; import path from 'path'; +import {logger} from "../utils/logging"; let mongo; +const log = logger("Migrations") + async function startup() { mongo = await mongooseLoader(config, { syncIndexes: true, poolSize: 1 }); - console.log('Running migrations...'); + log.info('Running migrations...'); const dirPath: string = path.join(__dirname, 'migrations'); @@ -20,7 +23,7 @@ async function startup() { .sort((a, b) => a.localeCompare(b)); for (let file of files) { - console.log(file); + log.info(file); const filePath = path.join(dirPath, file); const script = require(filePath); @@ -28,7 +31,7 @@ async function startup() { try { await script.migrate(mongo.connection.db); } catch (e) { - console.error(e); + log.error(e); return Promise.reject(e); } @@ -42,17 +45,17 @@ process.on('SIGINT', async () => { }); async function shutdown() { - console.log('Shutting down...'); + log.info('Shutting down...'); await mongo.disconnect(); - console.log('Shutdown complete.'); + log.info('Shutdown complete.'); process.exit(); } startup().then(async () => { - console.log('Database migrated.'); + log.info('Database migrated.'); await shutdown(); }).catch(async err => { diff --git a/server/db/migrations/020-add-scheduled-actions.js b/server/db/migrations/020-add-scheduled-actions.js index 9147ec595..d22cca1b4 100644 --- a/server/db/migrations/020-add-scheduled-actions.js +++ b/server/db/migrations/020-add-scheduled-actions.js @@ -1,5 +1,3 @@ -const { Console } = require("console"); - module.exports = { async migrate(db) { const games = db.collection('games'); diff --git a/server/db/recalculateRankings.ts b/server/db/recalculateRankings.ts index 5556c2f74..53cf688c5 100644 --- a/server/db/recalculateRankings.ts +++ b/server/db/recalculateRankings.ts @@ -4,10 +4,13 @@ import containerLoader from '../services'; import {DependencyContainer} from '../services/types/DependencyContainer'; import {User} from '../services/types/User'; import {GameWinnerKind} from "../services/leaderboard"; +import {logger} from "../utils/logging"; let mongo, container: DependencyContainer; +const log = logger("Recalculate Rankings"); + function binarySearchUsers(users: User[], id: string) { let start = 0; let end = users.length - 1; @@ -41,9 +44,9 @@ async function startup() { container = containerLoader(config); - console.log('Recalculating all player ranks...'); + log.info('Recalculating all player ranks...'); - console.log(`Resetting users...`); + log.info(`Resetting users...`); await container.userService.userRepo.updateMany({}, { $set: { 'achievements.level': 1, @@ -72,7 +75,7 @@ async function startup() { 'achievements.badges.special_arcade': 0, } }); - console.log(`Done.`); + log.info(`Done.`); let users = await container.userService.userRepo.find({}, { _id: 1, @@ -80,7 +83,7 @@ async function startup() { }, { _id: 1 }); - console.log(`Total users: ${users.length}`); + log.info(`Total users: ${users.length}`); let dbQuery = { 'state.endDate': { $ne: null }, @@ -89,7 +92,7 @@ async function startup() { let total = (await container.gameService.gameRepo.count(dbQuery)); - console.log(`Recalculating rank for ${total} games...`); + log.info(`Recalculating rank for ${total} games...`); let page = 0; let pageSize = 10; @@ -186,12 +189,12 @@ async function startup() { await container.gameService.gameRepo.bulkWrite(leaderboardWrites); - console.log(`Page ${page}/${totalPages}`); + log.info(`Page ${page}/${totalPages}`); page++; } while (page <= totalPages); - console.log(`Done.`); + log.info(`Done.`); let dbWrites = users.map(user => { return { @@ -229,9 +232,9 @@ async function startup() { } }); - console.log(`Updating users...`); + log.info(`Updating users...`); await container.userService.userRepo.bulkWrite(dbWrites); - console.log(`Users updated.`); + log.info(`Users updated.`); } process.on('SIGINT', async () => { @@ -239,21 +242,21 @@ process.on('SIGINT', async () => { }); async function shutdown() { - console.log('Shutting down...'); + log.info('Shutting down...'); await mongo.disconnect(); - console.log('Shutdown complete.'); + log.info('Shutdown complete.'); process.exit(); } startup().then(async () => { - console.log('Done.'); + log.info('Done.'); await shutdown(); }).catch(async err => { - console.error(err); + log.error(err); await shutdown(); }); diff --git a/server/db/restoreGame.ts b/server/db/restoreGame.ts new file mode 100644 index 000000000..39b192637 --- /dev/null +++ b/server/db/restoreGame.ts @@ -0,0 +1,192 @@ +import {DependencyContainer} from "../services/types/DependencyContainer"; +import mongooseLoader from "./index"; +import config from "../config"; +import containerLoader from "../services"; +import mongoose, { ObjectId } from "mongoose"; +import {DBObjectId, objectId} from "../services/types/DBObjectId"; +import {GameHistory, GameHistoryCarrier} from "../services/types/GameHistory"; +import {Game} from "../services/types/Game"; +import {Carrier} from "../services/types/Carrier"; + +let mongo, + container: DependencyContainer; + +const loadHistory = async (gameId: DBObjectId, tick: number) => { + const history = await container.historyService.getHistoryByTick(gameId, tick); + + return history; +} + +const applyGameState = (game: Game, history: GameHistory) => { + game.state.tick = history.tick; + game.state.productionTick = history.productionTick; + game.state.lastTickDate = new Date(); +} + +const applyPlayers = (game: Game, history: GameHistory) => { + game.galaxy.players.forEach(player => { + const histPlayer = history.players.find(p => p.playerId.toString() === player._id.toString()); + + if (!histPlayer) { + console.log(`Player ${player._id}/${player.alias} not found in history`); + + return; + } + + player.userId = histPlayer.userId; + player.alias = histPlayer.alias; + player.avatar = histPlayer.avatar; + player.researchingNow = histPlayer.researchingNow; + player.researchingNext = histPlayer.researchingNext; + player.credits = histPlayer.credits; + player.creditsSpecialists = histPlayer.creditsSpecialists; + player.isOpenSlot = histPlayer.isOpenSlot; + player.defeated = histPlayer.defeated; + player.defeatedDate = histPlayer.defeatedDate; + player.afk = histPlayer.afk; + player.ready = histPlayer.ready; + player.readyToQuit = histPlayer.readyToQuit; + player.research = histPlayer.research; + }); +} + +const applyStars = (game: Game, history: GameHistory) => { + game.galaxy.stars.forEach(star => { + const histStar = history.stars.find(s => s.starId.toString() === star._id.toString()); + + if (!histStar) { + console.log(`Star ${star._id}/${star.name} not found in history`); + + return; + } + + star.homeStar = histStar.homeStar + star.ships = histStar.ships; + star.shipsActual = histStar.shipsActual; + star.specialistId = histStar.specialistId; + star.naturalResources = histStar.naturalResources; + star.infrastructure = histStar.infrastructure; + star.ignoreBulkUpgrade = histStar.ignoreBulkUpgrade; + star.warpGate = histStar.warpGate; + star.ownedByPlayerId = histStar.ownedByPlayerId; + star.location = histStar.location; + }); +} + +const applyWaypoints = (carrier: Carrier, histCarrier: GameHistoryCarrier) => { + const historyWaypoint = histCarrier.waypoints[0]; + + if (!historyWaypoint) { + return; + } + + carrier.waypoints = [{ + _id: objectId(), + source: historyWaypoint.source, + destination: historyWaypoint.destination, + action: "nothing", + actionShips: 0, + delayTicks: 0 + }]; +} + +const applyCarriers = (game: Game, history: GameHistory) => { + const removeCarriers = new Array(); + + game.galaxy.carriers.forEach(carrier => { + const histCarrier = history.carriers.find(c => c.carrierId.toString() === carrier._id.toString()); + + if (!histCarrier) { + console.log(`Carrier ${carrier._id}/${carrier.name} not found in history`); + + removeCarriers.push(carrier._id); + return; + } + + carrier.ships = histCarrier.ships; + carrier.specialistId = histCarrier.specialistId + carrier.ownedByPlayerId = histCarrier.ownedByPlayerId; + carrier.name = histCarrier.name; + carrier.location = histCarrier.location + carrier.isGift = histCarrier.isGift; + carrier.orbiting = histCarrier.orbiting; + applyWaypoints(carrier, histCarrier); + }); + + game.galaxy.carriers = game.galaxy.carriers.filter(c => !removeCarriers.includes(c._id)); +} + +const applyHistory = (game: Game, history: GameHistory) => { + applyGameState(game, history); + applyPlayers(game, history); + applyStars(game, history); + applyCarriers(game, history); +} + +const startup = async () => { + mongo = await mongooseLoader(config, { + syncIndexes: true, + poolSize: 1 + }); + + container = containerLoader(config); + + console.log("Initialised"); + + const gameIdS = process.argv[2]; + const tick = Number.parseInt(process.argv[3]); + + if (!gameIdS || !tick) { + throw new Error("Invalid arguments. Usage: npm run restore-game "); + } + + const gameId = new mongoose.Types.ObjectId(gameIdS) as DBObjectId; + + const hist = await loadHistory(gameId, tick); + if (!hist) { + throw new Error("History not found"); + } + + console.log("Loaded history entry"); + + const currentState = await container.gameService.getByIdAll(gameId); + if (!currentState) { + throw new Error("Game not found"); + } + + console.log("Loaded current game state"); + + applyHistory(currentState, hist); + + console.log("History applied"); + + await currentState.save(); + + console.log("Game state saved"); +} + +process.on('SIGINT', async () => { + await shutdown(); +}); + +const shutdown = async () =>{ + console.log('Shutting down...'); + + await mongo.disconnect(); + + console.log('Shutdown complete.'); + + process.exit(); +} + +startup().then(async () => { + console.log('Done.'); + + await shutdown(); +}).catch(async err => { + console.error(err); + + await shutdown(); +}); + +export {}; diff --git a/server/jobs/cleanupGamesTimedOut.ts b/server/jobs/cleanupGamesTimedOut.ts index 72fdb7fec..aaaa5bef8 100644 --- a/server/jobs/cleanupGamesTimedOut.ts +++ b/server/jobs/cleanupGamesTimedOut.ts @@ -1,4 +1,7 @@ import { DependencyContainer } from "../services/types/DependencyContainer"; +import {logger} from "../utils/logging"; + +const log = logger("Cleanup Games Timed Out Job"); export default (container: DependencyContainer) => { @@ -19,13 +22,13 @@ export default (container: DependencyContainer) => { await container.emailService.sendGameTimedOutEmail(game._id); await container.gameService.delete(game); } catch (e) { - console.error(e); + log.error(e); } } done(); } catch (e) { - console.error("CleanupGamesTimedOut job threw unhandled: " + e, e); + log.error("CleanupGamesTimedOut job threw unhandled: " + e, e); } } }; diff --git a/server/jobs/cleanupOldGameHistory.ts b/server/jobs/cleanupOldGameHistory.ts index 51fc64982..cbb7ceac4 100644 --- a/server/jobs/cleanupOldGameHistory.ts +++ b/server/jobs/cleanupOldGameHistory.ts @@ -1,4 +1,7 @@ import { DependencyContainer } from "../services/types/DependencyContainer"; +import {logger} from "../utils/logging"; + +const log = logger("Cleanup Old Game History Job"); export default (container: DependencyContainer) => { @@ -22,22 +25,22 @@ export default (container: DependencyContainer) => { continue } - console.log(`Deleting history for old game: ${game._id}`); + log.info(`Deleting history for old game: ${game._id}`); try { await container.historyService.deleteByGameId(game._id); await container.eventService.deleteByGameId(game._id); await container.gameService.markAsCleaned(game._id); } catch (e) { - console.error(e); + log.error(e); } } - console.log('Cleanup completed.'); + log.info('Cleanup completed.'); done(); } catch (e) { - console.error("CleanupOldGameHistory job threw unhandled: " + e, e); + log.error("CleanupOldGameHistory job threw unhandled: " + e, e); } } diff --git a/server/jobs/cleanupOldTutorials.ts b/server/jobs/cleanupOldTutorials.ts index a113e44de..264c444f0 100644 --- a/server/jobs/cleanupOldTutorials.ts +++ b/server/jobs/cleanupOldTutorials.ts @@ -1,4 +1,7 @@ import { DependencyContainer } from "../services/types/DependencyContainer"; +import {logger} from "../utils/logging"; + +const log = logger("Cleanup Old Tutorials Job"); export default (container: DependencyContainer) => { @@ -14,13 +17,13 @@ export default (container: DependencyContainer) => { try { await container.gameService.delete(game); } catch (e) { - console.error(e); + log.error(e); } } done(); } catch (e) { - console.error("CleanupOldTutorials job threw unhandled: " + e, e); + log.error("CleanupOldTutorials job threw unhandled: " + e, e); } } }; diff --git a/server/jobs/gameTick.ts b/server/jobs/gameTick.ts index 8552a97fd..dce3964c6 100644 --- a/server/jobs/gameTick.ts +++ b/server/jobs/gameTick.ts @@ -3,6 +3,9 @@ import { DBObjectId } from "../services/types/DBObjectId"; import { DependencyContainer } from "../services/types/DependencyContainer"; import { Game } from "../services/types/Game"; import { GameMutexLock } from "../services/types/GameMutexLock"; +import {logger} from "../utils/logging"; + +const log = logger("Game Tick Job"); export default (container: DependencyContainer) => { @@ -31,7 +34,7 @@ export default (container: DependencyContainer) => { } } catch (e) { - console.error(`Error in game ${game.settings.general.name} (${game._id})`, e); + log.error(`Error in game ${game.settings.general.name} (${game._id})`, e); } finally { await container.gameLockService.lock(gameId, false); @@ -39,7 +42,7 @@ export default (container: DependencyContainer) => { } } catch (e) { - console.error(e); + log.error(e); } finally { //console.log(`tryTickGame() finished!`); @@ -59,7 +62,7 @@ export default (container: DependencyContainer) => { done(); } catch (e) { - console.error("GameTick job threw unhandled: " + e, e); + log.error("GameTick job threw unhandled: " + e, e); } } diff --git a/server/jobs/index.ts b/server/jobs/index.ts index 8897912cc..9c479fc89 100644 --- a/server/jobs/index.ts +++ b/server/jobs/index.ts @@ -1,3 +1,5 @@ +import {logger, onReady, setupLogging} from "../utils/logging"; + const Agenda = require('agenda'); import config from '../config'; import mongooseLoader from '../db'; @@ -11,6 +13,11 @@ import CleanupOldTutorialsJob from './cleanupOldTutorials'; import SendReviewRemindersJob from './sendReviewReminders'; let mongo; +Error.stackTraceLimit = 1000; + +setupLogging(); + +const log = logger(); async function startup() { const container = containerLoader(config); @@ -26,9 +33,9 @@ async function startup() { // ------------------------------ // Jobs that run every time the server restarts. - console.log('Unlock all games...'); + log.info('Unlock all games...'); await container.gameService.lockAll(false); - console.log('All games unlocked'); + log.info('All games unlocked'); // ------------------------------ @@ -103,20 +110,20 @@ async function startup() { agendajs.every('10 seconds', 'send-review-reminders'); // TODO: Every 10 seconds until we've gone through all backlogged users. process.on('SIGINT', async () => { - console.log('Shutting down...'); + log.info('Shutting down...'); await agendajs.stop(); await mongo.disconnect(); - console.log('Shutdown complete.'); + log.info('Shutdown complete.'); - process.exit(0); + onReady(() => process.exit()); }); } startup().then(() => { - console.log('Jobs started.'); + log.info('Jobs started.'); }); export {}; diff --git a/server/jobs/officialGamesCheck.ts b/server/jobs/officialGamesCheck.ts index 404c2b00b..bc5f4fe4b 100644 --- a/server/jobs/officialGamesCheck.ts +++ b/server/jobs/officialGamesCheck.ts @@ -1,6 +1,9 @@ import {DependencyContainer} from "../services/types/DependencyContainer"; import {Game, GameSettings} from "../services/types/Game"; import {OfficialGameCategory, OfficialGameKind} from "../config/officialGames"; +import {logger} from "../utils/logging"; + +const log = logger("Official Games Check Job"); const chooseSetting = (container: DependencyContainer, category: OfficialGameCategory): GameSettings => { if (category.kind === OfficialGameKind.Standard) { @@ -36,7 +39,7 @@ export default (container: DependencyContainer) => { const existingOpen = findExistingGame(category, openGames); if (!existingOpen) { - console.log(`Could not find game [${container.gameTypeService.getOfficialGameCategoryName(category)}], creating it now...`); + log.info(`Could not find game [${container.gameTypeService.getOfficialGameCategoryName(category)}], creating it now...`); const existingRunning = findExistingGame(category, runningGames); const existingTemplate = existingRunning?.settings.general.createdFromTemplate; @@ -53,16 +56,16 @@ export default (container: DependencyContainer) => { try { const newGame = await container.gameCreateService.create(newSetting); - console.log(`${newGame.settings.general.type} game created: ${newGame.settings.general.name}`); + log.info(`${newGame.settings.general.type} game created: ${newGame.settings.general.name}`); } catch (e) { - console.error(e); + log.error(e); } } } done(); } catch (e) { - console.error("OfficialGamesCheck job threw unhandled: " + e, e); + log.error("OfficialGamesCheck job threw unhandled: " + e, e); } } diff --git a/server/jobs/sendReviewReminders.ts b/server/jobs/sendReviewReminders.ts index 0548edced..fad5cc1e0 100644 --- a/server/jobs/sendReviewReminders.ts +++ b/server/jobs/sendReviewReminders.ts @@ -1,4 +1,5 @@ import { DependencyContainer } from "../services/types/DependencyContainer"; +import {logger} from "../utils/logging"; function sleep(ms: number) { return new Promise((resolve) => { @@ -6,6 +7,8 @@ function sleep(ms: number) { }); } +const log = logger("Send Review Reminders Job"); + export default (container: DependencyContainer) => { return { @@ -23,7 +26,7 @@ export default (container: DependencyContainer) => { try { await container.emailService.sendReviewReminderEmail(user); } catch (e) { - console.error(e); + log.error(e); } finally { await container.userService.setReviewReminderEmailSent(user._id, true); } @@ -33,7 +36,7 @@ export default (container: DependencyContainer) => { done(); } catch (e) { - console.error("SendReviewReminders job threw unhandled: " + e, e); + log.error("SendReviewReminders job threw unhandled: " + e, e); } } diff --git a/server/package-lock.json b/server/package-lock.json index f216f415e..0e53956e2 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -30,6 +30,8 @@ "mongoose": "^5.12.8", "mongoose-lean-defaults": "^0.4.1", "nodemailer": "^6.5.0", + "pino": "^9.5.0", + "pino-pretty": "^11.3.0", "qheap": "^1.4.0", "random-seed": "^0.3.0", "recaptcha-v2": "^0.1.3", @@ -519,6 +521,14 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/axios": { "version": "0.18.1", "resolved": "https://registry.npmjs.org/axios/-/axios-0.18.1.tgz", @@ -547,6 +557,25 @@ "node": ">= 0.6.0" } }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/base64id": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", @@ -661,6 +690,29 @@ "node": ">=0.6.19" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -730,6 +782,11 @@ "color-support": "bin.js" } }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==" + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -973,6 +1030,14 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "engines": { + "node": "*" + } + }, "node_modules/debug": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", @@ -1096,6 +1161,14 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/engine.io": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.6.0.tgz", @@ -1224,6 +1297,14 @@ "node": ">=6" } }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/express": { "version": "4.18.2", "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", @@ -1379,6 +1460,24 @@ } ] }, + "node_modules/fast-copy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz", + "integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==" + }, + "node_modules/fast-redact": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", + "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -1615,6 +1714,11 @@ "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" }, + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==" + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -1658,6 +1762,25 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/indexof": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", @@ -1805,6 +1928,14 @@ "@sideway/pinpoint": "^2.0.0" } }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "engines": { + "node": ">=10" + } + }, "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -1941,7 +2072,6 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -2289,6 +2419,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -2373,6 +2511,117 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pino": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.5.0.tgz", + "integrity": "sha512-xSEmD4pLnV54t0NOUN16yCl7RIB1c5UUOse5HSyEXtBp+FgFQyPeDutc+Q2ZO7/22vImV7VfEjH/1zV2QuqvYw==", + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^4.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-pretty": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-11.3.0.tgz", + "integrity": "sha512-oXwn7ICywaZPHmu3epHGU2oJX4nPmKvHvB/bwrJHlGcbEWaVcotkpyVHMKLKmiVryWYByNp0jpgAcXpFJDXJzA==", + "dependencies": { + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^3.0.2", + "fast-safe-stringify": "^2.1.1", + "help-me": "^5.0.0", + "joycon": "^3.1.1", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pump": "^3.0.0", + "readable-stream": "^4.0.0", + "secure-json-parse": "^2.4.0", + "sonic-boom": "^4.0.1", + "strip-json-comments": "^3.1.1" + }, + "bin": { + "pino-pretty": "bin.js" + } + }, + "node_modules/pino-pretty/node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/pino-pretty/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/pino-pretty/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/pino-pretty/node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", + "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==" + }, "node_modules/prism-media": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/prism-media/-/prism-media-1.3.4.tgz", @@ -2398,11 +2647,24 @@ } } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, + "node_modules/process-warning": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.0.tgz", + "integrity": "sha512-/MyYDxttz7DfGMMHiysAsFE4qF+pQYAA8ziO/3NcRVrQ5fSk+Mns4QZA/oRPFzvcqNoVJXQNWNAsdwBXLUkQKw==" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -2415,6 +2677,15 @@ "node": ">= 0.10" } }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/qheap": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/qheap/-/qheap-1.4.0.tgz", @@ -2437,6 +2708,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" + }, "node_modules/random-bytes": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", @@ -2504,6 +2780,14 @@ "node": ">=8.10.0" } }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "engines": { + "node": ">= 12.13.0" + } + }, "node_modules/recaptcha-v2": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/recaptcha-v2/-/recaptcha-v2-0.1.3.tgz", @@ -2573,6 +2857,14 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -2590,6 +2882,11 @@ "node": ">=6" } }, + "node_modules/secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==" + }, "node_modules/semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -2780,6 +3077,14 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", "integrity": "sha512-c2cu3UxbI+b6kR3fy0nRnAhodsvR9dx7U5+znCOzdj6IfP3upFURTr0Xl5BlQZNKZjEtxrmVyfSdeE3O57smoQ==" }, + "node_modules/sonic-boom": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", + "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -2808,6 +3113,14 @@ "memory-pager": "^1.0.2" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -2894,6 +3207,14 @@ "node": ">=10" } }, + "node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "dependencies": { + "real-require": "^0.2.0" + } + }, "node_modules/to-array": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", @@ -3624,6 +3945,11 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, + "atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==" + }, "axios": { "version": "0.18.1", "resolved": "https://registry.npmjs.org/axios/-/axios-0.18.1.tgz", @@ -3648,6 +3974,11 @@ "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz", "integrity": "sha512-a1eIFi4R9ySrbiMuyTGx5e92uRH5tQY6kArNcFaKBUleIoLjdjBg7Zxm3Mqm3Kmkf27HLR/1fnxX9q8GQ7Iavg==" }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, "base64id": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", @@ -3744,6 +4075,15 @@ "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.6.tgz", "integrity": "sha512-EvVNVeGo4tHxwi8L6bPj3y3itEvStdwvvlojVxxbyYfoaxJ6keLgrTuKdyfEAszFK+H3olzBuafE0yoh0D1gdg==" }, + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -3790,6 +4130,11 @@ "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==" }, + "colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==" + }, "combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -3977,6 +4322,11 @@ } } }, + "dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==" + }, "debug": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", @@ -4070,6 +4420,14 @@ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "requires": { + "once": "^1.4.0" + } + }, "engine.io": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.6.0.tgz", @@ -4162,6 +4520,11 @@ "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" }, + "events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" + }, "express": { "version": "4.18.2", "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", @@ -4276,6 +4639,21 @@ } } }, + "fast-copy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz", + "integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==" + }, + "fast-redact": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", + "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==" + }, + "fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -4468,6 +4846,11 @@ "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" }, + "help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==" + }, "http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -4502,6 +4885,11 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, "indexof": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", @@ -4608,6 +4996,11 @@ "@sideway/pinpoint": "^2.0.0" } }, + "joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==" + }, "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -4712,8 +5105,7 @@ "minimist": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", - "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", - "dev": true + "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==" }, "minipass": { "version": "5.0.0", @@ -4947,6 +5339,11 @@ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==" }, + "on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==" + }, "on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -5010,17 +5407,111 @@ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true }, + "pino": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.5.0.tgz", + "integrity": "sha512-xSEmD4pLnV54t0NOUN16yCl7RIB1c5UUOse5HSyEXtBp+FgFQyPeDutc+Q2ZO7/22vImV7VfEjH/1zV2QuqvYw==", + "requires": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^4.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + } + }, + "pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "requires": { + "split2": "^4.0.0" + } + }, + "pino-pretty": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-11.3.0.tgz", + "integrity": "sha512-oXwn7ICywaZPHmu3epHGU2oJX4nPmKvHvB/bwrJHlGcbEWaVcotkpyVHMKLKmiVryWYByNp0jpgAcXpFJDXJzA==", + "requires": { + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^3.0.2", + "fast-safe-stringify": "^2.1.1", + "help-me": "^5.0.0", + "joycon": "^3.1.1", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pump": "^3.0.0", + "readable-stream": "^4.0.0", + "secure-json-parse": "^2.4.0", + "sonic-boom": "^4.0.1", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "requires": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" + } + } + }, + "pino-std-serializers": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", + "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==" + }, "prism-media": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/prism-media/-/prism-media-1.3.4.tgz", "integrity": "sha512-eW7LXORkTCQznZs+eqe9VjGOrLBxcBPXgNyHXMTSRVhphvd/RrxgIR7WaWt4fkLuhshcdT5KHL88LAfcvS3f5g==", "requires": {} }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==" + }, "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, + "process-warning": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.0.tgz", + "integrity": "sha512-/MyYDxttz7DfGMMHiysAsFE4qF+pQYAA8ziO/3NcRVrQ5fSk+Mns4QZA/oRPFzvcqNoVJXQNWNAsdwBXLUkQKw==" + }, "proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -5030,6 +5521,15 @@ "ipaddr.js": "1.9.1" } }, + "pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "qheap": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/qheap/-/qheap-1.4.0.tgz", @@ -5043,6 +5543,11 @@ "side-channel": "^1.0.4" } }, + "quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" + }, "random-bytes": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", @@ -5095,6 +5600,11 @@ "picomatch": "^2.2.1" } }, + "real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==" + }, "recaptcha-v2": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/recaptcha-v2/-/recaptcha-v2-0.1.3.tgz", @@ -5149,6 +5659,11 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, + "safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==" + }, "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -5163,6 +5678,11 @@ "sparse-bitfield": "^3.0.3" } }, + "secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==" + }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -5349,6 +5869,14 @@ } } }, + "sonic-boom": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", + "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==", + "requires": { + "atomic-sleep": "^1.0.0" + } + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -5374,6 +5902,11 @@ "memory-pager": "^1.0.2" } }, + "split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==" + }, "statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -5436,6 +5969,14 @@ "yallist": "^4.0.0" } }, + "thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "requires": { + "real-require": "^0.2.0" + } + }, "to-array": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", diff --git a/server/package.json b/server/package.json index e7aa15dd4..9cc3b4513 100644 --- a/server/package.json +++ b/server/package.json @@ -12,6 +12,7 @@ "start-db-migrate:prod": "node dist/db/migrate.js", "start-db-recalc-ranks:dev": "ts-node-dev --transpile-only --respawn --inspect=9234 db/recalculateRankings.ts", "start-db-recalc-ranks:prod": "node dist/db/recalculateRankings.js", + "restore-game": "ts-node-dev --transpile-only db/restoreGame.ts", "prebuild": "rm -rf ./dist", "build": "tsc", "postbuild": "cp -r services/emailTemplates ./dist/services/emailTemplates && cp .env ./dist | true", @@ -42,6 +43,8 @@ "mongoose": "^5.12.8", "mongoose-lean-defaults": "^0.4.1", "nodemailer": "^6.5.0", + "pino": "^9.5.0", + "pino-pretty": "^11.3.0", "qheap": "^1.4.0", "random-seed": "^0.3.0", "recaptcha-v2": "^0.1.3", diff --git a/server/services/ai.ts b/server/services/ai.ts index b1c551c27..7768f6f1b 100644 --- a/server/services/ai.ts +++ b/server/services/ai.ts @@ -22,6 +22,7 @@ import BasicAIService from "./basicAi"; import PlayerAfkService from "./playerAfk"; import ShipService from "./ship"; import PathfindingService from "./pathfinding"; +import {logger} from "../utils/logging"; const Heap = require('qheap'); const mongoose = require("mongoose"); @@ -123,6 +124,8 @@ interface Movement { score: number; } +const log = logger("AI Service"); + // IMPORTANT IMPLEMENTATION NOTES // During AI tick, care must be taken to NEVER write any changes to the database. // This is performed automatically by mongoose (when calling game.save()). @@ -203,7 +206,7 @@ export default class AIService { await this.basicAIService._doBasicLogic(game, player, isFirstTickOfCycle, isLastTickOfCycle); } } catch (e) { - console.error(e); + log.error(e); } } diff --git a/server/services/discord.ts b/server/services/discord.ts index f1557db1f..33aa7f751 100644 --- a/server/services/discord.ts +++ b/server/services/discord.ts @@ -2,9 +2,12 @@ import ValidationError from '../errors/validation'; import Repository from './repository'; import { Config } from '../config/types/Config'; import { User } from './types/User'; +import {logger} from "../utils/logging"; const Discord = require('discord.js'); +const log = logger("Discord Service"); + export default class DiscordService { config: Config; userRepo: Repository; @@ -24,7 +27,7 @@ export default class DiscordService { this.client = new Discord.Client() await this.client.login(this.config.discord.botToken); - console.log('Discord Initialized'); + log.info('Discord Initialized'); } } @@ -102,7 +105,7 @@ export default class DiscordService { embed: messageTemplate }); } catch (err) { - console.error(err); + log.error(err); } } @@ -126,7 +129,7 @@ export default class DiscordService { embed: messageTemplate }); } catch (err) { - console.error(err); + log.error(err); } } } \ No newline at end of file diff --git a/server/services/email.ts b/server/services/email.ts index 6689fb5fa..28928d5d4 100644 --- a/server/services/email.ts +++ b/server/services/email.ts @@ -14,15 +14,18 @@ import GamePlayerAFKEvent from "./types/events/GamePlayerAFK"; import { BaseGameEvent } from "./types/events/BaseGameEvent"; import GameJoinService, { GameJoinServiceEvents } from "./gameJoin"; import PlayerReadyService, { PlayerReadyServiceEvents } from "./playerReady"; +import {logger} from "../utils/logging"; const nodemailer = require('nodemailer'); const fs = require('fs'); const path = require('path'); +const log = logger("Email Service"); + function getFakeTransport() { return { async sendMail(message) { - console.log(`SMTP DISABLED`); + log.info(`SMTP DISABLED`); // console.log(message.text); // console.log(message.html); } @@ -162,7 +165,7 @@ export default class EmailService { text }; - console.log(`EMAIL: [${message.to}] - ${subject}`); + log.info(`EMAIL: [${message.to}] - ${subject}`); return await transport.sendMail(message); } @@ -176,8 +179,8 @@ export default class EmailService { subject, html }; - - console.log(`EMAIL HTML: [${message.to}] - ${subject}`); + + log.info(`EMAIL HTML: [${message.to}] - ${subject}`); return await transport.sendMail(message); } @@ -214,7 +217,7 @@ export default class EmailService { try { await this.sendTemplate(user.email, this.TEMPLATES.WELCOME, [user.username]); } catch (err) { - console.error(err); + log.error(err); } } @@ -411,7 +414,7 @@ export default class EmailService { try { await this.sendTemplate(user.email, template, args); } catch (err) { - console.error(err); + log.error(err); } } } diff --git a/server/services/game.ts b/server/services/game.ts index 3c9b39178..3553b2df5 100644 --- a/server/services/game.ts +++ b/server/services/game.ts @@ -22,6 +22,7 @@ import GamePlayerDefeatedEvent from './types/events/GamePlayerDefeated'; import {LeaderboardPlayer} from "./types/Leaderboard"; import GameJoinService from "./gameJoin"; import GameAuthService from "./gameAuth"; +import cluster from "cluster"; export const GameServiceEvents = { onPlayerQuit: 'onPlayerQuit', @@ -259,6 +260,29 @@ export default class GameService extends EventEmitter { }); } + async kickPlayer(game: Game, kickingUser: DBObjectId, playerToKick: DBObjectId) { + if (!await this.gameAuthService.isGameAdmin(game, kickingUser)) { + throw new ValidationError('You do not have permission to force start this game.'); + } + + console.log({ + kickingUser, + playerToKick + }) + + const player = game.galaxy.players.find(p => p._id.toString() === playerToKick.toString()); + + if (!player) { + throw new ValidationError('Player not found'); + } + + if (game.state.startDate) { + await this.concedeDefeat(game, player, true); + } else { + await this.quit(game, player); + } + } + async forceStart(game: Game, forceStartingUserId: DBObjectId) { if (!await this.gameAuthService.isGameAdmin(game, forceStartingUserId)) { throw new ValidationError('You do not have permission to force start this game.'); diff --git a/server/services/gameTick.ts b/server/services/gameTick.ts index 25e4bef5f..2e0bf562e 100644 --- a/server/services/gameTick.ts +++ b/server/services/gameTick.ts @@ -41,10 +41,13 @@ import ShipService from "./ship"; import ScheduleBuyService from "./scheduleBuy"; import {Moment} from "moment"; import GameLockService from "./gameLock"; +import {logger} from "../utils/logging"; const EventEmitter = require('events'); const moment = require('moment'); +const log = logger("Game Tick Service"); + export const GameTickServiceEvents = { onPlayerGalacticCycleCompleted: 'onPlayerGalacticCycleCompleted', onGameCycleEnded: 'onGameCycleEnded', @@ -159,7 +162,7 @@ export default class GameTickService extends EventEmitter { const game = (await this.gameService.getByIdAll(gameId)); if (!game) { - console.error(`Game not found: ${gameId}`); + log.error(`Game not found: ${gameId}`); return; } @@ -179,7 +182,10 @@ export default class GameTickService extends EventEmitter { */ let startTime = process.hrtime(); - console.log(`[${game.settings.general.name}] - Game tick started at ${new Date().toISOString()}`); + log.info({ + gameId: game._id, + gameName: game.settings.general.name + }, `[${game.settings.general.name}] - Game tick started at ${new Date().toISOString()}`); game.state.lastTickDate = moment().utc(); game.state.forceTick = false; @@ -190,7 +196,11 @@ export default class GameTickService extends EventEmitter { let logTime = (taskName: string) => { taskTimeEnd = process.hrtime(taskTime); taskTime = process.hrtime(); - console.log(`[${game.settings.general.name}] - ${taskName}: %ds %dms'`, taskTimeEnd[0], taskTimeEnd[1] / 1000000); + log.info({ + gameId: game._id, + gameName: game.settings.general.name, + tick: game.state.tick + }, `[${game.settings.general.name}] - ${taskName}: %ds %dms'`, taskTimeEnd[0], taskTimeEnd[1] / 1000000); }; let gameUsers = await this.userService.getGameUsers(game); @@ -206,9 +216,20 @@ export default class GameTickService extends EventEmitter { this.playerService.incrementMissedTurns(game); } + // Check if win condition was reached before the tick (for example due to RTQ) + let hasWinnerBeforeTick = this._gameWinCheck(game, gameUsers); + if (hasWinnerBeforeTick) { + log.info({ + gameId: game._id, + gameName: game.settings.general.name, + tick: game.state.tick + }, `Game has reached a win condition before the tick. Tick processing will be skipped.`); + iterations = 0; + } + let hasProductionTicked: boolean = false; - while (iterations--) { + while (iterations > 0) { if (!await this.gameLockService.isLockedInDatabase(game._id)) { throw new Error(`The game was not locked after game processing, concurrency issue?`); } @@ -275,6 +296,8 @@ export default class GameTickService extends EventEmitter { if (hasWinner) { break; } + + iterations--; } // TODO: This has been moved out of _moveCarriers, see comment in there. @@ -301,7 +324,10 @@ export default class GameTickService extends EventEmitter { let endTime = process.hrtime(startTime); - console.log(`[${game.settings.general.name}] - Game tick ended: %ds %dms'`, endTime[0], endTime[1] / 1000000); + log.info({ + gameId: game._id, + gameName: game.settings.general.name + }, `[${game.settings.general.name}] - Game tick ended: %ds %dms'`, endTime[0], endTime[1] / 1000000); } canTick(game: Game) { diff --git a/server/services/history.ts b/server/services/history.ts index d7d5f51a8..b65473164 100644 --- a/server/services/history.ts +++ b/server/services/history.ts @@ -1,3 +1,5 @@ +import GameStateService from "./gameState"; + const cache = require('memory-cache'); import { DBObjectId } from './types/DBObjectId'; import ValidationError from '../errors/validation'; @@ -13,30 +15,34 @@ export default class HistoryService { playerService: PlayerService; gameService: GameService; playerStatisticsService: PlayerStatisticsService; + gameStateService: GameStateService; constructor( historyRepo: Repository, playerService: PlayerService, gameService: GameService, - playerStatisticsService: PlayerStatisticsService + playerStatisticsService: PlayerStatisticsService, + gameStateService: GameStateService, ) { this.historyRepo = historyRepo; this.playerService = playerService; this.gameService = gameService; this.playerStatisticsService = playerStatisticsService; + this.gameStateService = gameStateService; this.gameService.on('onGameDeleted', (args) => this.deleteByGameId(args.gameId)); } async listIntel(gameId: DBObjectId, startTick: number, endTick: number) { - let settings = await this.gameService.getGameSettings(gameId); + const game = await this.gameService.getById(gameId); - if (!settings || settings.specialGalaxy.darkGalaxy === 'extra') { + // change here + if (!game?.settings || (game?.settings.specialGalaxy.darkGalaxy === 'extra' && !this.gameStateService.isFinished(game))) { throw new ValidationError('Intel is not available in this game mode.'); } startTick = startTick || 0; - endTick = endTick || Number.MAX_VALUE;; + endTick = endTick || Number.MAX_VALUE; let cacheKey = `intel_${gameId}_${startTick}_${endTick}`; let cached = cache.get(cacheKey); diff --git a/server/services/index.ts b/server/services/index.ts index 0b885fe02..9e218ac16 100644 --- a/server/services/index.ts +++ b/server/services/index.ts @@ -113,10 +113,13 @@ import PlayerColourService from "./playerColour"; import GameMaskingService from "./gameMaskingService"; import SessionService from "./session"; import starMovementService from "./starMovement"; +import {logger} from "../utils/logging"; const gameNames = require('../config/game/gameNames'); const starNames = require('../config/game/starNames'); +const log = logger("Dependency Container"); + const gameRepository = new Repository(GameModel); const userRepository = new Repository(UserModel); const historyRepository = new Repository(HistoryModel); @@ -200,7 +203,7 @@ export default (config): DependencyContainer => { const userLeaderboardService = new UserLeaderboardService(userRepository, guildUserService); const researchService = new ResearchService(gameRepository, technologyService, randomService, playerStatisticsService, starService, userService, gameTypeService); const combatService = new CombatService(technologyService, specialistService, playerService, starService, reputationService, diplomacyService, gameTypeService); - const historyService = new HistoryService(historyRepository, playerService, gameService, playerStatisticsService); + const historyService = new HistoryService(historyRepository, playerService, gameService, playerStatisticsService, gameStateService); const waypointService = new WaypointService(gameRepository, carrierService, starService, distanceService, starDistanceService, technologyService, gameService, playerService, carrierMovementService, gameMaskingService, historyService); const specialistBanService = new SpecialistBanService(specialistService); const specialistHireService = new SpecialistHireService(gameRepository, specialistService, achievementService, waypointService, playerCreditsService, starService, gameTypeService, specialistBanService, technologyService); @@ -231,7 +234,7 @@ export default (config): DependencyContainer => { const gameMutexService = new GameMutexService(); - console.log('Dependency container initialized.'); + log.info('Dependency container initialized.'); return { config, diff --git a/server/services/notification.ts b/server/services/notification.ts index ceeb668fa..1e0b23485 100644 --- a/server/services/notification.ts +++ b/server/services/notification.ts @@ -16,6 +16,9 @@ import GameEndedEvent from './types/events/GameEnded'; import GameTurnEndedEvent from './types/events/GameTurnEnded'; import ConversationMessageSentEvent from './types/events/ConversationMessageSent'; import GameJoinService, { GameJoinServiceEvents } from './gameJoin'; +import {logger} from "../utils/logging"; + +const log = logger("Notification Service"); // Note: We only support discord subscriptions at this point, if any new ones are added // this class will need to be refactored to use something like the strategy pattern. @@ -81,7 +84,7 @@ export default class NotificationService { this.tradeService.on(TradeServiceEvents.onPlayerRenownReceived, (args) => this.onPlayerRenownReceived(args.gameId, args.fromPlayer, args.toPlayer, args.amount)); this.tradeService.on(TradeServiceEvents.onPlayerTechnologyReceived, (args) => this.onPlayerTechnologyReceived(args.gameId, args.fromPlayer, args.toPlayer, args.technology)); - console.log('Notifications initialized.') + log.info('Notifications initialized.') } } diff --git a/server/services/playerAfk.ts b/server/services/playerAfk.ts index 70a71d67a..e1d39b701 100644 --- a/server/services/playerAfk.ts +++ b/server/services/playerAfk.ts @@ -60,7 +60,7 @@ export default class PlayerAfkService extends EventEmitter { if (!player.afk) { // Check if the player has been AFK. - let isAfk = this.isAfk(game, player); + const isAfk = this.isAfk(game, player); if (isAfk) { this.setPlayerAsAfk(game, player); @@ -69,11 +69,11 @@ export default class PlayerAfkService extends EventEmitter { // Check if the player has been defeated by conquest. if (!player.defeated) { - let stars = this.starService.listStarsOwnedByPlayer(game.galaxy.stars, player._id); + const stars = this.starService.listStarsOwnedByPlayer(game.galaxy.stars, player._id); // If there are no stars and there are no carriers then the player is defeated. if (stars.length === 0) { - let carriers = this.carrierService.listCarriersOwnedByPlayer(game.galaxy.carriers, player._id); // Note: This logic looks a bit weird, but its more performant. + const carriers = this.carrierService.listCarriersOwnedByPlayer(game.galaxy.carriers, player._id); // Note: This logic looks a bit weird, but its more performant. if (carriers.length === 0) { this.playerService.setPlayerAsDefeated(game, player, false); @@ -146,6 +146,11 @@ export default class PlayerAfkService extends EventEmitter { return false; } + // if the player is ready for turn/cycle in a TB game, they are not afk + if (player.ready) { + return false; + } + let lastSeenMoreThanXDaysAgo = moment(player.lastSeen).utc() <= moment().utc().subtract(game.settings.gameTime.afk.lastSeenTimeout, 'days'); if (lastSeenMoreThanXDaysAgo) { diff --git a/server/services/scheduleBuy.ts b/server/services/scheduleBuy.ts index 6b6a666a5..5c4615d8c 100644 --- a/server/services/scheduleBuy.ts +++ b/server/services/scheduleBuy.ts @@ -1,3 +1,5 @@ +import {logger} from "../utils/logging"; + const mongoose = require('mongoose'); import ValidationError from '../errors/validation'; @@ -8,6 +10,7 @@ import {Game} from "./types/Game"; import {Player, PlayerScheduledActions} from "./types/Player"; import {InfrastructureType} from './types/Star'; import {ObjectId} from "mongoose"; +import { DBObjectId } from "./types/DBObjectId"; const buyTypeToPriority = { @@ -19,6 +22,8 @@ const buyTypeToPriority = { const EventEmitter = require('events'); +const log = logger("Schedule Buy Service"); + export default class ScheduleBuyService extends EventEmitter { gameRepo: Repository; starUpgradeService: StarUpgradeService @@ -70,7 +75,7 @@ export default class ScheduleBuyService extends EventEmitter { await this.starUpgradeService.executeBulkUpgradeReport(game, player, report); } catch (e) { - console.error(e) + log.error(e) } } @@ -80,7 +85,7 @@ export default class ScheduleBuyService extends EventEmitter { const totalPercentage = percentageActions.reduce((total, cur) => total + cur.amount, 0); await this._executePercentageAction(game, player, percentageActions, totalPercentage); } catch (e) { - console.error(e) + log.error(e) } // Only keep actions that are repeated or in the future @@ -139,8 +144,8 @@ export default class ScheduleBuyService extends EventEmitter { return action; } - async toggleBulkRepeat(game: Game, player: Player, actionId: ObjectId) { - let action = player.scheduledActions.find(a => a._id == actionId); + async toggleBulkRepeat(game: Game, player: Player, actionId: DBObjectId) { + const action = player.scheduledActions.find(a => a._id.toString() == actionId.toString()); if (!action) { throw new ValidationError('Action does not exist'); } @@ -159,8 +164,8 @@ export default class ScheduleBuyService extends EventEmitter { return action; } - async trashAction(game: Game, player: Player, actionId: ObjectId) { - let action = player.scheduledActions.find(a => a._id == actionId); + async trashAction(game: Game, player: Player, actionId: DBObjectId) { + const action = player.scheduledActions.find(a => a._id.toString() == actionId.toString()); if (!action) { throw new ValidationError('Action does not exist'); } diff --git a/server/services/types/DBObjectId.ts b/server/services/types/DBObjectId.ts index 1f9b14d63..97d300a9a 100644 --- a/server/services/types/DBObjectId.ts +++ b/server/services/types/DBObjectId.ts @@ -1,10 +1,11 @@ import mongoose, { ObjectId } from "mongoose"; -import { ObjectId as MongoObjectId } from "mongodb" -export interface DBObjectId extends ObjectId { +export interface DBObjectId extends mongoose.Types.ObjectId { // equals(id: DBObjectId): boolean; -- Note: We never use this as we cannot ensure that anything that comes through the API layer via params are mongo object IDs unless we explicitly cast them. getTimestamp(): Date; toString(): string; }; -export const objectId = (): DBObjectId => new mongoose.Types.ObjectId() as any; \ No newline at end of file +export const objectId = (): DBObjectId => new mongoose.Types.ObjectId() as any; + +export const objectIdFromString = (s: string): DBObjectId => new mongoose.Types.ObjectId(s); \ No newline at end of file diff --git a/server/services/types/Mutex.ts b/server/services/types/Mutex.ts index c863ec6ef..1c76c2063 100644 --- a/server/services/types/Mutex.ts +++ b/server/services/types/Mutex.ts @@ -1,4 +1,7 @@ import crypto from 'crypto' +import {logger} from "../../utils/logging"; + +const log = logger("Mutex"); export class Mutex { static lastMutexId: number = 0; @@ -30,7 +33,7 @@ export class Mutex { return true; } else { - console.warn(`Cannot unlock Mutex wiht id ${this.id} as lockId does not match. Expected: ${lockId}, Actual: ${lockIdInput}`); + log.warn(`Cannot unlock Mutex wiht id ${this.id} as lockId does not match. Expected: ${lockId}, Actual: ${lockIdInput}`); } return false; diff --git a/server/utils/logging.ts b/server/utils/logging.ts new file mode 100644 index 000000000..681880855 --- /dev/null +++ b/server/utils/logging.ts @@ -0,0 +1,41 @@ +import {Logger} from "pino"; +import config from '../config'; + +const pino = require('pino'); +let transport; +let baseLogger; + +export const setupLogging = () => { + const loggingT = config.logging || 'stdout'; + + if (loggingT === 'pretty') { + transport = pino.transport({ + target: 'pino-pretty' + }); + } else if (loggingT === 'stdout') { + transport = pino.transport({ + target: 'pino/file', + options: {destination: 1} + }); + } else { + throw new Error(`Invalid logging type: ${loggingT}`); + } + + baseLogger = pino(transport); +} + +export const onReady = (callback: () => void) => { + transport.on('ready', callback); +} + +export const logger = (name?: string): Logger => { + if (!baseLogger) { + setupLogging(); + } + + if (name) { + return baseLogger.child({name}); + } else { + return baseLogger; + } +} \ No newline at end of file