Files
TurboTrades/index.js
iDefineHD 7a32454b83
All checks were successful
Build Frontend / Build Frontend (push) Successful in 24s
system now uses seperate pricing.
2026-01-11 03:24:54 +00:00

657 lines
19 KiB
JavaScript

import Fastify from "fastify";
import fastifyCookie from "@fastify/cookie";
import fastifyCors from "@fastify/cors";
import fastifyHelmet from "@fastify/helmet";
import fastifyRateLimit from "@fastify/rate-limit";
import fastifyWebsocket from "@fastify/websocket";
import passport from "passport";
import { config } from "./config/index.js";
import { connectDatabase } from "./config/database.js";
import { configurePassport } from "./config/passport.js";
import { wsManager } from "./utils/websocket.js";
// Import middleware
import { checkMaintenance } from "./middleware/maintenance.js";
// Import routes
import authRoutes from "./routes/auth.js";
import userRoutes from "./routes/user.js";
import websocketRoutes from "./routes/websocket.js";
import marketRoutes from "./routes/market.js";
import inventoryRoutes from "./routes/inventory.js";
import adminRoutes from "./routes/admin.js";
import adminManagementRoutes from "./routes/admin-management.js";
import configRoutes from "./routes/config.js";
// Import services
import pricingService from "./services/pricing.js";
import { getSteamBotManager } from "./services/steamBot.js";
// Import models
import User from "./models/User.js";
import Trade from "./models/Trade.js";
import Transaction from "./models/Transaction.js";
/**
* Create and configure Fastify server
*/
const createServer = () => {
const fastify = Fastify({
logger: {
level: config.isDevelopment ? "info" : "warn",
transport: config.isDevelopment
? {
target: "pino-pretty",
options: {
translateTime: "HH:MM:ss Z",
ignore: "pid,hostname",
},
}
: undefined,
},
trustProxy: true,
requestIdHeader: "x-request-id",
requestIdLogLabel: "reqId",
});
return fastify;
};
/**
* Register plugins
*/
const registerPlugins = async (fastify) => {
// CORS - Allow both local development and production
await fastify.register(fastifyCors, {
origin: (origin, callback) => {
const allowedOrigins = [
"http://localhost:5173",
"http://127.0.0.1:5173",
"https://turbotrades.dev",
"https://www.turbotrades.dev",
config.cors.origin,
];
// Allow requests from file:// protocol (local HTML files)
if (!origin || origin === "null") {
callback(null, true);
return;
}
// Allow localhost on any port in development
if (config.isDevelopment && origin.includes("localhost")) {
callback(null, true);
return;
}
// Check if origin is in allowed list
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error("Not allowed by CORS"), false);
}
},
preflightContinue: true,
credentials: true,
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"],
allowedHeaders: [
"Content-Type",
"Authorization",
"Cookie",
"X-Requested-With",
"Accept",
],
exposedHeaders: ["Set-Cookie", "Content-Type"],
preflight: true,
preflightContinue: false,
optionsSuccessStatus: 204,
maxAge: 86400, // Cache preflight requests for 24 hours
});
// Skip CORS for WebSocket connections
fastify.addHook("preHandler", async (request, reply) => {
// Allow WebSocket upgrade requests from any origin
if (request.raw.headers.upgrade === "websocket") {
reply.header(
"Access-Control-Allow-Origin",
request.headers.origin || "*"
);
reply.header("Access-Control-Allow-Credentials", "true");
}
});
// Security headers
await fastify.register(fastifyHelmet, {
contentSecurityPolicy: config.isProduction,
global: true,
});
// Cookies - Don't sign cookies, just parse them
await fastify.register(fastifyCookie, {
secret: config.session.secret,
parseOptions: {
httpOnly: true,
secure: config.cookie.secure,
sameSite: config.cookie.sameSite,
path: "/",
},
hook: "onRequest", // Parse cookies on every request
});
// Rate limiting
await fastify.register(fastifyRateLimit, {
max: config.rateLimit.max,
timeWindow: config.rateLimit.timeWindow,
cache: 10000,
allowList: config.isDevelopment ? ["127.0.0.1", "localhost"] : [],
redis: null, // TODO: Add Redis for distributed rate limiting
skipOnError: true,
});
// WebSocket support
await fastify.register(fastifyWebsocket, {
options: {
maxPayload: config.websocket.maxPayload,
verifyClient: (info, next) => {
// Can add additional WebSocket verification logic here
next(true);
},
},
});
// Passport for Steam authentication
fastify.decorate("passport", passport);
configurePassport();
console.log("✅ All plugins registered");
// Debug hook to log cookies on every request (development only)
if (config.isDevelopment) {
fastify.addHook("onRequest", async (request, reply) => {
if (request.url.includes("/user/") || request.url.includes("/auth/")) {
console.log(`\n🔍 Incoming ${request.method} ${request.url}`);
console.log(
" Cookies:",
Object.keys(request.cookies || {}).join(", ") || "NONE"
);
console.log(" Has accessToken:", !!request.cookies?.accessToken);
console.log(" Origin:", request.headers.origin);
console.log(" Host:", request.headers.host);
}
});
}
};
/**
* Setup routes
*/
const registerRoutes = async (fastify) => {
// Health check (both with and without /api prefix)
fastify.get("/health", async (request, reply) => {
return {
status: "ok",
timestamp: Date.now(),
uptime: process.uptime(),
environment: config.nodeEnv,
};
});
fastify.get("/api/health", async (request, reply) => {
return {
status: "ok",
timestamp: Date.now(),
uptime: process.uptime(),
environment: config.nodeEnv,
};
});
// Root endpoint
fastify.get("/", async (request, reply) => {
return {
name: "TurboTrades API",
version: "1.0.0",
environment: config.nodeEnv,
endpoints: {
health: "/api/health",
auth: "/api/auth/*",
user: "/api/user/*",
websocket: "/ws",
market: "/api/market/*",
},
};
});
// Debug endpoint - list all registered routes (development only)
fastify.get("/api/routes", async (request, reply) => {
if (config.isProduction) {
return reply.status(404).send({ error: "Not found" });
}
// Get all routes using Fastify's print routes
const routes = [];
// Iterate through Fastify's internal routes
for (const route of fastify.routes.values()) {
routes.push({
method: route.method,
url: route.url,
path: route.routePath || route.path,
});
}
// Sort by URL
routes.sort((a, b) => {
const urlCompare = a.url.localeCompare(b.url);
if (urlCompare !== 0) return urlCompare;
return a.method.localeCompare(b.method);
});
return reply.send({
success: true,
count: routes.length,
routes: routes,
userRoutes: routes.filter((r) => r.url.startsWith("/user")),
authRoutes: routes.filter((r) => r.url.startsWith("/auth")),
});
});
// Register maintenance mode check globally (before routes)
fastify.addHook("preHandler", checkMaintenance);
// Register auth routes WITHOUT /api prefix for Steam OAuth (external callback)
await fastify.register(authRoutes, { prefix: "/auth" });
// Register auth routes WITH /api prefix for frontend calls
await fastify.register(authRoutes, { prefix: "/api/auth" });
// Register other routes with /api prefix
await fastify.register(userRoutes, { prefix: "/api/user" });
await fastify.register(websocketRoutes);
await fastify.register(marketRoutes, { prefix: "/api/market" });
await fastify.register(inventoryRoutes, { prefix: "/api/inventory" });
await fastify.register(adminRoutes, { prefix: "/api/admin" });
await fastify.register(adminManagementRoutes, { prefix: "/api/admin" });
await fastify.register(configRoutes, { prefix: "/api/config" });
console.log("✅ All routes registered");
console.log("✅ Maintenance mode middleware active");
};
/**
* Setup error handlers
*/
const setupErrorHandlers = (fastify) => {
// Global error handler
fastify.setErrorHandler((error, request, reply) => {
fastify.log.error(error);
// Validation errors
if (error.validation) {
return reply.status(400).send({
error: "ValidationError",
message: "Invalid request data",
details: error.validation,
});
}
// Rate limit errors
if (error.statusCode === 429) {
return reply.status(429).send({
error: "TooManyRequests",
message: "Rate limit exceeded. Please try again later.",
});
}
// JWT errors
if (error.message && error.message.includes("jwt")) {
return reply.status(401).send({
error: "Unauthorized",
message: "Invalid or expired token",
});
}
// Default error response
const statusCode = error.statusCode || 500;
return reply.status(statusCode).send({
error: error.name || "InternalServerError",
message:
config.isProduction && statusCode === 500
? "An internal server error occurred"
: error.message,
...(config.isDevelopment && { stack: error.stack }),
});
});
// 404 handler
fastify.setNotFoundHandler((request, reply) => {
reply.status(404).send({
error: "NotFound",
message: `Route ${request.method} ${request.url} not found`,
});
});
console.log("✅ Error handlers configured");
};
/**
* Graceful shutdown handler
*/
const setupGracefulShutdown = (fastify) => {
const signals = ["SIGINT", "SIGTERM"];
signals.forEach((signal) => {
process.on(signal, async () => {
console.log(`\n🛑 Received ${signal}, starting graceful shutdown...`);
try {
// Close WebSocket connections
wsManager.closeAll();
// Close Fastify server
await fastify.close();
console.log("✅ Server closed gracefully");
process.exit(0);
} catch (error) {
console.error("❌ Error during shutdown:", error);
process.exit(1);
}
});
});
// Handle uncaught exceptions
process.on("uncaughtException", (error) => {
console.error("❌ Uncaught Exception:", error);
process.exit(1);
});
// Handle unhandled promise rejections
process.on("unhandledRejection", (reason, promise) => {
console.error("❌ Unhandled Rejection at:", promise, "reason:", reason);
process.exit(1);
});
console.log("✅ Graceful shutdown handlers configured");
};
/**
* Start the server
*/
const start = async () => {
try {
console.log("🚀 Starting TurboTrades Backend...\n");
// Connect to database
await connectDatabase();
// Create Fastify instance
const fastify = createServer();
// Add WebSocket manager to fastify instance
fastify.decorate("websocketManager", wsManager);
// Initialize Steam Bot Manager
const botManager = getSteamBotManager();
// Setup trade event listeners
botManager.on("tradeAccepted", async (offer, tradeData) => {
console.log(`✅ Trade ${offer.id} accepted! Crediting user...`);
try {
// Find the trade record
const trade = await Trade.findOne({ offerId: offer.id });
if (!trade) {
console.error(`❌ Trade record not found for offer ${offer.id}`);
return;
}
if (trade.state === "accepted") {
console.log(`⚠️ Trade ${offer.id} already completed, skipping`);
return;
}
// Find the user
const user = await User.findById(trade.userId);
if (!user) {
console.error(`❌ User not found for trade ${offer.id}`);
return;
}
// Credit user balance
user.balance += trade.totalValue;
await user.save();
// Update trade status
trade.state = "accepted";
trade.completedAt = new Date();
await trade.save();
// Create transaction record
const transaction = new Transaction({
user: user._id,
type: "sale",
amount: trade.totalValue,
description: `Sold ${trade.items.length} item(s)`,
status: "completed",
metadata: {
tradeId: trade._id,
offerId: offer.id,
botId: tradeData.botId,
itemCount: trade.items.length,
verificationCode: trade.verificationCode,
},
});
await transaction.save();
console.log(
`✅ Credited $${trade.totalValue.toFixed(2)} to user ${
user.username
} (Balance: $${user.balance.toFixed(2)})`
);
// Notify user via WebSocket
wsManager.sendToUser(user.steamId, {
type: "trade_completed",
data: {
tradeId: trade._id,
offerId: offer.id,
amount: trade.totalValue,
newBalance: user.balance,
itemCount: trade.items.length,
timestamp: Date.now(),
},
});
// Also send balance update
wsManager.sendToUser(user.steamId, {
type: "balance_update",
data: {
balance: user.balance,
change: trade.totalValue,
reason: "trade_completed",
},
});
} catch (error) {
console.error(`❌ Error processing accepted trade ${offer.id}:`, error);
}
});
botManager.on("tradeDeclined", async (offer, tradeData) => {
console.log(`❌ Trade ${offer.id} declined`);
try {
const trade = await Trade.findOne({ offerId: offer.id });
if (trade && trade.state === "pending") {
trade.state = "declined";
trade.completedAt = new Date();
await trade.save();
// Notify user
const user = await User.findById(trade.userId);
if (user) {
wsManager.sendToUser(user.steamId, {
type: "trade_declined",
data: {
tradeId: trade._id,
offerId: offer.id,
timestamp: Date.now(),
},
});
}
}
} catch (error) {
console.error(`❌ Error processing declined trade ${offer.id}:`, error);
}
});
botManager.on("tradeExpired", async (offer, tradeData) => {
console.log(`⏰ Trade ${offer.id} expired`);
try {
const trade = await Trade.findOne({ offerId: offer.id });
if (trade && trade.state === "pending") {
trade.state = "expired";
trade.completedAt = new Date();
await trade.save();
// Notify user
const user = await User.findById(trade.userId);
if (user) {
wsManager.sendToUser(user.steamId, {
type: "trade_expired",
data: {
tradeId: trade._id,
offerId: offer.id,
timestamp: Date.now(),
},
});
}
}
} catch (error) {
console.error(`❌ Error processing expired trade ${offer.id}:`, error);
}
});
botManager.on("tradeCanceled", async (offer, tradeData) => {
console.log(`🚫 Trade ${offer.id} canceled`);
try {
const trade = await Trade.findOne({ offerId: offer.id });
if (trade && trade.state === "pending") {
trade.state = "canceled";
trade.completedAt = new Date();
await trade.save();
}
} catch (error) {
console.error(`❌ Error processing canceled trade ${offer.id}:`, error);
}
});
botManager.on("botError", (error, botId) => {
console.error(`❌ Bot ${botId} error:`, error);
});
// Initialize bots if config exists
if (process.env.STEAM_BOT_AUTO_START === "true") {
try {
console.log("🤖 Auto-starting Steam bots...");
// You can load bot config from file or env vars
// const botsConfig = require("./config/steam-bots.json");
// await botManager.initialize(botsConfig);
console.log("⚠️ Bot auto-start enabled but no config found");
console.log(
" Configure bots in config/steam-bots.json or via env vars"
);
} catch (error) {
console.error("❌ Failed to initialize bots:", error.message);
}
} else {
console.log(
"🤖 Steam bots not auto-started (set STEAM_BOT_AUTO_START=true to enable)"
);
}
// Register plugins
await registerPlugins(fastify);
// Register routes
await registerRoutes(fastify);
// Setup error handlers
setupErrorHandlers(fastify);
// Setup graceful shutdown
setupGracefulShutdown(fastify);
// Start WebSocket heartbeat
wsManager.startHeartbeat(config.websocket.pingInterval);
// Start automatic price updates (every 1 hour)
if (!config.isDevelopment || process.env.ENABLE_PRICE_UPDATES === "true") {
console.log("⏰ Starting automatic price update scheduler...");
console.log("🔄 Running initial price update on startup...");
// Force immediate price update on launch
pricingService
.updateAllPrices()
.then((result) => {
console.log("✅ Initial price update completed successfully");
console.log(" 📊 MarketPrice Reference Database:");
console.log(
` CS2: ${
result.marketPrices.cs2.updated || 0
} prices updated, ${result.marketPrices.cs2.inserted || 0} new`
);
console.log(
` Rust: ${
result.marketPrices.rust.updated || 0
} prices updated, ${result.marketPrices.rust.inserted || 0} new`
);
console.log(" 📦 Marketplace Items:");
console.log(
` CS2: ${result.itemPrices.cs2.updated || 0} items updated`
);
console.log(
` Rust: ${result.itemPrices.rust.updated || 0} items updated`
);
})
.catch((error) => {
console.error("❌ Initial price update failed:", error.message);
console.error(" Scheduled updates will continue normally");
});
// Schedule recurring updates every hour
pricingService.scheduleUpdates(60 * 60 * 1000); // 1 hour
} else {
console.log("⏰ Automatic price updates disabled in development");
console.log(" Set ENABLE_PRICE_UPDATES=true to enable");
}
// Start listening
await fastify.listen({
port: config.port,
host: config.host,
});
console.log(`\n✅ Server running on http://${config.host}:${config.port}`);
console.log(
`📡 WebSocket available at ws://${config.host}:${config.port}/ws`
);
console.log(`🌍 Environment: ${config.nodeEnv}`);
console.log(
`🔐 Steam Login: http://${config.host}:${config.port}/auth/steam\n`
);
} catch (error) {
console.error("❌ Failed to start server:", error);
process.exit(1);
}
};
// Start the server
start();