chore: refine demo mode
This commit is contained in:
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'));
|
||||
});
|
||||
|
||||
|
Reference in New Issue
Block a user