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();