chore: refine demo mode
This commit is contained in:
parent
d19b5524c6
commit
18284f7483
277
app.js
277
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'));
|
||||
});
|
||||
|
||||
|
381
src/js/backend-min.js
vendored
381
src/js/backend-min.js
vendored
@ -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
209
src/js/demoTracker.js
Normal 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,
|
||||
};
|
@ -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())
|
||||
|
Loading…
x
Reference in New Issue
Block a user