| 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<string, any> | 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<string, number>(); | |
| 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<any>) => { | |
| log(`Unhandled Rejection at: ${promise}, reason: ${reason}`, 'error'); | |
| console.error(reason); | |
| await getStatsTracker().shutdown(); | |
| }); | |
| })(); |