chore: refine demo mode

This commit is contained in:
Rim 2025-04-16 07:36:40 -04:00
parent d19b5524c6
commit 18284f7483
4 changed files with 337 additions and 560 deletions

277
app.js
View File

@ -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'));
});

381
src/js/backend-min.js vendored
View File

@ -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;
}

209
src/js/demoTracker.js Normal file
View File

@ -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<boolean>} 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,
};

View File

@ -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())