diff --git a/app.js b/app.js index 1e60871..79fd437 100644 --- a/app.js +++ b/app.js @@ -3,6 +3,7 @@ const rateLimit = require('express-rate-limit'); const path = require('path'); const bodyParser = require('body-parser'); const API = require('./src/js/index.js'); +const demoTracker = require('./src/js/demoTracker.js'); const { logger } = require('./src/js/logger.js'); const favicon = require('serve-favicon'); const app = express(); @@ -27,156 +28,16 @@ app.use( }) ); // app.use(express.raw({ type: 'application/json', limit: '10mb' })); +app.use(demoTracker.demoModeMiddleware); + +// Set up demo mode cleanup interval +setInterval( + () => demoTracker.demoModeRequestTracker.cleanupExpiredEntries(), + 3600000 +); // Clean up every hour const fs = require('fs'); -let DEFAULT_SSO_TOKEN = ''; - -// Try to read the token from a file -try { - const tokenPath = path.join(__dirname, 'token.txt'); - if (fs.existsSync(tokenPath)) { - DEFAULT_SSO_TOKEN = fs.readFileSync(tokenPath, 'utf8').trim(); - logger.info('Default SSO token loaded successfully from token.txt'); - } else { - logger.warn('token.txt not found, demo mode may not function correctly'); - } -} catch (error) { - logger.error('Error loading token file:', { error: error.message }); -} - -setInterval(() => demoModeRequestTracker.cleanupExpiredEntries(), 3600000); // Clean up every hour - -// Configure rate limiting for demo mode -const demoModeRequestTracker = { - requests: new Map(), - maxRequestsPerHour: 20, // Adjust as needed - resetInterval: 60 * 60 * 1000, // 1 hour in milliseconds - - isLimited: function (ip) { - const now = Date.now(); - const userRequests = this.requests.get(ip) || { - count: 0, - resetTime: now + this.resetInterval, - }; - - // Reset counter if the reset time has passed - if (now > userRequests.resetTime) { - userRequests.count = 0; - userRequests.resetTime = now + this.resetInterval; - } - - return userRequests.count >= this.maxRequestsPerHour; - }, - - incrementCounter: function (ip) { - const now = Date.now(); - const userRequests = this.requests.get(ip) || { - count: 0, - resetTime: now + this.resetInterval, - }; - - // Reset counter if the reset time has passed - if (now > userRequests.resetTime) { - userRequests.count = 1; // Start with 1 for this request - userRequests.resetTime = now + this.resetInterval; - } else { - userRequests.count++; - } - - this.requests.set(ip, userRequests); - return userRequests.count; - }, - - cleanupExpiredEntries: function () { - const now = Date.now(); - for (const [ip, data] of this.requests.entries()) { - if (now > data.resetTime) { - // Either remove entries completely or reset them to 0 - this.requests.delete(ip); - } - } - }, -}; - -// Demo mode middleware -const demoModeMiddleware = (req, res, next) => { - // Skip non-API routes - if (!req.path.startsWith('/api/')) { - return next(); - } - - // Get client IP for tracking - // const clientIP = req.headers['x-forwarded-for'] || req.ip || req.connection.remoteAddress; - const clientIP = - req.headers['cf-connecting-ip'] || - req.headers['x-forwarded-for']?.split(',')[0] || - req.ip || - req.connection.remoteAddress; - - // Check if demo mode is active - if (DEFAULT_SSO_TOKEN) { - // For API endpoints, check rate limit in demo mode - if (demoModeRequestTracker.isLimited(clientIP)) { - const userRequests = demoModeRequestTracker.requests.get(clientIP); - const resetTimeMs = userRequests.resetTime - Date.now(); - - // Format time as HH:MM:SS - const hours = Math.floor(resetTimeMs / (60 * 60 * 1000)); - const minutes = Math.floor( - (resetTimeMs % (60 * 60 * 1000)) / (60 * 1000) - ); - const seconds = Math.floor((resetTimeMs % (60 * 1000)) / 1000); - const timeFormat = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; - - logger.warn(`Demo mode rate limit exceeded for IP: ${clientIP}`); - return res.status(429).json({ - status: 'error', - message: `Please try again in ${timeFormat} or use your own SSO token.`, - timestamp: global.Utils.toIsoString(new Date()), - }); - } - - // Increment the counter - // const count = demoModeRequestTracker.incrementCounter(clientIP); - // logger.debug(`Demo mode request count for ${clientIP}: ${count}`); - } - - next(); -}; - -function incrementDemoCounter(req, ssoToken) { - if (ssoToken === DEFAULT_SSO_TOKEN) { - const clientIP = - req.headers['cf-connecting-ip'] || - req.headers['x-forwarded-for']?.split(',')[0] || - req.ip || - req.connection.remoteAddress; - - const count = demoModeRequestTracker.incrementCounter(clientIP); - logger.debug(`Demo mode request count for ${clientIP}: ${count}`); - } -} - -app.use(demoModeMiddleware); -app.use((req, res, next) => { - if (req.query.demo === 'true' || req.query.demo === '1') { - // Enable demo mode for this session if it's not already enabled - if (!req.session) { - req.session = {}; - } - req.session.forceDemoMode = true; - logger.info('Demo mode enabled via URL parameter'); - } else if (req.query.demo === 'false' || req.query.demo === '0') { - // Disable demo mode for this session - if (req.session) { - req.session.forceDemoMode = false; - } - logger.info('Demo mode disabled via URL parameter'); - } - next(); -}); - // Initialize key replacements let keyReplacements = {}; @@ -375,13 +236,14 @@ app.post('/api/stats', async (req, res) => { let { username, ssoToken, platform, game, apiCall, sanitize, replaceKeys } = req.body; - if (!ssoToken && DEFAULT_SSO_TOKEN) { - ssoToken = DEFAULT_SSO_TOKEN; + const defaultToken = demoTracker.getDefaultSsoToken(); + if (!ssoToken && defaultToken) { + ssoToken = defaultToken; logger.info('Using default SSO token for demo mode'); - } else if (!ssoToken && !DEFAULT_SSO_TOKEN) { + } else if (!ssoToken) { return res.status(200).json({ status: 'error', - message: 'SSO Token is required as demo mode is not active', + message: 'SSO Token is required', timestamp: global.Utils.toIsoString(new Date()), }); } @@ -399,6 +261,11 @@ app.post('/api/stats', async (req, res) => { logger.debug(`Processing Options - Sanitize: ${sanitize}, Replace Keys: ${replaceKeys}`); logger.debug("====================="); */ + /* + if (!ssoToken) { + return res.status(400).json({ error: 'SSO Token is required' }); + } */ + // For mapList, username is not required if (apiCall !== 'mapList' && !username) { return res.status(400).json({ error: 'Username is required' }); @@ -583,7 +450,7 @@ app.post('/api/stats', async (req, res) => { const { sanitize, replaceKeys } = req.body; - incrementDemoCounter(req, ssoToken); + demoTracker.incrementDemoCounter(req, ssoToken); return res.json({ // status: "success", @@ -644,13 +511,20 @@ app.post('/api/matches', async (req, res) => { return res.status(400).json({ error: 'Username is required' }); } - if (!ssoToken && DEFAULT_SSO_TOKEN) { - ssoToken = DEFAULT_SSO_TOKEN; + if (!username || !ssoToken) { + return res + .status(400) + .json({ error: 'Username and SSO Token are required' }); + } + + const defaultToken = demoTracker.getDefaultSsoToken(); + if (!ssoToken && defaultToken) { + ssoToken = defaultToken; logger.info('Using default SSO token for demo mode'); - } else if (!ssoToken && !DEFAULT_SSO_TOKEN) { + } else if (!ssoToken) { return res.status(200).json({ status: 'error', - message: 'SSO Token is required as demo mode is not active', + message: 'SSO Token is required', timestamp: global.Utils.toIsoString(new Date()), }); } @@ -743,7 +617,7 @@ app.post('/api/matches', async (req, res) => { const { sanitize, replaceKeys } = req.body; - incrementDemoCounter(req, ssoToken); + demoTracker.incrementDemoCounter(req, ssoToken); return res.json({ // status: "success", @@ -800,13 +674,20 @@ app.post('/api/matchInfo', async (req, res) => { return res.status(400).json({ error: 'Match ID is required' }); } - if (!ssoToken && DEFAULT_SSO_TOKEN) { - ssoToken = DEFAULT_SSO_TOKEN; + if (!matchId || !ssoToken) { + return res + .status(400) + .json({ error: 'Match ID and SSO Token are required' }); + } + + const defaultToken = demoTracker.getDefaultSsoToken(); + if (!ssoToken && defaultToken) { + ssoToken = defaultToken; logger.info('Using default SSO token for demo mode'); - } else if (!ssoToken && !DEFAULT_SSO_TOKEN) { + } else if (!ssoToken) { return res.status(200).json({ status: 'error', - message: 'SSO Token is required as demo mode is not active', + message: 'SSO Token is required', timestamp: global.Utils.toIsoString(new Date()), }); } @@ -885,7 +766,7 @@ app.post('/api/matchInfo', async (req, res) => { const { sanitize, replaceKeys } = req.body; - incrementDemoCounter(req, ssoToken); + demoTracker.incrementDemoCounter(req, ssoToken); return res.json({ // status: "success", @@ -939,6 +820,11 @@ app.post('/api/user', async (req, res) => { logger.debug(`Processing Options - Sanitize: ${sanitize}, Replace Keys: ${replaceKeys}`); logger.debug("========================="); */ + /* + if (!ssoToken) { + return res.status(400).json({ error: 'SSO Token is required' }); + } */ + // For eventFeed and identities, username is not required if ( !username && @@ -951,13 +837,14 @@ app.post('/api/user', async (req, res) => { .json({ error: 'Username is required for this API call' }); } - if (!ssoToken && DEFAULT_SSO_TOKEN) { - ssoToken = DEFAULT_SSO_TOKEN; + const defaultToken = demoTracker.getDefaultSsoToken(); + if (!ssoToken && defaultToken) { + ssoToken = defaultToken; logger.info('Using default SSO token for demo mode'); - } else if (!ssoToken && !DEFAULT_SSO_TOKEN) { + } else if (!ssoToken) { return res.status(200).json({ status: 'error', - message: 'SSO Token is required as demo mode is not active', + message: 'SSO Token is required', timestamp: global.Utils.toIsoString(new Date()), }); } @@ -1027,7 +914,7 @@ app.post('/api/user', async (req, res) => { const { sanitize, replaceKeys } = req.body; - incrementDemoCounter(req, ssoToken); + demoTracker.incrementDemoCounter(req, ssoToken); return res.json({ // status: "success", @@ -1083,13 +970,20 @@ app.post('/api/search', async (req, res) => { return res.status(400).json({ error: 'Username is required' }); } - if (!ssoToken && DEFAULT_SSO_TOKEN) { - ssoToken = DEFAULT_SSO_TOKEN; + if (!username || !ssoToken) { + return res + .status(400) + .json({ error: 'Username and SSO Token are required' }); + } + + const defaultToken = demoTracker.getDefaultSsoToken(); + if (!ssoToken && defaultToken) { + ssoToken = defaultToken; logger.info('Using default SSO token for demo mode'); - } else if (!ssoToken && !DEFAULT_SSO_TOKEN) { + } else if (!ssoToken) { return res.status(200).json({ status: 'error', - message: 'SSO Token is required as demo mode is not active', + message: 'SSO Token is required', timestamp: global.Utils.toIsoString(new Date()), }); } @@ -1124,7 +1018,7 @@ app.post('/api/search', async (req, res) => { const { sanitize, replaceKeys } = req.body; - incrementDemoCounter(req, ssoToken); + demoTracker.incrementDemoCounter(req, ssoToken); return res.json({ // status: "success", @@ -1228,22 +1122,21 @@ app.get('/health', (req, res) => { req.headers['x-forwarded-for']?.split(',')[0] || req.ip || req.connection.remoteAddress; - // Check if demo mode is forced via URL parameter - const forceDemoMode = req.session && req.session.forceDemoMode === true; - const isDemoMode = !!DEFAULT_SSO_TOKEN || forceDemoMode; + const isDemoMode = demoTracker.isDemoModeActive(); res.json({ status: 'ok', timestamp: global.Utils.toIsoString(new Date()), demoMode: isDemoMode, requestsRemaining: isDemoMode ? - demoModeRequestTracker.maxRequestsPerHour - - (demoModeRequestTracker.requests.get(clientIP)?.count || 0) + demoTracker.demoModeRequestTracker.maxRequestsPerHour - + (demoTracker.demoModeRequestTracker.requests.get(clientIP)?.count || 0) : null, }); }); +/* app.get('/demo', (req, res) => { // Set a cookie or session variable to enable demo mode if (!req.session) { @@ -1253,10 +1146,38 @@ app.get('/demo', (req, res) => { // Redirect to the main app res.redirect('/'); +}); */ + +// Demo mode route - enables demo mode when visited +app.get('/demo', async (req, res) => { + const success = await demoTracker.initializeDemoMode(true); + + if (success) { + logger.info('Demo mode activated via /demo route'); + res.redirect('/?demo=true'); + } else { + res + .status(500) + .send('Failed to enable demo mode. Check server logs for details.'); + } +}); + +// Demo mode deactivation route +app.get('/demo/disable', async (req, res) => { + await demoTracker.initializeDemoMode(false); + logger.info('Demo mode deactivated via /demo/disable route'); + res.redirect('/?demo=false'); }); // Serve the main HTML file -app.get('/', (req, res) => { +app.get('/', async (req, res) => { + // Check demo mode query parameter + if (req.query.demo === 'true') { + await demoTracker.initializeDemoMode(true); + } else if (req.query.demo === 'false') { + await demoTracker.initializeDemoMode(false); + } + res.sendFile(path.join(__dirname, 'src', 'index.html')); }); diff --git a/src/js/backend-min.js b/src/js/backend-min.js deleted file mode 100644 index 285e576..0000000 --- a/src/js/backend-min.js +++ /dev/null @@ -1,381 +0,0 @@ -const a0_0x3e5ceb = a0_0x1776; -function a0_0x424e() { - const _0x308173 = [ - '8HLbzTb', - 'number', - 'replace', - 'endTime', - 'timePlayed', - 'uiAPI', - 'GMT', - 'forEach', - '[CLIENT]\x20Request\x20data:\x20', - 'An\x20error\x20occurred', - '[CLIENT]\x20Response\x20received\x20at\x20', - '\x20Hours\x20', - 'includes', - '\x20at\x20', - 'getElementById', - 'dateAdded', - 'toISOString', - 'textContent', - 'Request\x20timed\x20out.\x20Please\x20try\x20again.', - 'string', - 'Fetch\x20error:', - 'length', - '828318JvPrOY', - 'time', - 'replaceKeysOption', - 'toString', - 'substring', - 'json', - 'An\x20error\x20occurred\x20while\x20fetching\x20data.', - 'keys', - 'entries', - '274851lFAePH', - 'displayError', - 'toUTCString', - 'checked', - '5mFBRcl', - '1809891DxJKoQ', - 'DOMContentLoaded', - '7128aSefdV', - 'Server\x20returned\x20non-JSON\x20response', - 'none', - 'log', - 'stringify', - 'currentData', - 'object', - 'displayResults', - 'appState', - 'headers', - 'null', - 'signal', - '\x20Days\x20', - 'error', - 'style', - 'test', - '\x20Minutes\x20', - 'utcStartSeconds', - 'tutorialDismissed', - 'isArray', - 'trimStart', - 'ssoToken', - 'querySelectorAll', - '70rCMOkH', - 'addEventListener', - '1218049tFLRcR', - '.tutorial', - 'repeat', - '16176Hmtcab', - 'message', - 'loading', - '[CLIENT]\x20Response\x20status:\x20', - 'map', - 'POST', - 'floor', - '70193EjRYyO', - 'name', - 'objTime', - 'display', - 'date', - 'get', - 'Error:\x20', - 'content-type', - 'application/json', - 'status', - 'UTC', - 'boolean', - '2031464mgplJK', - '8ohsaRV', - ]; - a0_0x424e = function () { - return _0x308173; - }; - return a0_0x424e(); -} -(function (_0x26b91f, _0x29f7ca) { - const _0x4906c0 = a0_0x1776, - _0x243b93 = _0x26b91f(); - while (!![]) { - try { - const _0x1e80bd = - (parseInt(_0x4906c0(0x11e)) / 0x1) * - (-parseInt(_0x4906c0(0x12b)) / 0x2) + - -parseInt(_0x4906c0(0x150)) / 0x3 + - parseInt(_0x4906c0(0x12a)) / 0x4 + - (-parseInt(_0x4906c0(0x14f)) / 0x5) * - (-parseInt(_0x4906c0(0x142)) / 0x6) + - (parseInt(_0x4906c0(0x114)) / 0x7) * - (parseInt(_0x4906c0(0x12c)) / 0x8) + - (parseInt(_0x4906c0(0x14b)) / 0x9) * - (-parseInt(_0x4906c0(0x112)) / 0xa) + - (-parseInt(_0x4906c0(0x152)) / 0xb) * - (-parseInt(_0x4906c0(0x117)) / 0xc); - if (_0x1e80bd === _0x29f7ca) break; - else _0x243b93['push'](_0x243b93['shift']()); - } catch (_0x14bf47) { - _0x243b93['push'](_0x243b93['shift']()); - } - } -})(a0_0x424e, 0x91684), - (window['backendAPI'] = { - fetchData: fetchData, - jsonToYAML: jsonToYAML, - formatDuration: formatDuration, - formatEpochTime: formatEpochTime, - processTimestamps: processTimestamps, - }), - (window['appState'] = { - currentData: null, - outputFormat: a0_0x3e5ceb(0x147), - tutorialDismissed: ![], - }), - document[a0_0x3e5ceb(0x113)](a0_0x3e5ceb(0x151), function () {}); -function jsonToYAML(_0x57e066) { - const _0x4e69bf = a0_0x3e5ceb, - _0x1e3ebf = 0x2; - function _0x58e6eb(_0x4d120a, _0x248fd6 = 0x0) { - const _0x3a284a = a0_0x1776, - _0x52d5f6 = '\x20'[_0x3a284a(0x116)](_0x248fd6); - if (_0x4d120a === null) return _0x3a284a(0x105); - if (_0x4d120a === undefined) return ''; - if (typeof _0x4d120a === _0x3a284a(0x13f)) { - if ( - /[:{}[\],&*#?|\-<>=!%@`]/[_0x3a284a(0x10a)](_0x4d120a) || - _0x4d120a === '' || - !isNaN(_0x4d120a) - ) - return '\x22' + _0x4d120a[_0x3a284a(0x12e)](/"/g, '\x5c\x22') + '\x22'; - return _0x4d120a; - } - if ( - typeof _0x4d120a === _0x3a284a(0x12d) || - typeof _0x4d120a === _0x3a284a(0x129) - ) - return _0x4d120a[_0x3a284a(0x145)](); - if (Array[_0x3a284a(0x10e)](_0x4d120a)) { - if (_0x4d120a[_0x3a284a(0x141)] === 0x0) return '[]'; - let _0x53132f = ''; - for (const _0x194c43 of _0x4d120a) { - _0x53132f += - '\x0a' + - _0x52d5f6 + - '-\x20' + - _0x58e6eb(_0x194c43, _0x248fd6 + _0x1e3ebf)[_0x3a284a(0x10f)](); - } - return _0x53132f; - } - if (typeof _0x4d120a === 'object') { - if (Object[_0x3a284a(0x149)](_0x4d120a)[_0x3a284a(0x141)] === 0x0) - return '{}'; - let _0x42a784 = ''; - for (const [_0x1a8665, _0x32613d] of Object[_0x3a284a(0x14a)]( - _0x4d120a - )) { - const _0x3a78cc = _0x58e6eb(_0x32613d, _0x248fd6 + _0x1e3ebf); - _0x3a78cc[_0x3a284a(0x138)]('\x0a') ? - (_0x42a784 += '\x0a' + _0x52d5f6 + _0x1a8665 + ':' + _0x3a78cc) - : (_0x42a784 += '\x0a' + _0x52d5f6 + _0x1a8665 + ':\x20' + _0x3a78cc); - } - return _0x42a784; - } - return String(_0x4d120a); - } - return _0x58e6eb(_0x57e066, 0x0)[_0x4e69bf(0x146)](0x1); -} -async function fetchData(_0x5d7e74, _0x3de528) { - const _0x5f0540 = a0_0x3e5ceb; - console['log']( - '[CLIENT]\x20Request\x20to\x20' + - _0x5d7e74 + - _0x5f0540(0x139) + - new Date()[_0x5f0540(0x13c)]() - ), - console['log']( - _0x5f0540(0x134) + - JSON[_0x5f0540(0x156)]({ - ..._0x3de528, - ssoToken: - _0x3de528[_0x5f0540(0x110)] ? - _0x3de528['ssoToken'][_0x5f0540(0x146)](0x0, 0x5) + '...' - : _0x5f0540(0x154), - }) - ); - const _0x4d0c9e = document['getElementById']('error'), - _0x4a22b8 = document['getElementById'](_0x5f0540(0x119)), - _0x2f0994 = document[_0x5f0540(0x13a)]('results'); - (_0x4d0c9e[_0x5f0540(0x13d)] = ''), - (_0x2f0994[_0x5f0540(0x109)][_0x5f0540(0x121)] = _0x5f0540(0x154)), - (_0x4a22b8[_0x5f0540(0x109)][_0x5f0540(0x121)] = 'block'); - !window[_0x5f0540(0x103)][_0x5f0540(0x10d)] && - ((window[_0x5f0540(0x103)][_0x5f0540(0x10d)] = !![]), - document[_0x5f0540(0x111)](_0x5f0540(0x115))[_0x5f0540(0x133)]( - (_0x520561) => { - const _0x511e86 = _0x5f0540; - _0x520561[_0x511e86(0x109)][_0x511e86(0x121)] = _0x511e86(0x154); - } - )); - if (!_0x3de528[_0x5f0540(0x110)]) { - window[_0x5f0540(0x131)][_0x5f0540(0x14c)]( - 'SSO\x20Token\x20is\x20required' - ), - (_0x4a22b8[_0x5f0540(0x109)][_0x5f0540(0x121)] = _0x5f0540(0x154)); - return; - } - try { - const _0x4c0dfe = new AbortController(), - _0x5e0646 = setTimeout(() => _0x4c0dfe['abort'](), 0x7530), - _0x7ef5a8 = await fetch(_0x5d7e74, { - method: _0x5f0540(0x11c), - headers: { 'Content-Type': _0x5f0540(0x126) }, - body: JSON[_0x5f0540(0x156)](_0x3de528), - signal: _0x4c0dfe[_0x5f0540(0x106)], - }); - clearTimeout(_0x5e0646); - const _0x64ad7 = _0x7ef5a8[_0x5f0540(0x104)][_0x5f0540(0x123)]( - _0x5f0540(0x125) - ); - if (!_0x64ad7 || !_0x64ad7[_0x5f0540(0x138)](_0x5f0540(0x126))) - throw new Error(_0x5f0540(0x153)); - const _0x572a77 = await _0x7ef5a8[_0x5f0540(0x147)](); - console['log'](_0x5f0540(0x136) + new Date()['toISOString']()), - console[_0x5f0540(0x155)](_0x5f0540(0x11a) + _0x7ef5a8['status']), - console[_0x5f0540(0x155)]( - '[CLIENT]\x20Response\x20size:\x20~' + - JSON[_0x5f0540(0x156)](_0x572a77)[_0x5f0540(0x141)] / 0x400 + - '\x20KB' - ); - if (!_0x7ef5a8['ok']) - throw new Error( - _0x572a77[_0x5f0540(0x108)] || - _0x572a77[_0x5f0540(0x118)] || - _0x5f0540(0x124) + _0x7ef5a8[_0x5f0540(0x127)] - ); - if (_0x572a77['error']) - window[_0x5f0540(0x131)][_0x5f0540(0x14c)](_0x572a77[_0x5f0540(0x108)]); - else - _0x572a77['status'] === _0x5f0540(0x108) ? - window[_0x5f0540(0x131)][_0x5f0540(0x14c)]( - _0x572a77['message'] || _0x5f0540(0x135) - ) - : ((window[_0x5f0540(0x103)][_0x5f0540(0x100)] = _0x572a77), - window[_0x5f0540(0x131)][_0x5f0540(0x102)](_0x572a77)); - } catch (_0x563951) { - _0x563951[_0x5f0540(0x11f)] === 'AbortError' ? - window[_0x5f0540(0x131)]['displayError'](_0x5f0540(0x13e)) - : (window['uiAPI'][_0x5f0540(0x14c)]( - _0x5f0540(0x124) + (_0x563951[_0x5f0540(0x118)] || _0x5f0540(0x148)) - ), - console['error'](_0x5f0540(0x140), _0x563951)); - } finally { - _0x4a22b8[_0x5f0540(0x109)][_0x5f0540(0x121)] = 'none'; - } -} -function formatDuration(_0x58e5da) { - const _0x547bb4 = a0_0x3e5ceb; - if (!_0x58e5da || isNaN(_0x58e5da)) return _0x58e5da; - const _0x2efa31 = parseFloat(_0x58e5da), - _0x5a8e77 = Math[_0x547bb4(0x11d)](_0x2efa31 / 0x15180), - _0x31860f = Math[_0x547bb4(0x11d)]((_0x2efa31 % 0x15180) / 0xe10), - _0x40f2d9 = Math[_0x547bb4(0x11d)]((_0x2efa31 % 0xe10) / 0x3c), - _0x364d47 = Math[_0x547bb4(0x11d)](_0x2efa31 % 0x3c); - return ( - _0x5a8e77 + - _0x547bb4(0x107) + - _0x31860f + - _0x547bb4(0x137) + - _0x40f2d9 + - _0x547bb4(0x10b) + - _0x364d47 + - '\x20Seconds' - ); -} -function formatEpochTime(_0x2ad881, _0x376584) { - const _0x564df4 = a0_0x3e5ceb; - if (!_0x2ad881) return _0x2ad881; - const _0x2eb117 = parseInt(_0x2ad881); - if (isNaN(_0x2eb117)) return _0x2ad881; - const _0x403f03 = - _0x2eb117['toString']()['length'] <= 0xa ? _0x2eb117 * 0x3e8 : _0x2eb117; - let _0x56476d = 0x0; - if (_0x376584 !== _0x564df4(0x128)) { - const _0x34e2a2 = _0x376584['match'](/GMT([+-])(\d+)(?::(\d+))?/); - if (_0x34e2a2) { - const _0x27099d = _0x34e2a2[0x1] === '+' ? 0x1 : -0x1, - _0x16eec3 = parseInt(_0x34e2a2[0x2]), - _0x3bff6c = _0x34e2a2[0x3] ? parseInt(_0x34e2a2[0x3]) : 0x0; - _0x56476d = _0x27099d * (_0x16eec3 * 0x3c + _0x3bff6c) * 0x3c * 0x3e8; - } - } - const _0x56f46a = new Date(_0x403f03 + _0x56476d); - return _0x56f46a[_0x564df4(0x14d)]()[_0x564df4(0x12e)]( - _0x564df4(0x132), - _0x376584 - ); -} -function a0_0x1776(_0x51f135, _0x4e9ae9) { - const _0x424e92 = a0_0x424e(); - return ( - (a0_0x1776 = function (_0x177606, _0x203857) { - _0x177606 = _0x177606 - 0x100; - let _0x3fd5d4 = _0x424e92[_0x177606]; - return _0x3fd5d4; - }), - a0_0x1776(_0x51f135, _0x4e9ae9) - ); -} -function processTimestamps( - _0x13d227, - _0x1f087c, - _0x345fbc = [ - a0_0x3e5ceb(0x122), - a0_0x3e5ceb(0x13b), - a0_0x3e5ceb(0x10c), - 'utcEndSeconds', - 'timestamp', - 'startTime', - a0_0x3e5ceb(0x12f), - ], - _0x3c9b02 = [ - a0_0x3e5ceb(0x143), - 'timePlayedTotal', - a0_0x3e5ceb(0x130), - 'avgLifeTime', - 'duration', - a0_0x3e5ceb(0x120), - ] -) { - const _0x4c2013 = a0_0x3e5ceb; - if (!_0x13d227 || typeof _0x13d227 !== _0x4c2013(0x101)) return _0x13d227; - if (Array[_0x4c2013(0x10e)](_0x13d227)) - return _0x13d227[_0x4c2013(0x11b)]((_0x2088ab) => - processTimestamps(_0x2088ab, _0x1f087c, _0x345fbc, _0x3c9b02) - ); - const _0x134b47 = {}; - for (const [_0x1a08ca, _0x13b85e] of Object[_0x4c2013(0x14a)](_0x13d227)) { - if ( - _0x345fbc[_0x4c2013(0x138)](_0x1a08ca) && - typeof _0x13b85e === _0x4c2013(0x12d) - ) - _0x134b47[_0x1a08ca] = formatEpochTime(_0x13b85e, _0x1f087c); - else { - if ( - _0x3c9b02[_0x4c2013(0x138)](_0x1a08ca) && - typeof _0x13b85e === 'number' && - document[_0x4c2013(0x13a)](_0x4c2013(0x144))[_0x4c2013(0x14e)] - ) - _0x134b47[_0x1a08ca] = formatDuration(_0x13b85e); - else - typeof _0x13b85e === _0x4c2013(0x101) && _0x13b85e !== null ? - (_0x134b47[_0x1a08ca] = processTimestamps( - _0x13b85e, - _0x1f087c, - _0x345fbc, - _0x3c9b02 - )) - : (_0x134b47[_0x1a08ca] = _0x13b85e); - } - } - return _0x134b47; -} diff --git a/src/js/demoTracker.js b/src/js/demoTracker.js new file mode 100644 index 0000000..62a70a3 --- /dev/null +++ b/src/js/demoTracker.js @@ -0,0 +1,209 @@ +/** + * Demo Mode Tracker Module + * Handles tracking and limiting demo mode API requests + */ + +const { logger } = require('./logger.js'); +const path = require('path'); +const fs = require('fs'); + +// Demo mode state +let demoModeActive = false; +let defaultSsoToken = ''; + +// Configure request tracking for demo mode +const demoModeRequestTracker = { + requests: new Map(), + maxRequestsPerHour: 20, // Adjust as needed + resetInterval: 60 * 60 * 1000, // 1 hour in milliseconds + + isLimited: function (ip) { + const now = Date.now(); + const userRequests = this.requests.get(ip) || { + count: 0, + resetTime: now + this.resetInterval, + }; + + // Reset counter if the reset time has passed + if (now > userRequests.resetTime) { + userRequests.count = 0; + userRequests.resetTime = now + this.resetInterval; + } + + return userRequests.count >= this.maxRequestsPerHour; + }, + + incrementCounter: function (ip) { + const now = Date.now(); + const userRequests = this.requests.get(ip) || { + count: 0, + resetTime: now + this.resetInterval, + }; + + // Reset counter if the reset time has passed + if (now > userRequests.resetTime) { + userRequests.count = 1; // Start with 1 for this request + userRequests.resetTime = now + this.resetInterval; + } else { + userRequests.count++; + } + + this.requests.set(ip, userRequests); + return userRequests.count; + }, + + cleanupExpiredEntries: function () { + const now = Date.now(); + for (const [ip, data] of this.requests.entries()) { + if (now > data.resetTime) { + // Remove expired entries + this.requests.delete(ip); + } + } + }, + + getRemainingRequests: function (ip) { + if (!demoModeActive) return null; + + const userRequests = this.requests.get(ip) || { count: 0 }; + return this.maxRequestsPerHour - userRequests.count; + }, +}; + +/** + * Initialize demo mode + * @param {boolean} active Whether to activate demo mode + * @returns {Promise} Whether demo mode was activated successfully + */ +async function initializeDemoMode(active) { + // If active is explicitly false, disable demo mode + if (active === false) { + demoModeActive = false; + defaultSsoToken = ''; + logger.info('Demo mode explicitly disabled'); + return false; + } + + // If active is true but demo mode is already active, just return true + if (active === true && demoModeActive && defaultSsoToken) { + logger.debug('Demo mode already active'); + return true; + } + + // Try to read the token from a file if active is true + if (active === true) { + try { + const tokenPath = path.join(__dirname, '..', '..', 'token.txt'); + if (fs.existsSync(tokenPath)) { + defaultSsoToken = fs.readFileSync(tokenPath, 'utf8').trim(); + if (defaultSsoToken) { + demoModeActive = true; + logger.info('Default SSO token loaded successfully for demo mode'); + return true; + } else { + logger.warn('token.txt exists but contains no token'); + return false; + } + } else { + logger.warn('token.txt not found, demo mode cannot be activated'); + return false; + } + } catch (error) { + logger.error('Error loading token file:', { error: error.message }); + return false; + } + } + + // If active is not explicitly set, return current state + return demoModeActive; +} + +/** + * Demo mode middleware to check request limits + */ +function demoModeMiddleware(req, res, next) { + // Skip if demo mode is not active + if (!demoModeActive) { + return next(); + } + + // Skip non-API routes + if (!req.path.startsWith('/api/')) { + return next(); + } + + // Get client IP for tracking + const clientIP = + req.headers['cf-connecting-ip'] || + req.headers['x-forwarded-for']?.split(',')[0] || + req.ip || + req.connection.remoteAddress; + + // Check for rate limit + if (demoModeRequestTracker.isLimited(clientIP)) { + const userRequests = demoModeRequestTracker.requests.get(clientIP); + const resetTimeMs = userRequests.resetTime - Date.now(); + + // Format time as HH:MM:SS + const hours = Math.floor(resetTimeMs / (60 * 60 * 1000)); + const minutes = Math.floor((resetTimeMs % (60 * 60 * 1000)) / (60 * 1000)); + const seconds = Math.floor((resetTimeMs % (60 * 1000)) / 1000); + const timeFormat = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; + + logger.warn(`Demo mode rate limit exceeded for IP: ${clientIP}`); + return res.status(429).json({ + status: 'error', + message: `Please try again in ${timeFormat} or use your own SSO token.`, + timestamp: global.Utils.toIsoString(new Date()), + }); + } + + next(); +} + +/** + * Increment the demo request counter when a valid API call is made + * @param {Object} req Express request object + * @param {string} ssoToken The token used for the request + * @returns {number|null} The new count or null if not in demo mode + */ +function incrementDemoCounter(req, ssoToken) { + if (!demoModeActive || ssoToken !== defaultSsoToken) { + return null; + } + + const clientIP = + req.headers['cf-connecting-ip'] || + req.headers['x-forwarded-for']?.split(',')[0] || + req.ip || + req.connection.remoteAddress; + + const count = demoModeRequestTracker.incrementCounter(clientIP); + logger.debug(`Demo mode request count for ${clientIP}: ${count}`); + return count; +} + +/** + * Get the default SSO token if demo mode is active + * @returns {string|null} The default SSO token or null if demo mode is not active + */ +function getDefaultSsoToken() { + return demoModeActive ? defaultSsoToken : null; +} + +/** + * Check if demo mode is active + * @returns {boolean} Whether demo mode is active + */ +function isDemoModeActive() { + return demoModeActive; +} + +module.exports = { + initializeDemoMode, + demoModeMiddleware, + incrementDemoCounter, + getDefaultSsoToken, + isDemoModeActive, + demoModeRequestTracker, +}; diff --git a/src/js/frontend.js b/src/js/frontend.js index bc905be..0c026a0 100644 --- a/src/js/frontend.js +++ b/src/js/frontend.js @@ -57,6 +57,8 @@ const clientLogger = { }, }; +window.addEventListener('popstate', checkDemoMode); + // Initialize once DOM is loaded document.addEventListener('DOMContentLoaded', function () { initTabSwitching(); @@ -86,9 +88,18 @@ function checkDemoMode() { }) .then((data) => { console.log('Demo mode status:', data.demoMode); - if (data.demoMode) { + // Check for URL parameter as well + const urlParams = new URLSearchParams(window.location.search); + const demoParam = urlParams.get('demo'); + + if (data.demoMode || demoParam === 'true') { enableDemoMode(); + } else if (demoParam === 'false' || !data.demoMode) { + disableDemoMode(); } + + // Update request limit information + updateRequestLimitInfo(); }) .catch((error) => { console.error('Error checking demo mode:', error); @@ -128,6 +139,23 @@ function enableDemoMode() { window.appState.demoMode = true; } +function disableDemoMode() { + const ssoTokenInput = document.getElementById('ssoToken'); + if (ssoTokenInput) { + ssoTokenInput.disabled = false; + ssoTokenInput.placeholder = 'Enter your SSO token'; + ssoTokenInput.classList.remove('demo-mode'); + } + + const demoNotice = document.querySelector('.demo-notice'); + if (demoNotice) { + demoNotice.remove(); + } + + window.appState = window.appState || {}; + window.appState.demoMode = false; +} + function updateRequestLimitInfo() { fetch('/health') .then((response) => response.json())