codtracker-js/app.js
2025-03-30 06:59:14 -04:00

932 lines
28 KiB
JavaScript

const express = require("express");
const path = require("path");
const bodyParser = require("body-parser");
const API = require("./src/js/index.js");
const favicon = require('serve-favicon');
const app = express();
const port = process.env.PORT || 3512;
// Middleware
app.use(bodyParser.json({ limit: "10mb" }));
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')));
const fs = require('fs');
// Initialize key replacements
let keyReplacements = {};
try {
const replacementsPath = path.join(__dirname, "src", "data", "replacements.json");
if (fs.existsSync(replacementsPath)) {
const replacementsContent = fs.readFileSync(replacementsPath, 'utf8');
keyReplacements = JSON.parse(replacementsContent);
// console.log("Replacements loaded successfully");
} else {
console.log("replacements.json not found, key replacement disabled");
}
} catch (error) {
console.error("Error loading replacements file:", error);
}
const replaceJsonKeys = (obj) => {
if (!obj || typeof obj !== 'object') return obj;
if (Array.isArray(obj)) {
return obj.map(item => replaceJsonKeys(item));
}
const newObj = {};
Object.keys(obj).forEach(key => {
// Replace key if it exists in replacements
const newKey = keyReplacements[key] || key;
// DEBUG: Log replacements when they happen
// if (newKey !== key) {
// console.log(`Replacing key "${key}" with "${newKey}"`);
// }
// Also check if the value should be replaced (if it's a string)
let value = obj[key];
if (typeof value === 'string' && keyReplacements[value]) {
value = keyReplacements[value];
// console.log(`Replacing value "${obj[key]}" with "${value}"`);
}
// Process value recursively if it's an object or array
newObj[newKey] = replaceJsonKeys(value);
});
return newObj;
};
// Utility regex function
const sanitizeJsonOutput = (data) => {
if (!data) return data;
// Convert to string to perform regex operations
const jsonString = JSON.stringify(data);
// Define regex pattern that matches HTML entities
const regexPattern = /<span class=".*?">|<\/span>|">/g;
// Replace unwanted patterns
const sanitizedString = jsonString.replace(regexPattern, '');
// Parse back to object
try {
return JSON.parse(sanitizedString);
} catch (e) {
console.error("Error parsing sanitized JSON:", e);
return data; // Return original data if parsing fails
}
};
// Combined function to sanitize and replace keys
const processJsonOutput = (data, options = { sanitize: true, replaceKeys: true }) => {
// Create a deep copy of the data to avoid reference issues
let processedData = JSON.parse(JSON.stringify(data));
// Apply sanitization if needed
if (options.sanitize) {
processedData = sanitizeJsonOutput(processedData);
}
// Apply key replacement if needed - make sure this is correctly receiving the option
if (options.replaceKeys) {
processedData = replaceJsonKeys(processedData);
}
return processedData;
};
// Improved token management with auto-refresh
const tokenManager = {
tokens: new Map(),
async getValidToken(ssoToken) {
// Check if we have a stored token and if it's still valid
const storedToken = this.tokens.get(ssoToken);
const currentTime = Date.now();
if (storedToken && (currentTime - storedToken.timestamp < 1800000)) { // 30 minutes expiry
console.log(`Attempting to validate token for SSO: ${ssoToken.substring(0, 5)}...`);
console.log("Using cached token");
return storedToken.token;
}
// We need to login and get a new token
try {
console.log(`Attempting to login with SSO token: ${ssoToken.substring(0, 5)}...`);
console.log(`Current active sessions: ${this.activeSessions.size}`);
const loginResult = await Promise.race([
API.login(ssoToken),
timeoutPromise(10000), // 10 second timeout
]);
// Store the new token with timestamp
this.tokens.set(ssoToken, {
token: loginResult,
timestamp: currentTime
});
console.log(`Authentication successful - Token: ${loginResult.substring(0, 5)}...`);
console.log(`Token cached at: ${new Date().toISOString()}`);
return loginResult;
} catch (error) {
console.error("Authentication failed:", error);
throw new Error("Failed to authenticate with SSO token");
}
},
invalidateToken(ssoToken) {
this.tokens.delete(ssoToken);
}
};
// Store active sessions to avoid repeated logins
const activeSessions = new Map();
// Utility function to create a timeout promise
const timeoutPromise = (ms) => {
return new Promise((_, reject) => {
setTimeout(() => reject(new Error(`Request timed out after ${ms}ms`)), ms);
});
};
// Helper function to ensure login
const ensureLogin = async (ssoToken) => {
if (!activeSessions.has(ssoToken)) {
console.log(`Attempting to login with SSO token: ${ssoToken.substring(0, 5)}...`);
// console.log(`Attempting to login with SSO token: ${ssoToken}`);
const loginResult = await Promise.race([
API.login(ssoToken),
timeoutPromise(10000), // 10 second timeout
]);
console.log(`Login successful: ${JSON.stringify(loginResult)}`);
console.log(`Session created at: ${new Date().toISOString()}`);
activeSessions.set(ssoToken, new Date());
} else {
console.log("Using existing session");
}
};
// Helper function to handle API errors
const handleApiError = (error, res) => {
console.error("API Error:", error);
console.error(`Error Stack: ${error.stack}`);
console.error(`Error Time: ${new Date().toISOString()}`);
// Try to extract more useful information from the error
let errorMessage = error.message || "Unknown API error";
let errorName = error.name || "ApiError";
// Handle the specific JSON parsing error
if (errorName === "SyntaxError" && errorMessage.includes("JSON")) {
console.log("JSON parsing error detected");
return res.status(200).json({
status: "error",
message:
"Failed to parse API response. This usually means the SSO token is invalid or expired.",
error_type: "InvalidResponseError",
timestamp: new Date().toISOString(),
});
}
// Send a more graceful response
return res.status(200).json({
status: "error",
message: errorMessage,
error_type: errorName,
timestamp: new Date().toISOString(),
});
};
// API endpoint to fetch stats
app.post("/api/stats", async (req, res) => {
console.log("Received request for /api/stats");
console.log(`Request IP: ${req.ip || req.connection.remoteAddress}`);
console.log(`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 {
const { username, ssoToken, platform, game, apiCall, sanitize, replaceKeys } = req.body;
/*
console.log(
`Request details - Username: ${username}, Platform: ${platform}, Game: ${game}, API Call: ${apiCall}`
);
console.log("=== STATS REQUEST ===");
console.log(`Username: ${username || 'Not provided'}`);
console.log(`Platform: ${platform}`);
console.log(`Game: ${game}`);
console.log(`API Call: ${apiCall}`);
console.log(`Processing Options - Sanitize: ${sanitize}, Replace Keys: ${replaceKeys}`);
console.log("====================="); */
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)) {
console.log("Clearing previous session");
activeSessions.delete(ssoToken);
}
// Login with the provided SSO token
try {
await ensureLogin(ssoToken);
} catch (loginError) {
console.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: new Date().toISOString(),
});
}
// 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") {
console.log(`${game} requires Uno ID`);
return res.status(200).json({
status: "error",
message: `${game} requires Uno ID (numerical ID)`,
timestamp: new Date().toISOString(),
});
}
try {
console.log(
`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: new Date().toISOString(),
});
}
} 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: new Date().toISOString(),
});
}
} 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: new Date().toISOString(),
});
}
}
console.log("Data fetched successfully");
console.log(`Response Size: ~${JSON.stringify(data).length / 1024} KB`);
console.log(`Response Time: ${new Date().toISOString()}`);
// Safely handle the response data
if (!data) {
console.log("No data returned from API");
return res.json({
status: "partial_success",
message: "No data returned from API, but no error thrown",
data: null,
timestamp: new Date().toISOString(),
});
}
// console.log("Returning data to client");
const { sanitize, replaceKeys } = req.body;
return res.json({
// status: "success",
data: processJsonOutput(data, { sanitize, replaceKeys }),
timestamp: new Date().toISOString(),
});
} catch (apiError) {
return handleApiError(apiError, res);
}
} catch (serverError) {
console.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: new Date().toISOString(),
});
}
});
// API endpoint to fetch recent matches
app.post("/api/matches", async (req, res) => {
console.log("Received request for /api/matches");
console.log(`Request IP: ${req.ip || req.connection.remoteAddress}`);
console.log(`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 {
const { username, ssoToken, platform, game, sanitize, replaceKeys } = req.body;
/*
console.log(
`Request details - Username: ${username}, Platform: ${platform}, Game: ${game}`
);
console.log("=== MATCHES REQUEST ===");
console.log(`Username: ${username}`);
console.log(`Platform: ${platform}`);
console.log(`Game: ${game}`);
console.log(`Processing Options - Sanitize: ${sanitize}, Replace Keys: ${replaceKeys}`);
console.log("========================"); */
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: new Date().toISOString(),
});
}
// 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 {
console.log(
`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: new Date().toISOString(),
});
}
// 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: new Date().toISOString(),
});
}
const { sanitize, replaceKeys } = req.body;
return res.json({
// status: "success",
data: processJsonOutput(data, { sanitize, replaceKeys }),
timestamp: new Date().toISOString(),
});
} 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: new Date().toISOString(),
});
}
});
// API endpoint to fetch match info
app.post("/api/matchInfo", async (req, res) => {
console.log("Received request for /api/matchInfo");
console.log(`Request IP: ${req.ip || req.connection.remoteAddress}`);
console.log(`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 {
const { matchId, ssoToken, platform, game, sanitize, replaceKeys } = req.body;
const mode = "mp";
/*
console.log(
`Request details - Match ID: ${matchId}, Platform: ${platform}, Game: ${game}`
);
console.log("=== MATCH INFO REQUEST ===");
console.log(`Match ID: ${matchId}`);
console.log(`Platform: ${platform}`);
console.log(`Game: ${game}`);
console.log(`Processing Options - Sanitize: ${sanitize}, Replace Keys: ${replaceKeys}`);
console.log("=========================="); */
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: new Date().toISOString(),
});
}
// 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 {
console.log(`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: new Date().toISOString(),
});
}
const { sanitize, replaceKeys } = req.body;
return res.json({
// status: "success",
data: processJsonOutput(data, { sanitize, replaceKeys }),
timestamp: new Date().toISOString(),
});
} 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: new Date().toISOString(),
});
}
});
// API endpoint for user-related API calls
app.post("/api/user", async (req, res) => {
console.log("Received request for /api/user");
console.log(`Request IP: ${req.ip || req.connection.remoteAddress}`);
console.log(`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 {
const { username, ssoToken, platform, userCall, sanitize, replaceKeys } = req.body;
/*
console.log(
`Request details - Username: ${username}, Platform: ${platform}, User Call: ${userCall}`
);
console.log("=== USER DATA REQUEST ===");
console.log(`Username: ${username || 'Not provided'}`);
console.log(`Platform: ${platform}`);
console.log(`User Call: ${userCall}`);
console.log(`Processing Options - Sanitize: ${sanitize}, Replace Keys: ${replaceKeys}`);
console.log("========================="); */
if (!ssoToken) {
return res.status(400).json({ error: "SSO Token is required" });
}
// 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: new Date().toISOString(),
});
}
// 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 {
console.log(`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: new Date().toISOString(),
});
}
const { sanitize, replaceKeys } = req.body;
return res.json({
// status: "success",
data: processJsonOutput(data, { sanitize, replaceKeys }),
timestamp: new Date().toISOString(),
});
} 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: new Date().toISOString(),
});
}
});
// API endpoint for fuzzy search
app.post("/api/search", async (req, res) => {
console.log("Received request for /api/search");
console.log(`Request IP: ${req.ip || req.connection.remoteAddress}`);
console.log(`Request JSON: ${JSON.stringify({
username: req.body.username,
platform: req.body.platform,
sanitize: req.body.sanitize,
replaceKeys: req.body.replaceKeys
})}`);
try {
const { username, ssoToken, platform, sanitize, replaceKeys } = req.body;
/*
console.log(
`Request details - Username to search: ${username}, Platform: ${platform}`
);
console.log("=== SEARCH REQUEST ===");
console.log(`Search Term: ${username}`);
console.log(`Platform: ${platform}`);
console.log(`Processing Options - Sanitize: ${sanitize}, Replace Keys: ${replaceKeys}`);
console.log("======================"); */
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: new Date().toISOString(),
});
}
// 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 {
console.log(
`Attempting fuzzy search for ${username} on platform ${platform}`
);
const data = await fetchWithTimeout(() =>
API.Misc.search(username, platform)
);
const { sanitize, replaceKeys } = req.body;
return res.json({
// status: "success",
data: processJsonOutput(data, { sanitize, replaceKeys }),
timestamp: new Date().toISOString(),
// 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: new Date().toISOString(),
});
}
});
// Basic health check endpoint
app.get("/health", (req, res) => {
res.json({ status: "ok", timestamp: new Date().toISOString() });
});
// Serve the main HTML file
app.get("/", (req, res) => {
res.sendFile(path.join(__dirname, "src", "index.html"));
});
// Start the server
app.listen(port, () => {
console.log(`Server running on http://localhost:${port}`);
});