mirror of
https://github.com/AlecM33/Werewolf.git
synced 2026-02-10 04:03:33 +01:00
* Initial plan * Refactor timer system to run on main thread instead of child processes Co-authored-by: AlecM33 <24642328+AlecM33@users.noreply.github.com> * Remove obsolete GameProcess.js and simplify TimerManager to stub Co-authored-by: AlecM33 <24642328+AlecM33@users.noreply.github.com> * Address code review feedback: add comments and reduce duplication Co-authored-by: AlecM33 <24642328+AlecM33@users.noreply.github.com> * Remove comments, delete TimerManager, and simplify SOURCE_TIMER_EVENT Co-authored-by: AlecM33 <24642328+AlecM33@users.noreply.github.com> * Fix cross-instance timer communication and expired timer handling - GET_TIME_REMAINING now publishes response through Redis for cross-instance communication - SOURCE_TIMER_EVENT handles GET_TIME_REMAINING even when timer is not running locally - Returns 0 time remaining when timer has expired or timerParams is null - Updated test to expect Redis publish instead of direct method call Co-authored-by: AlecM33 <24642328+AlecM33@users.noreply.github.com> * Simplify GET_TIME_REMAINING flow and add ended flag - Add ended: true flag to timerParams when timer completes - Simplify TIMER_EVENT to check ended flag first, then local timer, then publish SOURCE_TIMER_EVENT - SOURCE_TIMER_EVENT only publishes GET_TIME_REMAINING if it has the timer - Remove GET_TIME_REMAINING from handleTimerCommand helper - Direct socket emission for local timers instead of Redis publish - Updated test to verify socket emission Co-authored-by: AlecM33 <24642328+AlecM33@users.noreply.github.com> * Refactor GET_TIME_REMAINING into handleTimerCommand switch case - Moved GET_TIME_REMAINING logic from TIMER_EVENT handler into handleTimerCommand function - Simplified TIMER_EVENT to just call handleTimerCommand for all timer operations - Maintains same flow: check ended flag, check local timer, publish SOURCE_TIMER_EVENT if needed - All tests passing Co-authored-by: AlecM33 <24642328+AlecM33@users.noreply.github.com> * Remove unused getTimeRemaining function from GameManager - Function is no longer called after refactoring GET_TIME_REMAINING into handleTimerCommand - All timer time remaining logic now handled in Events.js handleTimerCommand switch case - Tests still passing Co-authored-by: AlecM33 <24642328+AlecM33@users.noreply.github.com> * Reset timerParams.ended flag when restarting game - Added reset of ended flag to false in restartGame function - Ensures restarted games don't have timer marked as already ended - Placed right after timer cleanup for logical grouping Co-authored-by: AlecM33 <24642328+AlecM33@users.noreply.github.com> * remuse unused constant --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: AlecM33 <24642328+AlecM33@users.noreply.github.com> Co-authored-by: AlecM33 <leohfx@gmail.com>
175 lines
7.0 KiB
JavaScript
175 lines
7.0 KiB
JavaScript
const { LOG_LEVEL, ENVIRONMENTS } = require('../config/globals');
|
|
const http = require('http');
|
|
const https = require('https');
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
const crypto = require('crypto');
|
|
const EventManager = require('./singletons/EventManager.js');
|
|
const GameManager = require('./singletons/GameManager.js');
|
|
const rateLimit = require('express-rate-limit').default;
|
|
|
|
const ServerBootstrapper = {
|
|
|
|
singletons: (logger, instanceId) => {
|
|
return {
|
|
eventManager: new EventManager(logger, instanceId),
|
|
gameManager: process.env.NODE_ENV.trim() === 'development'
|
|
? new GameManager(logger, ENVIRONMENTS.LOCAL, instanceId)
|
|
: new GameManager(logger, ENVIRONMENTS.PRODUCTION, instanceId)
|
|
};
|
|
},
|
|
|
|
injectDependencies: (singletons) => {
|
|
const gameManager = require('./singletons/GameManager').instance;
|
|
const eventManager = require('./singletons/EventManager').instance;
|
|
singletons.gameManager.eventManager = eventManager;
|
|
singletons.eventManager.gameManager = gameManager;
|
|
},
|
|
|
|
processCLIArgs: () => {
|
|
try {
|
|
const args = Array.from(process.argv.map((arg) => arg.trim().toLowerCase()));
|
|
const useHttps = args.includes('protocol=https');
|
|
const port = process.env.WEB_PORT || args
|
|
.filter((arg) => {
|
|
return /port=\d+/.test(arg);
|
|
})
|
|
.map((arg) => {
|
|
return /port=(\d+)/.exec(arg)[1];
|
|
})[0] || 5000;
|
|
const logLevel = args
|
|
.filter((arg) => {
|
|
return /loglevel=[a-zA-Z]+/.test(arg);
|
|
})
|
|
.map((arg) => {
|
|
return /loglevel=([a-zA-Z]+)/.exec(arg)[1];
|
|
})[0] || LOG_LEVEL.INFO;
|
|
|
|
return {
|
|
useHttps: useHttps,
|
|
port: port,
|
|
logLevel: logLevel
|
|
};
|
|
} catch (e) {
|
|
throw new Error('Your server run command is malformed. Consult the codebase wiki for proper usage. Error: ' + e);
|
|
}
|
|
},
|
|
|
|
createServerWithCorrectHTTPProtocol: (app, useHttps, port, logger) => {
|
|
let main;
|
|
if (process.env.NODE_ENV.trim() === 'development') {
|
|
logger.info('starting main in DEVELOPMENT mode.');
|
|
if (
|
|
useHttps
|
|
&& fs.existsSync(path.join(__dirname, '../../client/certs/localhost-key.pem'))
|
|
&& fs.existsSync(path.join(__dirname, '../../client/certs/localhost.pem'))
|
|
) {
|
|
const key = fs.readFileSync(path.join(__dirname, '../../client/certs/localhost-key.pem'), 'utf-8');
|
|
const cert = fs.readFileSync(path.join(__dirname, '../../client/certs/localhost.pem'), 'utf-8');
|
|
logger.info('local certs detected. Using HTTPS.');
|
|
main = https.createServer({ key, cert }, app);
|
|
logger.info(`navigate to https://localhost:${port}`);
|
|
} else {
|
|
logger.info('https not specified or no local certs detected. Certs should reside in /client/certs. Using HTTP.');
|
|
main = http.createServer(app);
|
|
logger.info(`navigate to http://localhost:${port}`);
|
|
}
|
|
} else {
|
|
logger.warn('starting main in PRODUCTION mode. This should not be used for local development.');
|
|
main = http.createServer(app);
|
|
app.use(function (req, res, next) {
|
|
const schema = (req.headers['x-forwarded-proto'] || '').toLowerCase();
|
|
if (!req.path.includes('/_ah/start') && req.headers.host.indexOf('localhost') < 0 && schema !== 'https') {
|
|
res.redirect('https://' + req.headers.host + req.url);
|
|
} else {
|
|
next();
|
|
}
|
|
});
|
|
app.use(function (req, res, next) {
|
|
const nonce = crypto.randomBytes(16).toString('base64');
|
|
res.setHeader(
|
|
'Content-Security-Policy',
|
|
"default-src 'self'; font-src 'self' https://fonts.gstatic.com/; img-src 'self' https://img.buymeacoffee.com;" +
|
|
" script-src 'self'; style-src 'self' https://fonts.googleapis.com/ 'nonce-" + nonce + "'; frame-src 'self'"
|
|
);
|
|
next();
|
|
});
|
|
}
|
|
|
|
return main;
|
|
},
|
|
|
|
establishRouting: (app, express) => {
|
|
const standardRateLimit = rateLimit({
|
|
windowMs: 60000,
|
|
max: 100,
|
|
standardHeaders: true,
|
|
legacyHeaders: false
|
|
});
|
|
|
|
// API endpoints
|
|
app.use('/api/games', standardRateLimit, require('../api/GamesAPI'));
|
|
app.use('/api/admin', (req, res, next) => {
|
|
if (isAuthorized(req)) {
|
|
next();
|
|
} else {
|
|
res.status(401).send('You are not authorized to make this request.');
|
|
}
|
|
}, standardRateLimit, require('../api/AdminAPI'));
|
|
|
|
// miscellaneous assets
|
|
app.use('/manifest.json', standardRateLimit, (req, res) => {
|
|
res.sendFile(path.join(__dirname, '../../manifest.json'));
|
|
});
|
|
|
|
app.use('/favicon.ico', standardRateLimit, (req, res) => {
|
|
res.sendFile(path.join(__dirname, '../../client/favicon_package/favicon.ico'));
|
|
});
|
|
|
|
app.use('/apple-touch-icon.png', standardRateLimit, (req, res) => {
|
|
res.sendFile(path.join(__dirname, '../../client/favicon_package/apple-touch-icon.png'));
|
|
});
|
|
|
|
const router = require('../routes/router');
|
|
app.use('', router);
|
|
|
|
app.use('/dist', (req, res, next) => {
|
|
if (req.url.includes('.js.gz')) {
|
|
res.set('Content-Encoding', 'gzip');
|
|
}
|
|
next();
|
|
});
|
|
|
|
app.use('/dist', express.static(path.join(__dirname, '../../client/dist')));
|
|
|
|
// set up routing for static content that isn't being bundled.
|
|
app.use('/images', express.static(path.join(__dirname, '../../client/src/images')));
|
|
app.use('/styles', express.static(path.join(__dirname, '../../client/src/styles')));
|
|
app.use('/webfonts', express.static(path.join(__dirname, '../../client/src/webfonts')));
|
|
app.use('/robots.txt', standardRateLimit, (req, res) => {
|
|
res.sendFile(path.join(__dirname, '../../client/robots.txt'));
|
|
});
|
|
|
|
app.use(standardRateLimit, function (req, res) {
|
|
res.sendFile(path.join(__dirname, '../../client/src/views/404.html'));
|
|
});
|
|
}
|
|
};
|
|
|
|
function isAuthorized (req) {
|
|
if (process.env.NODE_ENV.trim() === 'development' || req.method === 'OPTIONS') {
|
|
return true;
|
|
}
|
|
|
|
const header = req.headers.authorization;
|
|
if (header) {
|
|
const token = header.split(/\s+/).pop() || '';
|
|
const decodedToken = Buffer.from(token, 'base64').toString();
|
|
return decodedToken.trim() === process.env.ADMIN_KEY?.trim();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
module.exports = ServerBootstrapper;
|