All checks were successful
Build Frontend / Build Frontend (push) Successful in 24s
657 lines
19 KiB
JavaScript
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();
|