Reorganize server files and address TODO comments

Changes:
- Remove unneeded dependencies from package.json
- Remove unneeded async build() methods from services
    - Use constructor as often as possible
- Rename and move storage services for clarity
    - creds.ts -> accounts.ts, and creds.json -> accounts.json
    - admin-config.ts -> admin-account.ts
    - vault.ts -> signing-keys.ts
- Rename ws.ts to websocket-server.ts for clarity and consistency
- Make WebSocketServer initialize using constructor and bind server upgrade to WebSocketServer.upgrade
- Remove unused send-account-message endpoint from account-router.ts
- Set issuer and audience claims for JWT
- Create new utils/jwt.ts file to remove code duplication for JWT signing and verifying
- Delete utils.ts and merge it with jami-swig.ts
- Handle potentially undefined types in jami-swig.ts
- Replace hard to read one-liners with functions in jami-swig.ts
- Rename types in jami-swig.ts for consistency with daemon
- Remove handled/answered TODO comments
- Remove TODO comment about using .env for jamid.node as it does not work for require()

GitLab: #87
Change-Id: I1e5216ffa79ea34dd7e9b61540fb7e37d1f66c9f
diff --git a/server/src/websocket/webrtc-handler.ts b/server/src/websocket/webrtc-handler.ts
new file mode 100644
index 0000000..e6ebcbc
--- /dev/null
+++ b/server/src/websocket/webrtc-handler.ts
@@ -0,0 +1,41 @@
+/*
+ * 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 { AccountTextMessage, WebSocketMessageType } from 'jami-web-common';
+import log from 'loglevel';
+import { Container } from 'typedi';
+
+import { Jamid } from '../jamid/jamid.js';
+import { WebSocketServer } from './websocket-server.js';
+
+const jamid = Container.get(Jamid);
+const webSocketServer = Container.get(WebSocketServer);
+
+function sendWebRTCData<T>(data: Partial<AccountTextMessage<T>>) {
+  if (data.from === undefined || data.to === undefined || data.message === undefined) {
+    log.warn('Message is not a valid AccountTextMessage (missing from, to, or message fields)');
+    return;
+  }
+
+  jamid.sendAccountTextMessage(data.from, data.to, JSON.stringify(data.message));
+}
+
+export function bindWebRTCCallbacks() {
+  webSocketServer.bind(WebSocketMessageType.WebRTCOffer, sendWebRTCData);
+  webSocketServer.bind(WebSocketMessageType.WebRTCAnswer, sendWebRTCData);
+  webSocketServer.bind(WebSocketMessageType.IceCandidate, sendWebRTCData);
+}
diff --git a/server/src/websocket/websocket-server.ts b/server/src/websocket/websocket-server.ts
new file mode 100644
index 0000000..6750129
--- /dev/null
+++ b/server/src/websocket/websocket-server.ts
@@ -0,0 +1,127 @@
+/*
+ * 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 { IncomingMessage } from 'node:http';
+import { Duplex } from 'node:stream';
+
+import { WebSocketCallbacks, WebSocketMessage, WebSocketMessageTable, WebSocketMessageType } from 'jami-web-common';
+import log from 'loglevel';
+import { Service } from 'typedi';
+import { URL } from 'whatwg-url';
+import * as WebSocket from 'ws';
+
+import { verifyJwt } from '../utils/jwt.js';
+
+@Service()
+export class WebSocketServer {
+  private wss = new WebSocket.WebSocketServer({ noServer: true });
+  private sockets = new Map<string, WebSocket.WebSocket[]>();
+  private callbacks: WebSocketCallbacks = {
+    [WebSocketMessageType.ConversationMessage]: [],
+    [WebSocketMessageType.ConversationView]: [],
+    [WebSocketMessageType.WebRTCOffer]: [],
+    [WebSocketMessageType.WebRTCAnswer]: [],
+    [WebSocketMessageType.IceCandidate]: [],
+  };
+
+  constructor() {
+    this.wss.on('connection', (ws: WebSocket.WebSocket, _request: IncomingMessage, accountId: string) => {
+      log.info('New connection for account', accountId);
+      const accountSockets = this.sockets.get(accountId);
+      if (accountSockets) {
+        accountSockets.push(ws);
+      } else {
+        this.sockets.set(accountId, [ws]);
+      }
+
+      ws.on('message', <T extends WebSocketMessageType>(messageString: string) => {
+        const message: WebSocketMessage<T> = JSON.parse(messageString);
+        if (message.type === undefined || message.data === undefined) {
+          log.warn('WebSocket message is not a valid WebSocketMessage (missing type or data fields)');
+          return;
+        }
+
+        if (!Object.values(WebSocketMessageType).includes(message.type)) {
+          log.warn(`Invalid WebSocket message type: ${message.type}`);
+          return;
+        }
+
+        const callbacks = this.callbacks[message.type];
+        for (const callback of callbacks) {
+          callback(message.data);
+        }
+      });
+
+      ws.on('close', () => {
+        log.info('Closing connection for account', accountId);
+        const accountSockets = this.sockets.get(accountId);
+        if (accountSockets === undefined) {
+          return;
+        }
+
+        const index = accountSockets.indexOf(ws);
+        if (index !== -1) {
+          accountSockets.splice(index, 1);
+          if (accountSockets.length === 0) {
+            this.sockets.delete(accountId);
+          }
+        }
+      });
+    });
+  }
+
+  async upgrade(request: IncomingMessage, socket: Duplex, head: Buffer): Promise<void> {
+    // Do not use parseURL because it returns a URLRecord and not a URL
+    const url = new URL(request.url ?? '/', 'http://localhost/');
+    const token = url.searchParams.get('accessToken') ?? undefined;
+    if (token === undefined) {
+      socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
+      socket.destroy();
+      return;
+    }
+
+    try {
+      const { payload } = await verifyJwt(token);
+      const accountId = payload.accountId as string;
+      log.info('Authentication successful for account', accountId);
+      this.wss.handleUpgrade(request, socket, head, (ws) => {
+        this.wss.emit('connection', ws, request, accountId);
+      });
+    } catch (e) {
+      log.debug('Authentication failed:', e);
+      socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
+      socket.destroy();
+    }
+  }
+
+  bind<T extends WebSocketMessageType>(type: T, callback: (data: WebSocketMessageTable[T]) => void): void {
+    this.callbacks[type].push(callback);
+  }
+
+  send<T extends WebSocketMessageType>(accountId: string, type: T, data: WebSocketMessageTable[T]): boolean {
+    const accountSockets = this.sockets.get(accountId);
+    if (accountSockets === undefined) {
+      return false;
+    }
+
+    const webSocketMessageString = JSON.stringify({ type, data });
+    for (const accountSocket of accountSockets) {
+      accountSocket.send(webSocketMessageString);
+    }
+    return true;
+  }
+}