Files
TurboTrades/index.js
2026-01-10 04:57:43 +00:00

407 lines
11 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 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 services
import pricingService from "./services/pricing.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 requests from file:// protocol for test client
await fastify.register(fastifyCors, {
origin: (origin, callback) => {
// Allow requests from file:// protocol (local HTML files)
if (!origin || origin === "null" || origin === config.cors.origin) {
callback(null, true);
return;
}
// In development, allow localhost on any port
if (config.isDevelopment && origin.includes("localhost")) {
callback(null, true);
return;
}
// Otherwise, check if it matches configured origin
if (origin === config.cors.origin) {
callback(null, true);
} else {
callback(new Error("Not allowed by CORS"), false);
}
},
credentials: true,
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
allowedHeaders: ["Content-Type", "Authorization", "Cookie"],
exposedHeaders: ["Set-Cookie"],
});
// 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 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" });
console.log("✅ All routes registered");
};
/**
* 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);
// 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(` CS2: ${result.cs2.updated || 0} items updated`);
console.log(` Rust: ${result.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();