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 { setSocketServer } from "@utils/socket-events";
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";
import { registerInvestigationCollaborationHandlers } from "./socket/investigation-collaboration";
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");
    const { swaggerRoutes } = await import("@routes/swagger.routes");
    logger.info("Building application modules...");
    const app = express();
    app.use(helmet({
        contentSecurityPolicy: {
            directives: {
                defaultSrc: ["'self'"],
                styleSrc: ["'self'", "'unsafe-inline'"],
                scriptSrc: ["'self'", "'unsafe-inline'"],
                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" }));
    app.use("/api-docs", swaggerRoutes);
    // 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: "*",
            },
        });
        // Expose Socket.IO instance for other parts of the app (e.g. controllers)
        setSocketServer(io);
        // 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;
                const userName = typeof socket.handshake.query.userName === "string"
                    ? socket.handshake.query.userName
                    : null;
                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);
                // Attach basic context to the socket for later handlers
                socket.data = {
                    userId,
                    userName,
                    investigationId,
                };
                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 });
                    }
                });
                // Collaboration / presence / field locks
                registerInvestigationCollaborationHandlers(io, socket, {
                    connectionId,
                    logger: connectionLogger,
                    investigationId,
                    userId,
                    userName,
                });
                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;
                            }
                            if (investigationId) {
                                const { fetchUserProfile } = await import("@utils/user-profile.utils");
                                const { AuthGrpcService } = await import("@services/auth.service");
                                const grpcClient = new AuthGrpcService();
                                const userProfile = await fetchUserProfile(userId, undefined, grpcClient);
                                const userAvatar = userProfile?.avatar || null;
                                socket.to(investigationId).emit("accept", {
                                    message: data.message,
                                    error: false,
                                    user: {
                                        id: userId,
                                        name: userName,
                                        avatar: userAvatar,
                                    },
                                });
                            }
                            if (investigationId) {
                                io.to(investigationId).emit("chat:typing", {
                                    investigationId,
                                    userId: "bot",
                                    userName: "Bot",
                                    isTyping: true,
                                });
                            }
                            const response = await chatService.continueConversation(data);
                            socket.emit("accept", response);
                            if (investigationId && response && !response.error) {
                                socket.to(investigationId).emit("accept", response);
                            }
                            if (investigationId) {
                                io.to(investigationId).emit("chat:typing", {
                                    investigationId,
                                    userId: "bot",
                                    userName: "Bot",
                                    isTyping: false,
                                });
                            }
                        }
                        catch {
                            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);
    }
}