Source

app.js

/**
 * @module app
 */
import { env } from "@config/env";
import { globalLogger } from "@config/logger";
import { ChatService } from "@services/chat.service";
import { asyncLocalStorage, getLogger } from "@utils/asyncLocalStorage";
import { initializeVault, closeVault } from "@vault/index";
import cors from "cors";
import express from "express";
import helmet from "helmet";
import { createServer } from "http";
import { ObjectId } from "mongodb";
import morgan from "morgan";
import { randomUUID } from "node:crypto";
import { Server } from "socket.io";
import { initializeDatabases, closeDatabases } from "./database";
import { errorHandler, notFoundHandler } from "./middlewares/errorHandler";
import { requestIdMiddleware } from "./middlewares/requestId";
const logger = getLogger();
/**
 * Create the Express application.
 * @returns {Promise<Application>} The Express application.
 */
export async function createApp() {
    const { investigationRoutes } = await import("@routes/investigation.routes");
    const { healthRoutes } = await import("@routes/health.routes");
    logger.info("Building application modules...");
    const app = express();
    app.use(helmet({
        contentSecurityPolicy: {
            directives: {
                defaultSrc: ["'self'"],
                styleSrc: ["'self'", "'unsafe-inline'"],
                scriptSrc: ["'self'"],
                imgSrc: ["'self'", "data:", "https:"],
            },
        },
    }));
    app.use(cors({
        origin: "*",
        credentials: false,
        allowedHeaders: ["Content-Type", "Authorization", "x-request-id"],
        methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
    }));
    app.use(requestIdMiddleware);
    morgan.token("request-id", (req) => {
        const extReq = req;
        const bindings = extReq.logger?.bindings?.();
        return bindings?.requestId || "no-request-id";
    });
    app.use(morgan(':remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent" [requestId: :request-id]', {
        stream: {
            write: (message) => {
                logger.info(message.trim());
            },
        },
    }));
    app.use(express.json({ limit: "10mb" }));
    // API Routes
    app.use("/api/health", healthRoutes);
    app.use("/api/investigation", investigationRoutes);
    // 404 handler
    app.use(notFoundHandler);
    app.use(errorHandler);
    logger.info("Application modules configured successfully");
    return app;
}
/**
 * Start the application server.
 * @returns {Promise<void>} A promise that resolves when the server is started.
 */
export async function startApp() {
    logger.info("Starting Backend Core...");
    try {
        await initializeDatabases();
        try {
            await initializeVault();
        }
        catch (error) {
            logger.error({ error }, "Error initializing Vault");
        }
        const app = await createApp();
        const server = app.listen(env.PORT, () => {
            logger.info({
                port: env.PORT,
                environment: env.NODE_ENV,
                nodeVersion: process.version,
            }, "Server started successfully");
        });
        const httpServer = createServer();
        /* Socket.io server */
        const io = new Server(httpServer, {
            cors: {
                origin: "*",
            },
        });
        // Socket error handler
        io.on("error", (error) => {
            logger.error({
                error: error instanceof Error ? error.message : "Unknown error",
            }, "Socket error occurred");
        });
        io.on("connection", (socket) => {
            try {
                const connectionId = randomUUID();
                const connectionLogger = globalLogger.child({ connectionId, socketId: socket.id });
                connectionLogger.info(`Client connected: ${socket.id}`);
                const investigationId = socket.handshake.query.investigationId;
                const userId = socket.handshake.query.userId;
                connectionLogger.info(`USER ID: ${userId}`);
                if (!userId || typeof userId !== "string" || !ObjectId.isValid(userId)) {
                    connectionLogger.debug({
                        message: "User ID is invalid.",
                        userId,
                    });
                    socket.emit("accept", { message: "User ID is invalid.", error: true });
                    return;
                }
                const userObjectId = new ObjectId(userId);
                let chatService;
                if (investigationId) {
                    const investigationObjectId = new ObjectId(investigationId);
                    chatService = new ChatService(investigationObjectId, userObjectId);
                }
                else {
                    chatService = new ChatService(null, userObjectId);
                }
                void asyncLocalStorage.run({ logger: connectionLogger, requestId: connectionId }, async () => {
                    try {
                        const startConversationResult = await chatService.startConversation();
                        socket.emit("accept", startConversationResult);
                    }
                    catch (error) {
                        const logger = getLogger();
                        logger.error({ error }, "Error starting conversation");
                        socket.emit("accept", { message: "Error in socket connection.", error: true });
                    }
                });
                socket.on("message", (data) => {
                    void asyncLocalStorage.run({ logger: connectionLogger, requestId: connectionId }, async () => {
                        try {
                            if (!data.message) {
                                socket.emit("accept", { message: "Message must be exists.", error: true });
                                return;
                            }
                            const response = await chatService.continueConversation(data);
                            socket.emit("accept", response);
                        }
                        catch (error) {
                            const logger = getLogger();
                            logger.error({ error }, "Error processing message");
                            socket.emit("accept", { message: "Error processing message.", error: true });
                        }
                    });
                });
                socket.on("disconnect", () => {
                    connectionLogger.info(`Client disconnected: ${socket.id}`);
                });
            }
            catch (error) {
                logger.error({ error }, "Error in socket connection");
                socket.emit("accept", { message: "Error in socket connection.", error: true });
                return;
            }
        });
        /* Start the server */
        httpServer.listen(env.SOCKET_PORT, () => {
            logger.info(`WebSocket server running at ws://localhost:${env.SOCKET_PORT}/`);
        });
        const gracefulShutdown = async (signal) => {
            logger.info(`Received ${signal}. Starting graceful shutdown...`);
            const forceShutdown = setTimeout(() => {
                logger.error("Could not close connections in time, forcefully shutting down");
                process.exit(1);
            }, 10000);
            try {
                server.close(() => {
                    logger.info("HTTP server closed");
                });
                closeVault();
                await closeDatabases();
                clearTimeout(forceShutdown);
                logger.info("Graceful shutdown completed");
                process.exit(0);
            }
            catch (error) {
                logger.error({ error }, "Error during graceful shutdown");
                clearTimeout(forceShutdown);
                process.exit(1);
            }
        };
        const handleSignal = (signal) => {
            gracefulShutdown(signal).catch((error) => {
                logger.error({ error, signal }, "Unhandled error in signal handler");
                process.exit(1);
            });
        };
        process.on("SIGTERM", () => handleSignal("SIGTERM"));
        process.on("SIGINT", () => handleSignal("SIGINT"));
    }
    catch (error) {
        logger.fatal({
            error: error instanceof Error ? error.message : "Unknown error",
        }, "Failed to start application");
        try {
            closeVault();
            await closeDatabases();
        }
        catch (closeError) {
            logger.error({ closeError }, "Error closing services during startup failure");
        }
        process.exit(1);
    }
}