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