import express, { type Request, Response, NextFunction } from "express"; import { serveStatic } from "./static"; import { createServer } from "http"; import { initPluginLoader, getPluginLoader } from "./plugin-loader"; import { join } from "path"; import { initStatsTracker, getStatsTracker } from "./lib/stats-tracker"; const app = express(); const httpServer = createServer(app); declare module "http" { interface IncomingMessage { rawBody: unknown; } } app.use( express.json({ verify: (req, _res, buf) => { req.rawBody = buf; }, }), ); app.use(express.urlencoded({ extended: false })); export function log(message: string, source = "express") { const formattedTime = new Date().toLocaleTimeString("id-ID", { hour: "numeric", minute: "2-digit", second: "2-digit", hour12: true, }); console.log(`${formattedTime} [${source}] ${message}`); } interface RateLimitStore { [key: string]: { count: number; resetTime: number; }; } const rateLimitStore: RateLimitStore = {}; const RATE_LIMIT = 25; const WINDOW_MS = 60 * 1000; setInterval(() => { const now = Date.now(); Object.keys(rateLimitStore).forEach((key) => { if (rateLimitStore[key].resetTime < now) { delete rateLimitStore[key]; } }); }, 5 * 60 * 1000); app.use("/api", (req: Request, res: Response, next: NextFunction) => { const clientIp = req.ip || req.socket.remoteAddress || "unknown"; const now = Date.now(); if (!rateLimitStore[clientIp]) { rateLimitStore[clientIp] = { count: 1, resetTime: now + WINDOW_MS, }; return next(); } const clientData = rateLimitStore[clientIp]; if (now > clientData.resetTime) { clientData.count = 1; clientData.resetTime = now + WINDOW_MS; return next(); } clientData.count++; const remaining = Math.max(0, RATE_LIMIT - clientData.count); const resetInSeconds = Math.ceil((clientData.resetTime - now) / 1000); res.setHeader("X-RateLimit-Limit", RATE_LIMIT.toString()); res.setHeader("X-RateLimit-Remaining", remaining.toString()); res.setHeader("X-RateLimit-Reset", resetInSeconds.toString()); if (clientData.count > RATE_LIMIT) { log(`Rate limit exceeded for IP: ${clientIp}`, "rate-limit"); return res.status(429).json({ message: "Too many requests, please try again later.", retryAfter: resetInSeconds, }); } next(); }); app.use((req, res, next) => { const start = Date.now(); const path = req.path; let capturedJsonResponse: Record | undefined = undefined; const originalResJson = res.json; res.json = function(bodyJson, ...args) { capturedJsonResponse = bodyJson; return originalResJson.apply(res, [bodyJson, ...args]); }; res.on("finish", () => { const duration = Date.now() - start; if (path.startsWith("/api")) { let logLine = `${req.method} ${path} ${res.statusCode} in ${duration}ms`; if (capturedJsonResponse) { logLine += ` :: ${JSON.stringify(capturedJsonResponse, null, 2)}`; } log(logLine); const excludedPaths = [ '/api/plugins', '/api/stats', '/api/categories', '/docs' ]; const isPluginEndpoint = !excludedPaths.some(excluded => path.startsWith(excluded)); if (isPluginEndpoint) { const clientIp = req.ip || req.socket.remoteAddress || "unknown"; const tracked = getStatsTracker().trackRequest(path, res.statusCode, clientIp); if (!tracked) { log(`Failed request from ${clientIp} not tracked (limit exceeded)`, "stats"); } } } }); next(); }); (async () => { const statsFilePath = join(process.cwd(), "stats-data.json"); await initStatsTracker(statsFilePath); log("Stats tracker initialized with persistence"); const pluginsDir = join(process.cwd(), "src/server/plugins"); const pluginLoader = initPluginLoader(pluginsDir); const isDev = process.env.NODE_ENV === "development"; await pluginLoader.loadPlugins(app, isDev); app.get("/api/plugins", (_req, res) => { const metadata = getPluginLoader().getPluginMetadata(); res.json({ success: true, count: metadata.length, plugins: metadata, }); }); app.get("/api/plugins/category/:category", (req, res) => { const { category } = req.params; const allPlugins = getPluginLoader().getPluginMetadata(); const filtered = allPlugins.filter(p => p.category.includes(category) ); res.json({ success: true, category, count: filtered.length, plugins: filtered, }); }); app.get("/api/stats", (_req, res) => { const globalStats = getStatsTracker().getGlobalStats(); const topEndpoints = getStatsTracker().getTopEndpoints(5); res.json({ success: true, stats: { global: globalStats, topEndpoints, }, }); }); app.get("/api/stats/visitors", (req, res) => { const days = parseInt(req.query.days as string) || 30; const chartData = getStatsTracker().getVisitorChartData(days); res.json({ success: true, data: chartData, }); }); app.get("/api/categories", (_req, res) => { const allPlugins = getPluginLoader().getPluginMetadata(); const categoriesMap = new Map(); allPlugins.forEach(plugin => { plugin.category.forEach(cat => { categoriesMap.set(cat, (categoriesMap.get(cat) || 0) + 1); }); }); const categories = Array.from(categoriesMap.entries()).map(([name, count]) => ({ name, count, })); res.json({ success: true, categories, }); }); app.use((err: any, _req: Request, res: Response, _next: NextFunction) => { const status = err.status || err.statusCode || 500; const message = err.message || "Internal Server Error"; res.status(status).json({ message }); throw err; }); if (process.env.NODE_ENV === "production") { serveStatic(app); } else { const { setupVite } = await import("./vite"); await setupVite(httpServer, app); } app.use((req: Request, res: Response, next: NextFunction) => { if (req.path.startsWith("/api")) { return res.status(404).json({ message: "API endpoint not found", path: req.path, }); } next(); }); const port = parseInt(process.env.PORT || "7860", 10); httpServer.listen( { port, host: "0.0.0.0", reusePort: true, }, () => { log(`serving on port ${port}`); }, ); process.on('SIGTERM', async () => { log('SIGTERM received, saving stats...', 'shutdown'); await getStatsTracker().shutdown(); process.exit(0); }); process.on('SIGINT', async () => { log('SIGINT received, saving stats...', 'shutdown'); await getStatsTracker().shutdown(); process.exit(0); }); process.on('uncaughtException', async (error: Error) => { log(`Uncaught Exception: ${error.message}`, 'error'); console.error(error.stack); await getStatsTracker().shutdown(); }); process.on('unhandledRejection', async (reason: any, promise: Promise) => { log(`Unhandled Rejection at: ${promise}, reason: ${reason}`, 'error'); console.error(reason); await getStatsTracker().shutdown(); }); })();