Migrate server setup functionality

Changes:
- Check if admin is set up
- Redirect to admin setup if not already done
- Create admin password
- Login as admin and get access token for future requests
- Add a router for setup
- Middleware to prevent any route from being accessible on the server till the admin setup is done

GitLab: #80
GitLab: #73

Change-Id: I8b7ecab68f6b4d5c6313ce2e72a4ae4fdef9eda0
diff --git a/server/src/routers/setup-router.ts b/server/src/routers/setup-router.ts
new file mode 100644
index 0000000..d155ef5
--- /dev/null
+++ b/server/src/routers/setup-router.ts
@@ -0,0 +1,96 @@
+/*
+ * 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 argon2 from 'argon2';
+import { Router } from 'express';
+import asyncHandler from 'express-async-handler';
+import { ParamsDictionary, Request } from 'express-serve-static-core';
+import { HttpStatusCode } from 'jami-web-common';
+import { SignJWT } from 'jose';
+import { Container } from 'typedi';
+
+import { AdminConfig } from '../admin-config.js';
+import { checkAdminSetup } from '../middleware/setup.js';
+import { Vault } from '../vault.js';
+
+export const setupRouter = Router();
+
+const vault = Container.get(Vault);
+const adminConfig = Container.get(AdminConfig);
+
+setupRouter.get('/check', (_req, res, _next) => {
+  const isSetupComplete = adminConfig.get() !== undefined;
+  res.send({ isSetupComplete });
+});
+
+setupRouter.post(
+  '/admin/create',
+  asyncHandler(async (req: Request<ParamsDictionary, string, { password?: string }>, res, _next) => {
+    const isAdminCreated = adminConfig.get() !== undefined;
+    if (isAdminCreated) {
+      res.sendStatus(HttpStatusCode.BadRequest);
+      return;
+    }
+
+    const { password } = req.body;
+    if (!password) {
+      res.status(HttpStatusCode.BadRequest).send('Missing password');
+      return;
+    }
+
+    const hashedPassword = await argon2.hash(password, { type: argon2.argon2id });
+    adminConfig.set(hashedPassword);
+    await adminConfig.save();
+
+    res.sendStatus(HttpStatusCode.Created);
+  })
+);
+
+// Every request handler after this line will be submitted to this middleware
+// in order to ensure that the admin account is set up before proceeding with
+// setup related requests.
+setupRouter.use(checkAdminSetup);
+
+setupRouter.post(
+  '/admin/login',
+  asyncHandler(
+    async (req: Request<ParamsDictionary, { accessToken: string } | string, { password: string }>, res, _next) => {
+      const { password } = req.body;
+      if (!password) {
+        res.status(HttpStatusCode.BadRequest).send('Missing password');
+        return;
+      }
+
+      const hashedPassword = adminConfig.get();
+      const isPasswordVerified = await argon2.verify(hashedPassword, password);
+      if (!isPasswordVerified) {
+        res.sendStatus(HttpStatusCode.Forbidden);
+        return;
+      }
+
+      const jwt = await new SignJWT({ id: 'admin' })
+        .setProtectedHeader({ alg: 'EdDSA' })
+        .setIssuedAt()
+        // TODO: use valid issuer and audience
+        .setIssuer('urn:example:issuer')
+        .setAudience('urn:example:audience')
+        .setExpirationTime('2h')
+        .sign(vault.privateKey);
+      res.send({ accessToken: jwt });
+    }
+  )
+);