From 3796aab81b90031226cadb62ae157505302aee93 Mon Sep 17 00:00:00 2001 From: AlecM33 Date: Mon, 4 Jul 2022 15:08:47 -0400 Subject: [PATCH 1/4] beginning admin api --- client/src/scripts/create.js | 6 ++++ client/src/scripts/game.js | 3 ++ client/src/scripts/home.js | 5 +++ client/src/scripts/howToUse.js | 10 +++++- client/src/scripts/join.js | 5 +++ client/src/scripts/notFound.js | 10 +++++- index.js | 6 ++++ package.json | 4 +-- server/api/AdminAPI.js | 64 +++++++++++++++++++++++++++++++++ server/api/GamesAPI.js | 32 ++++++++--------- server/config/globals.js | 15 ++++++-- server/modules/GameManager.js | 7 +--- server/modules/SocketManager.js | 27 ++++++++++++++ 13 files changed, 166 insertions(+), 28 deletions(-) create mode 100644 server/api/AdminAPI.js create mode 100644 server/modules/SocketManager.js diff --git a/client/src/scripts/create.js b/client/src/scripts/create.js index c60d852..9303cc7 100644 --- a/client/src/scripts/create.js +++ b/client/src/scripts/create.js @@ -2,9 +2,15 @@ import { DeckStateManager } from '../modules/DeckStateManager.js'; import { GameCreationStepManager } from '../modules/GameCreationStepManager.js'; import { injectNavbar } from '../modules/Navbar.js'; import createTemplate from '../view_templates/CreateTemplate.js'; +import { io } from 'socket.io-client'; +import { toast } from '../modules/Toast'; const create = () => { injectNavbar(); + const socket = io(); + socket.on('broadcast', (message) => { + toast(message, 'warning', true, false); + }); document.getElementById('game-creation-container').innerHTML = createTemplate; const deckManager = new DeckStateManager(); const gameCreationStepManager = new GameCreationStepManager(deckManager); diff --git a/client/src/scripts/game.js b/client/src/scripts/game.js index 995b34c..3db7206 100644 --- a/client/src/scripts/game.js +++ b/client/src/scripts/game.js @@ -34,6 +34,9 @@ const game = () => { UserUtility.validateAnonUserSignature(res.content) ); }); + socket.on('broadcast', (message) => { + toast(message, 'warning', true, false); + }); socket.on('connect_error', (err) => { toast(err, 'error', true, false); }); diff --git a/client/src/scripts/home.js b/client/src/scripts/home.js index 17a9a94..d874772 100644 --- a/client/src/scripts/home.js +++ b/client/src/scripts/home.js @@ -1,8 +1,13 @@ import { XHRUtility } from '../modules/XHRUtility.js'; import { toast } from '../modules/Toast.js'; import { injectNavbar } from '../modules/Navbar.js'; +import { io } from 'socket.io-client'; const home = () => { + const socket = io(); + socket.on('broadcast', (message) => { + toast(message, 'warning', true, false); + }); injectNavbar(); document.getElementById('join-form').addEventListener('submit', attemptToJoinGame); }; diff --git a/client/src/scripts/howToUse.js b/client/src/scripts/howToUse.js index e7d2679..9446089 100644 --- a/client/src/scripts/howToUse.js +++ b/client/src/scripts/howToUse.js @@ -1,6 +1,14 @@ import { injectNavbar } from '../modules/Navbar.js'; +import { io } from 'socket.io-client'; +import { toast } from '../modules/Toast'; -const howToUse = () => { injectNavbar(); }; +const howToUse = () => { + injectNavbar(); + const socket = io(); + socket.on('broadcast', (message) => { + toast(message, 'warning', true, false); + }); +}; if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { module.exports = howToUse; diff --git a/client/src/scripts/join.js b/client/src/scripts/join.js index cd8a3c0..222cc56 100644 --- a/client/src/scripts/join.js +++ b/client/src/scripts/join.js @@ -3,9 +3,14 @@ import { toast } from '../modules/Toast.js'; import { XHRUtility } from '../modules/XHRUtility.js'; import { UserUtility } from '../modules/UserUtility.js'; import { globals } from '../config/globals.js'; +import { io } from 'socket.io-client'; const join = () => { injectNavbar(); + const socket = io(); + socket.on('broadcast', (message) => { + toast(message, 'warning', true, false); + }); const splitUrl = window.location.pathname.split('/join/'); const accessCode = splitUrl[1]; if (/^[a-zA-Z0-9]+$/.test(accessCode) && accessCode.length === globals.ACCESS_CODE_LENGTH) { diff --git a/client/src/scripts/notFound.js b/client/src/scripts/notFound.js index 68e7047..dafcea0 100644 --- a/client/src/scripts/notFound.js +++ b/client/src/scripts/notFound.js @@ -1,6 +1,14 @@ import { injectNavbar } from '../modules/Navbar.js'; +import { io } from 'socket.io-client'; +import { toast } from '../modules/Toast'; -const notFound = () => { injectNavbar(); }; +const notFound = () => { + injectNavbar(); + const socket = io(); + socket.on('broadcast', (message) => { + toast(message, 'warning', true, false); + }); +}; if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { module.exports = notFound; diff --git a/index.js b/index.js index 28fcfce..76b523b 100644 --- a/index.js +++ b/index.js @@ -5,6 +5,7 @@ const path = require('path'); const app = express(); const bodyParser = require('body-parser'); const GameManager = require('./server/modules/GameManager.js'); +const SocketManager = require('./server/modules/SocketManager.js'); const globals = require('./server/config/globals'); const ServerBootstrapper = require('./server/modules/ServerBootstrapper'); @@ -34,6 +35,9 @@ if (process.env.NODE_ENV.trim() === 'development') { gameManager = new GameManager(logger, globals.ENVIRONMENT.PRODUCTION, gameNamespace).getInstance(); } +/* Instantiate the singleton socket manager */ +const socketManager = new SocketManager(logger, inGameSocketServer).getInstance(); + gameNamespace.on('connection', function (socket) { socket.on('disconnecting', (reason) => { logger.trace('client socket disconnecting because: ' + reason); @@ -43,7 +47,9 @@ gameNamespace.on('connection', function (socket) { /* api endpoints */ const games = require('./server/api/GamesAPI'); +const admin = require('./server/api/AdminAPI'); app.use('/api/games', games); +app.use('/api/admin', admin); /* serve all the app's pages */ app.use('/manifest.json', (req, res) => { diff --git a/package.json b/package.json index dc38d1f..027a4a6 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,8 @@ "start:dev:no-hot-reload": "NODE_ENV=development && node index.js", "start:dev:windows": "SET NODE_ENV=development && nodemon index.js", "start:dev:windows:no-hot-reload": "SET NODE_ENV=development && node index.js", - "start": "NODE_ENV=production node index.js -- loglevel=info", - "start:windows": "SET NODE_ENV=production && node index.js -- loglevel=warn port=8080", + "start": "NODE_ENV=production node index.js -- loglevel=debug", + "start:windows": "SET NODE_ENV=production && node index.js -- loglevel=debug port=8080", "test": "jasmine && karma start --single-run --browsers ChromeHeadless karma.conf.js", "test:unit": "jasmine", "test:e2e": "karma start --single-run --browsers ChromeHeadless karma.conf.js" diff --git a/server/api/AdminAPI.js b/server/api/AdminAPI.js new file mode 100644 index 0000000..9f2e08b --- /dev/null +++ b/server/api/AdminAPI.js @@ -0,0 +1,64 @@ +const express = require('express'); +const router = express.Router(); +const debugMode = Array.from(process.argv.map((arg) => arg.trim().toLowerCase())).includes('debug'); +const logger = require('../modules/Logger')(debugMode); +const socketManager = new (require('../modules/SocketManager.js'))().getInstance(); +const gameManager = new (require('../modules/GameManager.js'))().getInstance(); +const globals = require('../config/globals.js'); +const cors = require('cors'); +const rateLimit = require('express-rate-limit').default; + +const KEY = process.env.NODE_ENV.trim() === 'development' + ? globals.MOCK_AUTH + : process.env.ADMIN_KEY; + +const apiLimiter = rateLimit({ + windowMs: 60000, + max: 50, + standardHeaders: true, + legacyHeaders: false +}); + +if (process.env.NODE_ENV.trim() === 'production') { + router.use(apiLimiter); +} + +router.use(cors(globals.CORS)); + +router.use((req, res, next) => { + if (isAuthorized(req)) { + next(); + } else { + res.status(401).send('You are not authorized to make this request.'); + } +}); + +router.post('/sockets/broadcast', function (req, res) { + logger.info('admin user broadcasting message: ' + req.body?.message); + socketManager.broadcast(req.body?.message); + res.status(201).send('Broadcasted message to all connected sockets: ' + req.body?.message); +}); + +router.get('/games/state', function (req, res) { + res.status(200).send(gameManager.activeGameRunner.activeGames); +}); + +router.put('/games/state', function (req, res) { + // TODO: validate the request body - can break the application if malformed. + gameManager.activeGameRunner.activeGames = req.body; + res.status(201).send(gameManager.activeGameRunner.activeGames); +}); + +/* validates Basic Auth */ +function isAuthorized (req) { + const header = req.headers.authorization; + if (header) { + const token = header.split(/\s+/).pop() || ''; + const decodedToken = Buffer.from(token, 'base64').toString(); + return decodedToken.trim() === KEY.trim(); + } + + return false; +} + +module.exports = router; diff --git a/server/api/GamesAPI.js b/server/api/GamesAPI.js index ae4364b..090403a 100644 --- a/server/api/GamesAPI.js +++ b/server/api/GamesAPI.js @@ -4,33 +4,33 @@ const debugMode = Array.from(process.argv.map((arg) => arg.trim().toLowerCase()) const logger = require('../modules/Logger')(debugMode); const GameManager = require('../modules/GameManager.js'); const rateLimit = require('express-rate-limit').default; -const globals = require('../config/globals'); +const globals = require('../config/globals.js'); const cors = require('cors'); const gameManager = new GameManager().getInstance(); const apiLimiter = rateLimit({ - windowMs: 600000, - max: 5, + windowMs: 60000, + max: 100, standardHeaders: true, legacyHeaders: false }); -const corsOptions = process.env.NODE_ENV.trim() === 'development' - ? { - origin: '*', - optionsSuccessStatus: 200 - } - : { - origin: 'https://playwerewolf.uk.r.appspot.com', - optionsSuccessStatus: 200 - }; +const gameEndpointLimiter = rateLimit({ // further limit the rate of game creation to 30 games per 10 minutes. + windowMs: 600000, + max: 30, + standardHeaders: true, + legacyHeaders: false +}); -router.use(cors(corsOptions)); -router.options('/:code/players', cors(corsOptions)); +router.use(cors(globals.CORS)); +router.options('/:code/players', cors(globals.CORS)); +router.options('/create', cors(globals.CORS)); +router.options('/restart', cors(globals.CORS)); -if (process.env.NODE_ENV.trim() === 'production') { // in prod, limit clients to creating 5 games per 10 minutes. - router.use('/create', apiLimiter); +if (process.env.NODE_ENV.trim() === 'production') { + router.use(apiLimiter); + router.use('/create', gameEndpointLimiter); } router.post('/create', function (req, res) { diff --git a/server/config/globals.js b/server/config/globals.js index 17c7c81..242b91e 100644 --- a/server/config/globals.js +++ b/server/config/globals.js @@ -3,6 +3,15 @@ const globals = { ACCESS_CODE_LENGTH: 4, ACCESS_CODE_GENERATION_ATTEMPTS: 50, CLOCK_TICK_INTERVAL_MILLIS: 100, + CORS: process.env.NODE_ENV.trim() === 'development' + ? { + origin: '*', + optionsSuccessStatus: 200 + } + : { + origin: 'https://play-werewolf.app', + optionsSuccessStatus: 200 + }, STALE_GAME_HOURS: 12, CLIENT_COMMANDS: { FETCH_GAME_STATE: 'fetchGameState', @@ -41,7 +50,8 @@ const globals = { PLAYER_JOINED: 'playerJoined', PLAYER_LEFT: 'playerLeft', SYNC_GAME_STATE: 'syncGameState', - NEW_SPECTATOR: 'newSpectator' + NEW_SPECTATOR: 'newSpectator', + BROADCAST: 'broadcast' }, ENVIRONMENT: { LOCAL: 'local', @@ -61,7 +71,8 @@ const globals = { PAUSE_TIMER: 'pauseTimer', RESUME_TIMER: 'resumeTimer', GET_TIME_REMAINING: 'getTimeRemaining' - } + }, + MOCK_AUTH: 'mock_auth' }; module.exports = globals; diff --git a/server/modules/GameManager.js b/server/modules/GameManager.js index 9fb7079..e83b8e1 100644 --- a/server/modules/GameManager.js +++ b/server/modules/GameManager.js @@ -199,12 +199,7 @@ class GameManager { checkAvailability = (code) => { const game = this.activeGameRunner.activeGames[code.toUpperCase()]; if (game) { - const unassignedPerson = game.people.find((person) => person.assigned === false); - if (!unassignedPerson) { - return Promise.resolve(new Error(globals.ERROR_MESSAGE.GAME_IS_FULL)); - } else { - return Promise.resolve({ accessCode: code, playerCount: getGameSize(game.deck), timerParams: game.timerParams }); - } + return Promise.resolve({ accessCode: code, playerCount: getGameSize(game.deck), timerParams: game.timerParams }); } else { return Promise.resolve(404); } diff --git a/server/modules/SocketManager.js b/server/modules/SocketManager.js new file mode 100644 index 0000000..35bf0e0 --- /dev/null +++ b/server/modules/SocketManager.js @@ -0,0 +1,27 @@ +const globals = require('../config/globals.js'); + +class SocketManager { + constructor (logger, io) { + this.logger = logger; + this.io = io; + } + + broadcast = (message) => { + this.io.emit(globals.EVENTS.BROADCAST, message); + }; +} + +class Singleton { + constructor (logger, io) { + if (!Singleton.instance) { + logger.info('CREATING SINGLETON SOCKET MANAGER'); + Singleton.instance = new SocketManager(logger, io); + } + } + + getInstance () { + return Singleton.instance; + } +} + +module.exports = Singleton; From de3a2b64cbdd771eecc449bf2e428a890d6ec85a Mon Sep 17 00:00:00 2001 From: AlecM33 Date: Tue, 5 Jul 2022 19:17:36 -0400 Subject: [PATCH 2/4] middleware for content-type validation, refactoring --- client/src/scripts/create.js | 5 ----- client/src/scripts/home.js | 5 ----- client/src/scripts/howToUse.js | 6 ------ client/src/scripts/join.js | 5 ----- client/src/scripts/notFound.js | 6 ------ index.js | 6 +----- server/api/AdminAPI.js | 11 ++++++++++- server/api/GamesAPI.js | 10 ++++++++++ server/config/globals.js | 8 ++++++++ 9 files changed, 29 insertions(+), 33 deletions(-) diff --git a/client/src/scripts/create.js b/client/src/scripts/create.js index 9303cc7..5045c75 100644 --- a/client/src/scripts/create.js +++ b/client/src/scripts/create.js @@ -2,15 +2,10 @@ import { DeckStateManager } from '../modules/DeckStateManager.js'; import { GameCreationStepManager } from '../modules/GameCreationStepManager.js'; import { injectNavbar } from '../modules/Navbar.js'; import createTemplate from '../view_templates/CreateTemplate.js'; -import { io } from 'socket.io-client'; import { toast } from '../modules/Toast'; const create = () => { injectNavbar(); - const socket = io(); - socket.on('broadcast', (message) => { - toast(message, 'warning', true, false); - }); document.getElementById('game-creation-container').innerHTML = createTemplate; const deckManager = new DeckStateManager(); const gameCreationStepManager = new GameCreationStepManager(deckManager); diff --git a/client/src/scripts/home.js b/client/src/scripts/home.js index d874772..17a9a94 100644 --- a/client/src/scripts/home.js +++ b/client/src/scripts/home.js @@ -1,13 +1,8 @@ import { XHRUtility } from '../modules/XHRUtility.js'; import { toast } from '../modules/Toast.js'; import { injectNavbar } from '../modules/Navbar.js'; -import { io } from 'socket.io-client'; const home = () => { - const socket = io(); - socket.on('broadcast', (message) => { - toast(message, 'warning', true, false); - }); injectNavbar(); document.getElementById('join-form').addEventListener('submit', attemptToJoinGame); }; diff --git a/client/src/scripts/howToUse.js b/client/src/scripts/howToUse.js index 9446089..6cf8d9d 100644 --- a/client/src/scripts/howToUse.js +++ b/client/src/scripts/howToUse.js @@ -1,13 +1,7 @@ import { injectNavbar } from '../modules/Navbar.js'; -import { io } from 'socket.io-client'; -import { toast } from '../modules/Toast'; const howToUse = () => { injectNavbar(); - const socket = io(); - socket.on('broadcast', (message) => { - toast(message, 'warning', true, false); - }); }; if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { diff --git a/client/src/scripts/join.js b/client/src/scripts/join.js index 222cc56..cd8a3c0 100644 --- a/client/src/scripts/join.js +++ b/client/src/scripts/join.js @@ -3,14 +3,9 @@ import { toast } from '../modules/Toast.js'; import { XHRUtility } from '../modules/XHRUtility.js'; import { UserUtility } from '../modules/UserUtility.js'; import { globals } from '../config/globals.js'; -import { io } from 'socket.io-client'; const join = () => { injectNavbar(); - const socket = io(); - socket.on('broadcast', (message) => { - toast(message, 'warning', true, false); - }); const splitUrl = window.location.pathname.split('/join/'); const accessCode = splitUrl[1]; if (/^[a-zA-Z0-9]+$/.test(accessCode) && accessCode.length === globals.ACCESS_CODE_LENGTH) { diff --git a/client/src/scripts/notFound.js b/client/src/scripts/notFound.js index dafcea0..e83aec9 100644 --- a/client/src/scripts/notFound.js +++ b/client/src/scripts/notFound.js @@ -1,13 +1,7 @@ import { injectNavbar } from '../modules/Navbar.js'; -import { io } from 'socket.io-client'; -import { toast } from '../modules/Toast'; const notFound = () => { injectNavbar(); - const socket = io(); - socket.on('broadcast', (message) => { - toast(message, 'warning', true, false); - }); }; if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { diff --git a/index.js b/index.js index 76b523b..a473d39 100644 --- a/index.js +++ b/index.js @@ -3,16 +3,12 @@ const express = require('express'); const path = require('path'); const app = express(); -const bodyParser = require('body-parser'); const GameManager = require('./server/modules/GameManager.js'); const SocketManager = require('./server/modules/SocketManager.js'); const globals = require('./server/config/globals'); const ServerBootstrapper = require('./server/modules/ServerBootstrapper'); -app.use(bodyParser.json()); -app.use(bodyParser.urlencoded({ - extended: true -})); +app.use(express.json()); const args = ServerBootstrapper.processCLIArgs(); diff --git a/server/api/AdminAPI.js b/server/api/AdminAPI.js index 9f2e08b..1d4a786 100644 --- a/server/api/AdminAPI.js +++ b/server/api/AdminAPI.js @@ -26,6 +26,7 @@ if (process.env.NODE_ENV.trim() === 'production') { router.use(cors(globals.CORS)); router.use((req, res, next) => { + req.accepts() if (isAuthorized(req)) { next(); } else { @@ -33,6 +34,14 @@ router.use((req, res, next) => { } }); +router.post('/sockets/broadcast', (req, res, next) => { + globals.CONTENT_TYPE_VALIDATOR(req, res, next); +}); +router.put('/games/state', (req, res, next) => { + globals.CONTENT_TYPE_VALIDATOR(req, res, next); +}); + +// TODO: implement client-side display of this message. router.post('/sockets/broadcast', function (req, res) { logger.info('admin user broadcasting message: ' + req.body?.message); socketManager.broadcast(req.body?.message); @@ -44,7 +53,7 @@ router.get('/games/state', function (req, res) { }); router.put('/games/state', function (req, res) { - // TODO: validate the request body - can break the application if malformed. + // TODO: validate the JSON object sent - ones that don't match the expected model could break the application. gameManager.activeGameRunner.activeGames = req.body; res.status(201).send(gameManager.activeGameRunner.activeGames); }); diff --git a/server/api/GamesAPI.js b/server/api/GamesAPI.js index 834b523..4fa21bf 100644 --- a/server/api/GamesAPI.js +++ b/server/api/GamesAPI.js @@ -28,6 +28,16 @@ router.options('/:code/players', cors(globals.CORS)); router.options('/create', cors(globals.CORS)); router.options('/restart', cors(globals.CORS)); +router.post('/create', (req, res, next) => { + globals.CONTENT_TYPE_VALIDATOR(req, res, next); +}); +router.patch('/players', (req, res, next) => { + globals.CONTENT_TYPE_VALIDATOR(req, res, next); +}); +router.patch('/restart', (req, res, next) => { + globals.CONTENT_TYPE_VALIDATOR(req, res, next); +}); + if (process.env.NODE_ENV.trim() === 'production') { router.use(apiLimiter); router.use('/create', gameEndpointLimiter); diff --git a/server/config/globals.js b/server/config/globals.js index 47442dd..14a0535 100644 --- a/server/config/globals.js +++ b/server/config/globals.js @@ -12,6 +12,14 @@ const globals = { origin: 'https://play-werewolf.app', optionsSuccessStatus: 200 }, + CONTENT_TYPE_VALIDATOR: (req, res, next) => { + req.accepts() + if (req.is('application/json')) { + next(); + } else { + res.status(400).send('Request has invalid content type.'); + } + }, STALE_GAME_HOURS: 12, CLIENT_COMMANDS: { FETCH_GAME_STATE: 'fetchGameState', From 8e455f258adb1a8c0cd3c521bcddedfbd49e7a8b Mon Sep 17 00:00:00 2001 From: AlecM33 Date: Tue, 5 Jul 2022 19:18:16 -0400 Subject: [PATCH 3/4] remove socket listener --- client/src/scripts/game.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/client/src/scripts/game.js b/client/src/scripts/game.js index 3db7206..995b34c 100644 --- a/client/src/scripts/game.js +++ b/client/src/scripts/game.js @@ -34,9 +34,6 @@ const game = () => { UserUtility.validateAnonUserSignature(res.content) ); }); - socket.on('broadcast', (message) => { - toast(message, 'warning', true, false); - }); socket.on('connect_error', (err) => { toast(err, 'error', true, false); }); From 072c49c47acfeddefcf23ec6de377d175a4a432a Mon Sep 17 00:00:00 2001 From: AlecM33 Date: Tue, 5 Jul 2022 19:28:56 -0400 Subject: [PATCH 4/4] add brightness affect to active card element --- client/src/scripts/create.js | 1 - client/src/styles/game.css | 4 ++++ server/api/AdminAPI.js | 2 +- server/config/globals.js | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/client/src/scripts/create.js b/client/src/scripts/create.js index 5045c75..c60d852 100644 --- a/client/src/scripts/create.js +++ b/client/src/scripts/create.js @@ -2,7 +2,6 @@ import { DeckStateManager } from '../modules/DeckStateManager.js'; import { GameCreationStepManager } from '../modules/GameCreationStepManager.js'; import { injectNavbar } from '../modules/Navbar.js'; import createTemplate from '../view_templates/CreateTemplate.js'; -import { toast } from '../modules/Toast'; const create = () => { injectNavbar(); diff --git a/client/src/styles/game.css b/client/src/styles/game.css index ab35553..4d6708c 100644 --- a/client/src/styles/game.css +++ b/client/src/styles/game.css @@ -271,6 +271,10 @@ h1 { -moz-user-select: none; } +#game-role:active, #game-role-back:active { + filter: brightness(0.85); +} + .game-role-good { border: 5px solid #5469c5 !important; } diff --git a/server/api/AdminAPI.js b/server/api/AdminAPI.js index 1d4a786..5e2e89b 100644 --- a/server/api/AdminAPI.js +++ b/server/api/AdminAPI.js @@ -26,7 +26,7 @@ if (process.env.NODE_ENV.trim() === 'production') { router.use(cors(globals.CORS)); router.use((req, res, next) => { - req.accepts() + req.accepts(); if (isAuthorized(req)) { next(); } else { diff --git a/server/config/globals.js b/server/config/globals.js index 14a0535..b686fcb 100644 --- a/server/config/globals.js +++ b/server/config/globals.js @@ -13,7 +13,7 @@ const globals = { optionsSuccessStatus: 200 }, CONTENT_TYPE_VALIDATOR: (req, res, next) => { - req.accepts() + req.accepts(); if (req.is('application/json')) { next(); } else {