407 lines
11 KiB
JavaScript
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();
|