All files / server/src/utils server.ts

78.68% Statements 48/61
59.37% Branches 19/32
100% Functions 5/5
78.68% Lines 48/61

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181                                            39x         213x   213x       213x   213x           213x                           213x   5x       213x     213x   213x 1x 1x 1x         213x 213x 213x   213x     213x     213x     213x   213x 213x                           213x 1x 1x                 213x     213x         1x         1x 1x     213x   213x   213x 213x               213x 213x         213x   213x     39x       208x     208x     208x     208x 208x   208x 208x         208x    
import http, { Server } from "node:http";
import path from "node:path";
import { once } from "node:events";
import express, { Request, Response, NextFunction } from "express";
import { setupExpressErrorHandler } from "@sentry/node";
import helmet from "helmet";
import expressStaticGzip from "express-static-gzip";
import { config } from "shared/config";
import { logger } from "server/utils/logger";
import { allowCORS } from "server/middleware/cors";
import "server/db/mongoosePlugins"; // Must be imported before apiRoutes which loads models
import { apiRoutes } from "server/api/apiRoutes";
import { db } from "server/db/mongodb";
import { stopCronJobs } from "server/utils/cron";
import { wwwRedirect } from "server/middleware/wwwRedirect";
import { sentryRoutes } from "server/api/sentryRoutes";
 
interface StartServerParams {
  dbConnString: string;
  port?: number;
  dbName?: string;
}
export const startServer = async ({
  dbConnString,
  port,
  dbName,
}: StartServerParams): Promise<Server> => {
  await db.connectToDb(dbConnString, dbName);
 
  const app = express();
 
  // Trust one hop of reverse proxy (k8s ingress / load balancer) so req.ip reads
  // X-Forwarded-For instead of the proxy's address. Harmless in dev.
  app.set("trust proxy", 1);
 
  const cspConnectSrc = [
    "'self'",
    "*.sentry.io",
    ...config.server().allowedCorsOrigins,
  ];
 
  app.use(
    helmet({
      contentSecurityPolicy: {
        directives: {
          "connect-src": cspConnectSrc,
          // Don't upgrade http to https when running CI playwright tests
          ...(process.env.SETTINGS === "ci" && {
            upgradeInsecureRequests: null,
          }),
        },
      },
    }),
  );
 
  Iif (process.env.NODE_ENV === "development") {
    // Kompassi mock service requires content type application/x-www-form-urlencoded
    app.use(express.urlencoded({ extended: true }));
  }
 
  // Accepts raw body
  app.use(sentryRoutes);
 
  // Parse body and populate req.body - only accepts JSON
  app.use(express.json({ limit: "1000kb", type: "*/*" })); // limit: 1MB
 
  app.use((err: Error, _req: Request, res: Response, next: NextFunction) => {
    Eif ("status" in err && err.status === 400) {
      logger.warn(`Invalid request: ${err.message}`);
      return res.sendStatus(400);
    }
    next(err);
  });
 
  app.use("/api", allowCORS);
  app.use("/auth", allowCORS);
  app.use(wwwRedirect);
 
  app.use(apiRoutes);
 
  // Set static path
  const staticPath = path.join(import.meta.dirname, "../../", "front");
 
  const serveIndexAndApi =
    !config.server().onlyCronjobs ||
    config.server().cronjobsAndBackendSameInstance;
 
  Eif (serveIndexAndApi) {
    // Set compression
    if (config.server().bundleCompression) {
      app.use(
        expressStaticGzip(staticPath, {
          enableBrotli: true,
          orderPreference: ["br", "gz"],
          serveStatic: {
            acceptRanges: false,
          },
        }),
      );
    } else E{
      app.use(express.static(staticPath, { acceptRanges: false }));
    }
  }
 
  app.get("/*splat", (req: Request, res: Response) => {
    if (req.originalUrl.includes("/api/")) {
      res.sendStatus(404);
    } else E{
      if (serveIndexAndApi) {
        res.sendFile(path.join(staticPath, "index.html"));
      }
    }
  });
 
  // Sentry setup: add this after all routes and before other error-handling middlewares
  setupExpressErrorHandler(app);
 
  // Error handler
  app.use((err: Error, _req: Request, res: Response, next: NextFunction) => {
    // Delegate to the default Express error handler, when the headers have already been sent to the client
    // For example, if error is encountered while streaming the response to the client
    // Express default error handler closes the connection and fails the request
    // https://expressjs.com/en/guide/error-handling.html
    Iif (res.headersSent) {
      logger.error(new Error("Error after headers sent", { cause: err }));
      next(err);
      return;
    }
    logger.error(err);
    return res.sendStatus(500);
  });
 
  const server = http.createServer(app);
 
  const runningServer = server.listen(port ?? process.env.PORT);
 
  try {
    await once(runningServer, "listening");
  } catch (error) {
    logger.warn("Starting server failed, shutting down...");
    await closeServer(server);
    // eslint-disable-next-line no-restricted-syntax -- Server startup
    throw error;
  }
 
  const address = runningServer.address();
  Iif (!address || typeof address === "string") {
    // eslint-disable-next-line no-restricted-syntax -- Server startup
    throw new Error("Unable to get address");
  }
 
  logger.info(`Express: Server started on port ${address.port}`);
 
  return runningServer;
};
 
export const closeServer = async (
  server: Server,
  signal?: string,
): Promise<void> => {
  logger.info(`Received signal to terminate${signal ? `: ${signal}` : ""}`);
 
  const enableCronjobs =
    config.server().onlyCronjobs ||
    config.server().cronjobsAndBackendSameInstance;
 
  Iif (enableCronjobs) {
    stopCronJobs();
  }
  server.close();
  logger.info("Server closed");
 
  try {
    await db.gracefulExit();
  } catch (error) {
    logger.error(error);
  }
 
  logger.info("Shutdown completed, bye");
};