Add Express.js boilerplate and consistent error handling middleware

Changes:
- Convert router.ts to app.ts and routers/auth-router.ts to separate routers from Express.js app
- Remove signal listeners as they do not work for the Jami daemon
- Add 'error' and 'listening' event listeners to the HTTP server to show useful info on startup
- Add error handling middleware to return error strings rather than HTML

Change-Id: Icc0410a9fc596a419be024c6b0503ca4183f7563
diff --git a/server/src/app.ts b/server/src/app.ts
new file mode 100644
index 0000000..f126ed4
--- /dev/null
+++ b/server/src/app.ts
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2022 Savoir-faire Linux Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this program.  If not, see
+ * <https://www.gnu.org/licenses/>.
+ */
+import express, { NextFunction, Request, Response } from 'express';
+import log from 'loglevel';
+import { Service } from 'typedi';
+
+import { StatusCode } from './constants.js';
+import { AuthRouter } from './routers/auth-router.js';
+
+@Service()
+class App {
+  constructor(private authRouter: AuthRouter) {}
+
+  async build() {
+    const app = express();
+
+    // Setup routing
+    const authRouter = await this.authRouter.build();
+    app.use('/auth', authRouter);
+
+    // Setup 404 error handling
+    app.use((_req, res) => {
+      res.sendStatus(StatusCode.NOT_FOUND);
+    });
+
+    // Setup internal error handling
+    app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
+      log.error(err);
+      res.status(StatusCode.INTERNAL_SERVER_ERROR).send(err.message);
+    });
+
+    return app;
+  }
+}
+
+export { App };
diff --git a/server/src/index.ts b/server/src/index.ts
index 587438c..661693f 100644
--- a/server/src/index.ts
+++ b/server/src/index.ts
@@ -22,31 +22,48 @@
 import log from 'loglevel';
 import { Container } from 'typedi';
 
-import { Router } from './router.js';
+import { App } from './app.js';
 import { Ws } from './ws.js';
 
 log.setLevel(process.env.NODE_ENV === 'production' ? 'error' : 'trace');
 
-const app = await Container.get(Router).build();
+const port: string | number = 5000;
+
+const app = await Container.get(App).build();
 const wss = await Container.get(Ws).build();
 
-// Disable HTTP 1.1 Keep-Alive
-const server = createServer((_, res) => res.setHeader('Connection', 'close'));
+const server = createServer();
+
 server.on('request', app);
 server.on('upgrade', wss);
-server.listen({
-  host: '0.0.0.0',
-  port: 5000,
-  exclusive: true,
-});
 
-log.debug('Server started (HTTP + WS)');
+server.listen(port);
+server.on('error', onError);
+server.on('listening', onListening);
 
-const closeFn: NodeJS.SignalsListener = (signal) => {
-  log.info(signal);
-  server.close();
-  log.info('server closed');
-};
-process.once('SIGTERM', closeFn);
-process.once('SIGHUP', closeFn);
-process.once('SIGINT', closeFn);
+function onError(error: NodeJS.ErrnoException) {
+  if (error.syscall !== 'listen') {
+    throw error;
+  }
+
+  const bind = typeof port === 'string' ? `Pipe ${port}` : `Port ${port}`;
+
+  switch (error.code) {
+    case 'EACCESS':
+      log.error(bind + ' requires elevated privileges');
+      process.exit(1);
+      break;
+    case 'EADDRINUSE':
+      log.error(bind + ' is already in use');
+      process.exit(1);
+      break;
+    default:
+      throw error;
+  }
+}
+
+function onListening() {
+  const address = server.address();
+  const bind = typeof address === 'string' ? `pipe ${address}` : `port ${address?.port}`;
+  log.debug('Listening on ' + bind);
+}
diff --git a/server/src/router.ts b/server/src/routers/auth-router.ts
similarity index 80%
rename from server/src/router.ts
rename to server/src/routers/auth-router.ts
index 0b00780..dec19b3 100644
--- a/server/src/router.ts
+++ b/server/src/routers/auth-router.ts
@@ -15,20 +15,16 @@
  * License along with this program.  If not, see
  * <https://www.gnu.org/licenses/>.
  */
-import express, { json, Router as ERouter } from 'express';
+import { Router } from 'express';
 import asyncHandler from 'express-async-handler';
 import { Service } from 'typedi';
 
-import { StatusCode } from './constants.js';
+import { StatusCode } from '../constants.js';
 
 @Service()
-class Router {
+class AuthRouter {
   async build() {
-    const router = ERouter();
-
-    router.use(json());
-
-    await Promise.resolve(42);
+    const router = Router();
 
     router.post(
       '/new-account',
@@ -46,10 +42,8 @@
       })
     );
 
-    const app = express();
-    app.use('/', router);
-    return app;
+    return router;
   }
 }
 
-export { Router };
+export { AuthRouter };