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/.gitignore b/server/.gitignore
index efc99e0..12fee05 100644
--- a/server/.gitignore
+++ b/server/.gitignore
@@ -1 +1,2 @@
 creds.json
+admin.json
\ No newline at end of file
diff --git a/server/src/admin-config.ts b/server/src/admin-config.ts
new file mode 100644
index 0000000..b52d608
--- /dev/null
+++ b/server/src/admin-config.ts
@@ -0,0 +1,52 @@
+/*
+ * 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 { readFileSync } from 'node:fs';
+import { writeFile } from 'node:fs/promises';
+
+import { Service } from 'typedi';
+
+@Service()
+export class AdminConfig {
+  private readonly file = 'admin.json';
+  private account: { admin: string };
+
+  constructor() {
+    let buffer: Buffer;
+
+    try {
+      buffer = readFileSync(this.file);
+    } catch (e) {
+      console.error(e);
+      buffer = Buffer.from('{}');
+    }
+
+    this.account = JSON.parse(buffer.toString());
+  }
+
+  get() {
+    return this.account.admin;
+  }
+
+  set(password: string) {
+    this.account.admin = password;
+  }
+
+  async save() {
+    await writeFile(this.file, JSON.stringify(this.account) + '/n');
+  }
+}
diff --git a/server/src/app.ts b/server/src/app.ts
index 4c1914b..f6086cd 100644
--- a/server/src/app.ts
+++ b/server/src/app.ts
@@ -23,6 +23,7 @@
 import { Service } from 'typedi';
 
 import { bindWebRTCCallbacks } from './handlers/webrtc-handler.js';
+import { checkAdminSetup } from './middleware/setup.js';
 import { accountRouter } from './routers/account-router.js';
 import { authRouter } from './routers/auth-router.js';
 import { callRouter } from './routers/call-router.js';
@@ -30,6 +31,7 @@
 import { conversationRouter } from './routers/conversation-router.js';
 import { defaultModeratorsRouter } from './routers/default-moderators-router.js';
 import { nameserverRouter } from './routers/nameserver-router.js';
+import { setupRouter } from './routers/setup-router.js';
 
 @Service()
 export class App {
@@ -41,6 +43,10 @@
     app.use(cors());
     app.use(json());
 
+    // Enforce admin setup
+    app.use('/setup', setupRouter);
+    app.use(checkAdminSetup);
+
     // Setup routing
     app.use('/auth', authRouter);
     app.use('/account', accountRouter);
diff --git a/server/src/middleware/setup.ts b/server/src/middleware/setup.ts
new file mode 100644
index 0000000..3575fd0
--- /dev/null
+++ b/server/src/middleware/setup.ts
@@ -0,0 +1,34 @@
+/*
+ * 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 { NextFunction, Request, Response } from 'express';
+import { HttpStatusCode } from 'jami-web-common';
+import { Container } from 'typedi';
+
+import { AdminConfig } from '../admin-config.js';
+
+const adminConfig = Container.get(AdminConfig);
+
+export async function checkAdminSetup(_req: Request, res: Response, next: NextFunction) {
+  const isSetupComplete = adminConfig.get() !== undefined;
+
+  if (!isSetupComplete) {
+    res.sendStatus(HttpStatusCode.Forbidden);
+    return;
+  }
+  next();
+}
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 });
+    }
+  )
+);