first commit
This commit is contained in:
406
index.js
Normal file
406
index.js
Normal file
@@ -0,0 +1,406 @@
|
||||
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();
|
||||
Reference in New Issue
Block a user