986 lines
29 KiB
JavaScript
986 lines
29 KiB
JavaScript
const express = require('express');
|
|
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 fs = require('fs');
|
|
const app = express();
|
|
const port = process.env.PORT || 3512;
|
|
const {
|
|
processJsonOutput,
|
|
sanitizeHeaders,
|
|
timeoutPromise,
|
|
ensureLogin,
|
|
handleApiError,
|
|
activeSessions,
|
|
} = require('./src/js/serverUtils.js');
|
|
require('./src/js/utils.js');
|
|
|
|
app.set('trust proxy', true);
|
|
|
|
// Middleware
|
|
app.use(bodyParser.urlencoded({ extended: true, limit: '10mb' }));
|
|
app.use(express.static(__dirname));
|
|
app.use(express.static(path.join(__dirname, 'public')));
|
|
app.use('/images', express.static(path.join(__dirname, 'src/images')));
|
|
app.use(favicon(path.join(__dirname, 'src', 'images', 'favicon.ico')));
|
|
app.use(
|
|
bodyParser.json({
|
|
limit: '10mb',
|
|
verify: (req, res, buf) => {
|
|
req.rawBody = buf.toString();
|
|
},
|
|
})
|
|
);
|
|
app.use(demoTracker.demoModeMiddleware);
|
|
|
|
// Set up demo mode cleanup interval
|
|
setInterval(
|
|
() => demoTracker.demoModeRequestTracker.cleanupExpiredEntries(),
|
|
3600000
|
|
); // Clean up every hour
|
|
|
|
app.get('/health', (req, res) => {
|
|
const clientIP =
|
|
req.headers['cf-connecting-ip'] ||
|
|
req.headers['x-forwarded-for']?.split(',')[0] ||
|
|
req.ip ||
|
|
req.connection.remoteAddress;
|
|
|
|
const isDemoMode = demoTracker.isDemoModeActive();
|
|
res.json({
|
|
status: 'ok',
|
|
timestamp: global.Utils.toIsoString(new Date()),
|
|
demoMode: isDemoMode,
|
|
requestsRemaining:
|
|
isDemoMode ?
|
|
demoTracker.demoModeRequestTracker.maxRequestsPerHour -
|
|
(demoTracker.demoModeRequestTracker.requests.get(clientIP)?.count || 0)
|
|
: null,
|
|
});
|
|
});
|
|
|
|
// Serve the main HTML file
|
|
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'));
|
|
});
|
|
|
|
// 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');
|
|
});
|
|
|
|
// API endpoint to fetch stats
|
|
app.post('/api/stats', async (req, res) => {
|
|
logger.debug('Received request for /api/stats');
|
|
logger.debug(
|
|
`Request IP: ${
|
|
req.headers['x-forwarded-for'] || req.ip || req.connection.remoteAddress
|
|
}`
|
|
);
|
|
logger.debug(
|
|
`Request JSON: ${JSON.stringify({
|
|
username: req.body.username,
|
|
platform: req.body.platform,
|
|
game: req.body.game,
|
|
apiCall: req.body.apiCall,
|
|
sanitize: req.body.sanitize,
|
|
replaceKeys: req.body.replaceKeys,
|
|
})}`
|
|
);
|
|
|
|
try {
|
|
let { username, ssoToken, platform, game, apiCall, sanitize, replaceKeys } =
|
|
req.body;
|
|
|
|
const defaultToken = demoTracker.getDefaultSsoToken();
|
|
if (!ssoToken && defaultToken) {
|
|
ssoToken = defaultToken;
|
|
logger.info('Using default SSO token for demo mode');
|
|
} else if (!ssoToken) {
|
|
return res.status(200).json({
|
|
status: 'error',
|
|
message: 'SSO Token is required',
|
|
timestamp: global.Utils.toIsoString(new Date()),
|
|
});
|
|
}
|
|
|
|
/*
|
|
logger.debug(
|
|
`Request details - Username: ${username}, Platform: ${platform}, Game: ${game}, API Call: ${apiCall}`
|
|
);
|
|
|
|
logger.debug("=== STATS REQUEST ===");
|
|
logger.debug(`Username: ${username || 'Not provided'}`);
|
|
logger.debug(`Platform: ${platform}`);
|
|
logger.debug(`Game: ${game}`);
|
|
logger.debug(`API Call: ${apiCall}`);
|
|
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' });
|
|
}
|
|
|
|
// Clear previous session if it exists
|
|
if (activeSessions.has(ssoToken)) {
|
|
logger.debug('Clearing previous session');
|
|
activeSessions.delete(ssoToken);
|
|
}
|
|
|
|
// Login with the provided SSO token
|
|
try {
|
|
await ensureLogin(ssoToken);
|
|
} catch (loginError) {
|
|
logger.error('Login error:', loginError);
|
|
return res.status(200).json({
|
|
status: 'error',
|
|
error_type: 'LoginError',
|
|
message: 'SSO token login failed',
|
|
details: loginError.message || 'Unknown login error',
|
|
timestamp: global.Utils.toIsoString(new Date()),
|
|
});
|
|
}
|
|
|
|
// Create a wrapped function for each API call to handle timeout
|
|
const fetchWithTimeout = async (apiFn) => {
|
|
return Promise.race([
|
|
apiFn(),
|
|
timeoutPromise(30000), // 30 second timeout
|
|
]);
|
|
};
|
|
|
|
// Check if the platform is valid for the game
|
|
const requiresUno = ['mw2', 'wz2', 'mw3', 'wzm'].includes(game);
|
|
if (requiresUno && platform !== 'uno' && apiCall !== 'mapList') {
|
|
logger.warn(`${game} requires Uno ID`);
|
|
return res.status(200).json({
|
|
status: 'error',
|
|
message: `${game} requires Uno ID (numerical ID)`,
|
|
timestamp: global.Utils.toIsoString(new Date()),
|
|
});
|
|
}
|
|
|
|
try {
|
|
logger.debug(
|
|
`Attempting to fetch ${game} data for ${username} on ${platform}`
|
|
);
|
|
let data;
|
|
|
|
if (apiCall === 'fullData') {
|
|
// Fetch lifetime stats based on game
|
|
switch (game) {
|
|
case 'mw':
|
|
data = await fetchWithTimeout(() =>
|
|
API.ModernWarfare.fullData(username, platform)
|
|
);
|
|
break;
|
|
case 'wz':
|
|
data = await fetchWithTimeout(() =>
|
|
API.Warzone.fullData(username, platform)
|
|
);
|
|
break;
|
|
case 'mw2':
|
|
data = await fetchWithTimeout(() =>
|
|
API.ModernWarfare2.fullData(username)
|
|
);
|
|
break;
|
|
case 'wz2':
|
|
data = await fetchWithTimeout(() =>
|
|
API.Warzone2.fullData(username)
|
|
);
|
|
break;
|
|
case 'mw3':
|
|
data = await fetchWithTimeout(() =>
|
|
API.ModernWarfare3.fullData(username)
|
|
);
|
|
break;
|
|
case 'cw':
|
|
data = await fetchWithTimeout(() =>
|
|
API.ColdWar.fullData(username, platform)
|
|
);
|
|
break;
|
|
case 'vg':
|
|
data = await fetchWithTimeout(() =>
|
|
API.Vanguard.fullData(username, platform)
|
|
);
|
|
break;
|
|
case 'wzm':
|
|
data = await fetchWithTimeout(() =>
|
|
API.WarzoneMobile.fullData(username)
|
|
);
|
|
break;
|
|
default:
|
|
return res.status(200).json({
|
|
status: 'error',
|
|
message: 'Invalid game selected',
|
|
timestamp: global.Utils.toIsoString(new Date()),
|
|
});
|
|
}
|
|
} else if (apiCall === 'combatHistory') {
|
|
// Fetch recent match history based on game
|
|
switch (game) {
|
|
case 'mw':
|
|
data = await fetchWithTimeout(() =>
|
|
API.ModernWarfare.combatHistory(username, platform)
|
|
);
|
|
break;
|
|
case 'wz':
|
|
data = await fetchWithTimeout(() =>
|
|
API.Warzone.combatHistory(username, platform)
|
|
);
|
|
break;
|
|
case 'mw2':
|
|
data = await fetchWithTimeout(() =>
|
|
API.ModernWarfare2.combatHistory(username)
|
|
);
|
|
break;
|
|
case 'wz2':
|
|
data = await fetchWithTimeout(() =>
|
|
API.Warzone2.combatHistory(username)
|
|
);
|
|
break;
|
|
case 'mw3':
|
|
data = await fetchWithTimeout(() =>
|
|
API.ModernWarfare3.combatHistory(username)
|
|
);
|
|
break;
|
|
case 'cw':
|
|
data = await fetchWithTimeout(() =>
|
|
API.ColdWar.combatHistory(username, platform)
|
|
);
|
|
break;
|
|
case 'vg':
|
|
data = await fetchWithTimeout(() =>
|
|
API.Vanguard.combatHistory(username, platform)
|
|
);
|
|
break;
|
|
case 'wzm':
|
|
data = await fetchWithTimeout(() =>
|
|
API.WarzoneMobile.combatHistory(username)
|
|
);
|
|
break;
|
|
default:
|
|
return res.status(200).json({
|
|
status: 'error',
|
|
message: 'Invalid game selected',
|
|
timestamp: global.Utils.toIsoString(new Date()),
|
|
});
|
|
}
|
|
} else if (apiCall === 'mapList') {
|
|
// Fetch map list (only valid for MW)
|
|
if (game === 'mw') {
|
|
data = await fetchWithTimeout(() =>
|
|
API.ModernWarfare.mapList(platform)
|
|
);
|
|
} else {
|
|
return res.status(200).json({
|
|
status: 'error',
|
|
message: 'Map list is only available for Modern Warfare',
|
|
timestamp: global.Utils.toIsoString(new Date()),
|
|
});
|
|
}
|
|
}
|
|
|
|
logger.debug('Data fetched successfully');
|
|
logger.debug(`Response Size: ~${JSON.stringify(data).length / 1024} KB`);
|
|
logger.debug(`Response Time: ${global.Utils.toIsoString(new Date())}`);
|
|
|
|
// Safely handle the response data
|
|
if (!data) {
|
|
logger.warn('No data returned from API');
|
|
return res.json({
|
|
status: 'partial_success',
|
|
message: 'No data returned from API, but no error thrown',
|
|
data: null,
|
|
timestamp: global.Utils.toIsoString(new Date()),
|
|
});
|
|
}
|
|
|
|
// logger.debug("Returning data to client");
|
|
|
|
const { sanitize, replaceKeys } = req.body;
|
|
|
|
demoTracker.incrementDemoCounter(req, ssoToken);
|
|
|
|
return res.json({
|
|
// status: "success",
|
|
data: processJsonOutput(data, { sanitize, replaceKeys }),
|
|
timestamp: global.Utils.toIsoString(new Date()),
|
|
});
|
|
} catch (apiError) {
|
|
return handleApiError(apiError, res);
|
|
}
|
|
} catch (serverError) {
|
|
logger.error('Server Error:', serverError);
|
|
|
|
// Return a structured error response even for unexpected errors
|
|
return res.status(200).json({
|
|
status: 'server_error',
|
|
message: 'The server encountered an unexpected error',
|
|
error_details: serverError.message || 'Unknown server error',
|
|
timestamp: global.Utils.toIsoString(new Date()),
|
|
});
|
|
}
|
|
});
|
|
|
|
// API endpoint to fetch recent matches
|
|
app.post('/api/matches', async (req, res) => {
|
|
logger.debug('Received request for /api/matches');
|
|
logger.debug(
|
|
`Request IP: ${
|
|
req.headers['x-forwarded-for'] || req.ip || req.connection.remoteAddress
|
|
}`
|
|
);
|
|
logger.debug(
|
|
`Request JSON: ${JSON.stringify({
|
|
username: req.body.username,
|
|
platform: req.body.platform,
|
|
game: req.body.game,
|
|
sanitize: req.body.sanitize,
|
|
replaceKeys: req.body.replaceKeys,
|
|
})}`
|
|
);
|
|
|
|
try {
|
|
let { username, ssoToken, platform, game, sanitize, replaceKeys } =
|
|
req.body;
|
|
|
|
/*
|
|
logger.debug(
|
|
`Request details - Username: ${username}, Platform: ${platform}, Game: ${game}`
|
|
);
|
|
|
|
logger.debug("=== MATCHES REQUEST ===");
|
|
logger.debug(`Username: ${username}`);
|
|
logger.debug(`Platform: ${platform}`);
|
|
logger.debug(`Game: ${game}`);
|
|
logger.debug(`Processing Options - Sanitize: ${sanitize}, Replace Keys: ${replaceKeys}`);
|
|
logger.debug("========================"); */
|
|
|
|
const defaultToken = demoTracker.getDefaultSsoToken();
|
|
if (!ssoToken && defaultToken) {
|
|
ssoToken = defaultToken;
|
|
logger.info('Using default SSO token for demo mode');
|
|
} else if (!ssoToken) {
|
|
return res.status(200).json({
|
|
status: 'error',
|
|
message: 'SSO Token is required',
|
|
timestamp: global.Utils.toIsoString(new Date()),
|
|
});
|
|
}
|
|
|
|
if (!username) {
|
|
return res.status(400).json({ error: 'Username is required' });
|
|
}
|
|
|
|
if (!username || !ssoToken) {
|
|
return res
|
|
.status(400)
|
|
.json({ error: 'Username and SSO Token are required' });
|
|
}
|
|
|
|
try {
|
|
await ensureLogin(ssoToken);
|
|
} catch (loginError) {
|
|
return res.status(200).json({
|
|
status: 'error',
|
|
error_type: 'LoginError',
|
|
message: 'SSO token login failed',
|
|
details: loginError.message || 'Unknown login error',
|
|
timestamp: global.Utils.toIsoString(new Date()),
|
|
});
|
|
}
|
|
|
|
// Create a wrapped function for each API call to handle timeout
|
|
const fetchWithTimeout = async (apiFn) => {
|
|
return Promise.race([
|
|
apiFn(),
|
|
timeoutPromise(30000), // 30 second timeout
|
|
]);
|
|
};
|
|
|
|
try {
|
|
logger.debug(
|
|
`Attempting to fetch combat history for ${username} on ${platform}`
|
|
);
|
|
let data;
|
|
|
|
// Check if the platform is valid for the game
|
|
const requiresUno = ['mw2', 'wz2', 'mw3', 'wzm'].includes(game);
|
|
if (requiresUno && platform !== 'uno') {
|
|
return res.status(200).json({
|
|
status: 'error',
|
|
message: `${game} requires Uno ID (numerical ID)`,
|
|
timestamp: global.Utils.toIsoString(new Date()),
|
|
});
|
|
}
|
|
|
|
// Fetch combat history based on game
|
|
switch (game) {
|
|
case 'mw':
|
|
data = await fetchWithTimeout(() =>
|
|
API.ModernWarfare.combatHistory(username, platform)
|
|
);
|
|
break;
|
|
case 'wz':
|
|
data = await fetchWithTimeout(() =>
|
|
API.Warzone.combatHistory(username, platform)
|
|
);
|
|
break;
|
|
case 'mw2':
|
|
data = await fetchWithTimeout(() =>
|
|
API.ModernWarfare2.combatHistory(username)
|
|
);
|
|
break;
|
|
case 'wz2':
|
|
data = await fetchWithTimeout(() =>
|
|
API.Warzone2.combatHistory(username)
|
|
);
|
|
break;
|
|
case 'mw3':
|
|
data = await fetchWithTimeout(() =>
|
|
API.ModernWarfare3.combatHistory(username)
|
|
);
|
|
break;
|
|
case 'cw':
|
|
data = await fetchWithTimeout(() =>
|
|
API.ColdWar.combatHistory(username, platform)
|
|
);
|
|
break;
|
|
case 'vg':
|
|
data = await fetchWithTimeout(() =>
|
|
API.Vanguard.combatHistory(username, platform)
|
|
);
|
|
break;
|
|
case 'wzm':
|
|
data = await fetchWithTimeout(() =>
|
|
API.WarzoneMobile.combatHistory(username)
|
|
);
|
|
break;
|
|
default:
|
|
return res.status(200).json({
|
|
status: 'error',
|
|
message: 'Invalid game selected',
|
|
timestamp: global.Utils.toIsoString(new Date()),
|
|
});
|
|
}
|
|
|
|
const { sanitize, replaceKeys } = req.body;
|
|
|
|
demoTracker.incrementDemoCounter(req, ssoToken);
|
|
|
|
return res.json({
|
|
// status: "success",
|
|
data: processJsonOutput(data, { sanitize, replaceKeys }),
|
|
timestamp: global.Utils.toIsoString(new Date()),
|
|
});
|
|
} catch (apiError) {
|
|
return handleApiError(apiError, res);
|
|
}
|
|
} catch (serverError) {
|
|
return res.status(200).json({
|
|
status: 'server_error',
|
|
message: 'The server encountered an unexpected error',
|
|
error_details: serverError.message || 'Unknown server error',
|
|
timestamp: global.Utils.toIsoString(new Date()),
|
|
});
|
|
}
|
|
});
|
|
|
|
// API endpoint to fetch match info
|
|
app.post('/api/matchInfo', async (req, res) => {
|
|
logger.debug('Received request for /api/matchInfo');
|
|
logger.debug(
|
|
`Request IP: ${
|
|
req.headers['x-forwarded-for'] || req.ip || req.connection.remoteAddress
|
|
}`
|
|
);
|
|
logger.debug(
|
|
`Request JSON: ${JSON.stringify({
|
|
matchId: req.body.matchId,
|
|
platform: req.body.platform,
|
|
game: req.body.game,
|
|
sanitize: req.body.sanitize,
|
|
replaceKeys: req.body.replaceKeys,
|
|
})}`
|
|
);
|
|
|
|
try {
|
|
let { matchId, ssoToken, platform, game, sanitize, replaceKeys } = req.body;
|
|
|
|
/*
|
|
logger.debug(
|
|
`Request details - Match ID: ${matchId}, Platform: ${platform}, Game: ${game}`
|
|
);
|
|
|
|
logger.debug("=== MATCH INFO REQUEST ===");
|
|
logger.debug(`Match ID: ${matchId}`);
|
|
logger.debug(`Platform: ${platform}`);
|
|
logger.debug(`Game: ${game}`);
|
|
logger.debug(`Processing Options - Sanitize: ${sanitize}, Replace Keys: ${replaceKeys}`);
|
|
logger.debug("=========================="); */
|
|
|
|
const defaultToken = demoTracker.getDefaultSsoToken();
|
|
if (!ssoToken && defaultToken) {
|
|
ssoToken = defaultToken;
|
|
logger.info('Using default SSO token for demo mode');
|
|
} else if (!ssoToken) {
|
|
return res.status(200).json({
|
|
status: 'error',
|
|
message: 'SSO Token is required',
|
|
timestamp: global.Utils.toIsoString(new Date()),
|
|
});
|
|
}
|
|
|
|
if (!matchId) {
|
|
return res.status(400).json({ error: 'Match ID is required' });
|
|
}
|
|
|
|
if (!matchId || !ssoToken) {
|
|
return res
|
|
.status(400)
|
|
.json({ error: 'Match ID and SSO Token are required' });
|
|
}
|
|
|
|
try {
|
|
await ensureLogin(ssoToken);
|
|
} catch (loginError) {
|
|
return res.status(200).json({
|
|
status: 'error',
|
|
error_type: 'LoginError',
|
|
message: 'SSO token login failed',
|
|
details: loginError.message || 'Unknown login error',
|
|
timestamp: global.Utils.toIsoString(new Date()),
|
|
});
|
|
}
|
|
|
|
// Create a wrapped function for each API call to handle timeout
|
|
const fetchWithTimeout = async (apiFn) => {
|
|
return Promise.race([
|
|
apiFn(),
|
|
timeoutPromise(30000), // 30 second timeout
|
|
]);
|
|
};
|
|
|
|
try {
|
|
logger.debug(`Attempting to fetch match info for match ID: ${matchId}`);
|
|
let data;
|
|
|
|
// Fetch match info based on game
|
|
switch (game) {
|
|
case 'mw':
|
|
data = await fetchWithTimeout(() =>
|
|
API.ModernWarfare.matchInfo(matchId, platform)
|
|
);
|
|
break;
|
|
case 'wz':
|
|
data = await fetchWithTimeout(() =>
|
|
API.Warzone.matchInfo(matchId, platform)
|
|
);
|
|
break;
|
|
case 'mw2':
|
|
data = await fetchWithTimeout(() =>
|
|
API.ModernWarfare2.matchInfo(matchId)
|
|
);
|
|
break;
|
|
case 'wz2':
|
|
data = await fetchWithTimeout(() => API.Warzone2.matchInfo(matchId));
|
|
break;
|
|
case 'mw3':
|
|
data = await fetchWithTimeout(() =>
|
|
API.ModernWarfare3.matchInfo(matchId)
|
|
);
|
|
break;
|
|
case 'cw':
|
|
data = await fetchWithTimeout(() =>
|
|
API.ColdWar.matchInfo(matchId, platform)
|
|
);
|
|
break;
|
|
case 'vg':
|
|
data = await fetchWithTimeout(() =>
|
|
API.Vanguard.matchInfo(matchId, platform)
|
|
);
|
|
break;
|
|
case 'wzm':
|
|
data = await fetchWithTimeout(() =>
|
|
API.WarzoneMobile.matchInfo(matchId)
|
|
);
|
|
break;
|
|
default:
|
|
return res.status(200).json({
|
|
status: 'error',
|
|
message: 'Invalid game selected',
|
|
timestamp: global.Utils.toIsoString(new Date()),
|
|
});
|
|
}
|
|
|
|
const { sanitize, replaceKeys } = req.body;
|
|
|
|
demoTracker.incrementDemoCounter(req, ssoToken);
|
|
|
|
return res.json({
|
|
// status: "success",
|
|
data: processJsonOutput(data, { sanitize, replaceKeys }),
|
|
timestamp: global.Utils.toIsoString(new Date()),
|
|
});
|
|
} catch (apiError) {
|
|
return handleApiError(apiError, res);
|
|
}
|
|
} catch (serverError) {
|
|
return res.status(200).json({
|
|
status: 'server_error',
|
|
message: 'The server encountered an unexpected error',
|
|
error_details: serverError.message || 'Unknown server error',
|
|
timestamp: global.Utils.toIsoString(new Date()),
|
|
});
|
|
}
|
|
});
|
|
|
|
// API endpoint for user-related API calls
|
|
app.post('/api/user', async (req, res) => {
|
|
logger.debug('Received request for /api/user');
|
|
logger.debug(
|
|
`Request IP: ${
|
|
req.headers['x-forwarded-for'] || req.ip || req.connection.remoteAddress
|
|
}`
|
|
);
|
|
logger.debug(
|
|
`Request JSON: ${JSON.stringify({
|
|
username: req.body.username,
|
|
platform: req.body.platform,
|
|
userCall: req.body.userCall,
|
|
sanitize: req.body.sanitize,
|
|
replaceKeys: req.body.replaceKeys,
|
|
})}`
|
|
);
|
|
|
|
try {
|
|
let { username, ssoToken, platform, userCall, sanitize, replaceKeys } =
|
|
req.body;
|
|
|
|
/*
|
|
logger.debug(
|
|
`Request details - Username: ${username}, Platform: ${platform}, User Call: ${userCall}`
|
|
);
|
|
|
|
logger.debug("=== USER DATA REQUEST ===");
|
|
logger.debug(`Username: ${username || 'Not provided'}`);
|
|
logger.debug(`Platform: ${platform}`);
|
|
logger.debug(`User Call: ${userCall}`);
|
|
logger.debug(`Processing Options - Sanitize: ${sanitize}, Replace Keys: ${replaceKeys}`);
|
|
logger.debug("========================="); */
|
|
|
|
const defaultToken = demoTracker.getDefaultSsoToken();
|
|
if (!ssoToken && defaultToken) {
|
|
ssoToken = defaultToken;
|
|
logger.info('Using default SSO token for demo mode');
|
|
} else if (!ssoToken) {
|
|
return res.status(200).json({
|
|
status: 'error',
|
|
message: 'SSO Token is required',
|
|
timestamp: global.Utils.toIsoString(new Date()),
|
|
});
|
|
}
|
|
|
|
// For eventFeed and identities, username is not required
|
|
if (
|
|
!username &&
|
|
userCall !== 'eventFeed' &&
|
|
userCall !== 'friendFeed' &&
|
|
userCall !== 'identities'
|
|
) {
|
|
return res
|
|
.status(400)
|
|
.json({ error: 'Username is required for this API call' });
|
|
}
|
|
|
|
try {
|
|
await ensureLogin(ssoToken);
|
|
} catch (loginError) {
|
|
return res.status(200).json({
|
|
status: 'error',
|
|
error_type: 'LoginError',
|
|
message: 'SSO token login failed',
|
|
details: loginError.message || 'Unknown login error',
|
|
timestamp: global.Utils.toIsoString(new Date()),
|
|
});
|
|
}
|
|
|
|
// Create a wrapped function for each API call to handle timeout
|
|
const fetchWithTimeout = async (apiFn) => {
|
|
return Promise.race([
|
|
apiFn(),
|
|
timeoutPromise(30000), // 30 second timeout
|
|
]);
|
|
};
|
|
|
|
try {
|
|
logger.debug(`Attempting to fetch user data for ${userCall}`);
|
|
let data;
|
|
|
|
// Fetch user data based on userCall
|
|
switch (userCall) {
|
|
case 'codPoints':
|
|
data = await fetchWithTimeout(() =>
|
|
API.Me.codPoints(username, platform)
|
|
);
|
|
break;
|
|
case 'connectedAccounts':
|
|
data = await fetchWithTimeout(() =>
|
|
API.Me.connectedAccounts(username, platform)
|
|
);
|
|
break;
|
|
case 'eventFeed':
|
|
data = await fetchWithTimeout(() => API.Me.eventFeed());
|
|
break;
|
|
case 'friendFeed':
|
|
data = await fetchWithTimeout(() =>
|
|
API.Me.friendFeed(username, platform)
|
|
);
|
|
break;
|
|
case 'identities':
|
|
data = await fetchWithTimeout(() => API.Me.loggedInIdentities());
|
|
break;
|
|
case 'friendsList':
|
|
data = await fetchWithTimeout(() => API.Me.friendsList());
|
|
break;
|
|
// case "settings":
|
|
// data = await fetchWithTimeout(() =>
|
|
// API.Me.settings(username, platform)
|
|
// );
|
|
// break;
|
|
default:
|
|
return res.status(200).json({
|
|
status: 'error',
|
|
message: 'Invalid user API call selected',
|
|
timestamp: global.Utils.toIsoString(new Date()),
|
|
});
|
|
}
|
|
|
|
const { sanitize, replaceKeys } = req.body;
|
|
|
|
demoTracker.incrementDemoCounter(req, ssoToken);
|
|
|
|
return res.json({
|
|
// status: "success",
|
|
data: processJsonOutput(data, { sanitize, replaceKeys }),
|
|
timestamp: global.Utils.toIsoString(new Date()),
|
|
});
|
|
} catch (apiError) {
|
|
return handleApiError(apiError, res);
|
|
}
|
|
} catch (serverError) {
|
|
return res.status(200).json({
|
|
status: 'server_error',
|
|
message: 'The server encountered an unexpected error',
|
|
error_details: serverError.message || 'Unknown server error',
|
|
timestamp: global.Utils.toIsoString(new Date()),
|
|
});
|
|
}
|
|
});
|
|
|
|
// API endpoint for fuzzy search
|
|
app.post('/api/search', async (req, res) => {
|
|
logger.debug('Received request for /api/search');
|
|
logger.debug(
|
|
`Request IP: ${
|
|
req.headers['x-forwarded-for'] || req.ip || req.connection.remoteAddress
|
|
}`
|
|
);
|
|
logger.debug(
|
|
`Request JSON: ${JSON.stringify({
|
|
username: req.body.username,
|
|
platform: req.body.platform,
|
|
sanitize: req.body.sanitize,
|
|
replaceKeys: req.body.replaceKeys,
|
|
})}`
|
|
);
|
|
|
|
try {
|
|
let { username, ssoToken, platform, sanitize, replaceKeys } = req.body;
|
|
|
|
/*
|
|
logger.debug(
|
|
`Request details - Match ID: ${matchId}, Platform: ${platform}, Game: ${game}`
|
|
);
|
|
|
|
logger.debug("=== MATCH INFO REQUEST ===");
|
|
logger.debug(`Match ID: ${matchId}`);
|
|
logger.debug(`Platform: ${platform}`);
|
|
logger.debug(`Game: ${game}`);
|
|
logger.debug(`Processing Options - Sanitize: ${sanitize}, Replace Keys: ${replaceKeys}`);
|
|
logger.debug("=========================="); */
|
|
|
|
const defaultToken = demoTracker.getDefaultSsoToken();
|
|
if (!ssoToken && defaultToken) {
|
|
ssoToken = defaultToken;
|
|
logger.info('Using default SSO token for demo mode');
|
|
} else if (!ssoToken) {
|
|
return res.status(200).json({
|
|
status: 'error',
|
|
message: 'SSO Token is required',
|
|
timestamp: global.Utils.toIsoString(new Date()),
|
|
});
|
|
}
|
|
|
|
if (!username) {
|
|
return res.status(400).json({ error: 'Username is required' });
|
|
}
|
|
|
|
if (!username || !ssoToken) {
|
|
return res
|
|
.status(400)
|
|
.json({ error: 'Username and SSO Token are required' });
|
|
}
|
|
|
|
try {
|
|
await ensureLogin(ssoToken);
|
|
} catch (loginError) {
|
|
return res.status(200).json({
|
|
status: 'error',
|
|
error_type: 'LoginError',
|
|
message: 'SSO token login failed',
|
|
details: loginError.message || 'Unknown login error',
|
|
timestamp: global.Utils.toIsoString(new Date()),
|
|
});
|
|
}
|
|
|
|
// Create a wrapped function for each API call to handle timeout
|
|
const fetchWithTimeout = async (apiFn) => {
|
|
return Promise.race([
|
|
apiFn(),
|
|
timeoutPromise(30000), // 30 second timeout
|
|
]);
|
|
};
|
|
|
|
try {
|
|
logger.debug(
|
|
`Attempting fuzzy search for ${username} on platform ${platform}`
|
|
);
|
|
const data = await fetchWithTimeout(() =>
|
|
API.Misc.search(username, platform)
|
|
);
|
|
|
|
const { sanitize, replaceKeys } = req.body;
|
|
|
|
demoTracker.incrementDemoCounter(req, ssoToken);
|
|
|
|
return res.json({
|
|
// status: "success",
|
|
data: processJsonOutput(data, { sanitize, replaceKeys }),
|
|
timestamp: global.Utils.toIsoString(new Date()),
|
|
// link: "Stats pulled using codtracker.rimmyscorner.com",
|
|
});
|
|
} catch (apiError) {
|
|
return handleApiError(apiError, res);
|
|
}
|
|
} catch (serverError) {
|
|
return res.status(200).json({
|
|
status: 'server_error',
|
|
message: 'The server encountered an unexpected error',
|
|
error_details: serverError.message || 'Unknown server error',
|
|
timestamp: global.Utils.toIsoString(new Date()),
|
|
});
|
|
}
|
|
});
|
|
|
|
// Improved logging endpoint
|
|
app.post('/api/log', (req, res) => {
|
|
const clientIP =
|
|
req.headers['cf-connecting-ip'] ||
|
|
req.headers['x-forwarded-for']?.split(',')[0] ||
|
|
req.ip ||
|
|
req.connection.remoteAddress;
|
|
const userAgent = req.headers['user-agent'];
|
|
const referer = req.headers['referer'];
|
|
const origin = req.headers['origin'];
|
|
let logData;
|
|
|
|
try {
|
|
// Handle data whether it comes as already parsed JSON or as a string
|
|
if (typeof req.body === 'string') {
|
|
logData = JSON.parse(req.body);
|
|
} else if (req.body && typeof req.body === 'object') {
|
|
logData = req.body;
|
|
} else {
|
|
// If no parsable data found, create a basic log entry
|
|
logData = {
|
|
eventType: 'unknown',
|
|
timestamp: global.Utils.toIsoString(new Date()),
|
|
};
|
|
}
|
|
|
|
// Enrich log with server-side data
|
|
const enrichedLog = {
|
|
...logData,
|
|
meta: {
|
|
clientIP,
|
|
userAgent,
|
|
referer,
|
|
origin,
|
|
requestHeaders: sanitizeHeaders(req.headers),
|
|
serverTimestamp: global.Utils.toIsoString(new Date()),
|
|
requestId: req.id || Math.random().toString(36).substring(2, 15),
|
|
},
|
|
};
|
|
|
|
// Use the dedicated user activity logger
|
|
logger.userActivity(enrichedLog.eventType || 'unknown', enrichedLog);
|
|
} catch (error) {
|
|
logger.error('Error processing log data', {
|
|
error: error.message,
|
|
rawBody: typeof req.body === 'object' ? '[Object]' : req.body,
|
|
});
|
|
}
|
|
|
|
// Always return 200 to avoid client-side errors
|
|
res.status(200).send();
|
|
});
|
|
|
|
// Database storage function
|
|
/*
|
|
function storeLogInDatabase(logData) {
|
|
// Example with MongoDB
|
|
db.collection('user_logs').insertOne(logData)
|
|
.catch(err => logger.error('Failed to store log in database:', err));
|
|
}
|
|
*/
|
|
|
|
// Start the server
|
|
app.listen(port, () => {
|
|
logger.info(`Server running on http://localhost:${port}`);
|
|
});
|