Create nameserver API routes

Change-Id: I1f5400556eb556d86273c8662b03e8cd5406a61c
diff --git a/server/src/app.ts b/server/src/app.ts
index 53c8c23..161711c 100644
--- a/server/src/app.ts
+++ b/server/src/app.ts
@@ -23,6 +23,7 @@
 
 import { accountRouter } from './routers/account-router.js';
 import { authRouter } from './routers/auth-router.js';
+import { nameserverRouter } from './routers/nameserver-router.js';
 
 @Service()
 export class App {
@@ -36,6 +37,7 @@
     // Setup routing
     app.use('/auth', authRouter);
     app.use('/account', accountRouter);
+    app.use('/ns', nameserverRouter);
 
     // Setup 404 error handling
     app.use((_req, res) => {
diff --git a/server/src/jamid/jamid.ts b/server/src/jamid/jamid.ts
index a865777..ab1963f 100644
--- a/server/src/jamid/jamid.ts
+++ b/server/src/jamid/jamid.ts
@@ -17,7 +17,7 @@
  */
 import { AccountDetails, VolatileDetails } from 'jami-web-common';
 import log from 'loglevel';
-import { filter, firstValueFrom, Subject } from 'rxjs';
+import { filter, firstValueFrom, map, Subject } from 'rxjs';
 import { Service } from 'typedi';
 
 import { JamiSignal } from './jami-signal.js';
@@ -133,8 +133,9 @@
     const accountId = this.jamiSwig.addAccount(detailsStringMap);
     return firstValueFrom(
       this.events.onRegistrationStateChanged.pipe(
-        filter(({ accountId: addedAccountId }) => addedAccountId === accountId),
+        filter((value) => value.accountId === accountId),
         // TODO: is it the only state?
+        // TODO: Replace with string enum in common/
         filter(({ state }) => state === 'REGISTERED')
       )
     );
@@ -154,26 +155,38 @@
     this.jamiSwig.sendAccountTextMessage(accountId, contactId, messageStringMap);
   }
 
-  async lookupUsername(username: string) {
-    const hasRingNs = this.jamiSwig.lookupName('', '', username);
+  async lookupUsername(username: string, accountId?: string) {
+    const hasRingNs = this.jamiSwig.lookupName(accountId || '', '', username);
     if (!hasRingNs) {
-      log.error('Jami does not have NS');
       throw new Error('Jami does not have NS');
     }
-    return firstValueFrom(this.events.onRegisteredNameFound.pipe(filter((r) => r.username === username)));
+    return firstValueFrom(
+      this.events.onRegisteredNameFound.pipe(
+        filter((value) => value.username === username),
+        map(({ accountId: _, ...response }) => response) // Remove accountId from response
+      )
+    );
+  }
+
+  async lookupAddress(address: string, accountId?: string) {
+    const hasRingNs = this.jamiSwig.lookupAddress(accountId || '', '', address);
+    if (!hasRingNs) {
+      throw new Error('Jami does not have NS');
+    }
+    return firstValueFrom(
+      this.events.onRegisteredNameFound.pipe(
+        filter((value) => value.address === address),
+        map(({ accountId: _, ...response }) => response) // Remove accountId from response
+      )
+    );
   }
 
   async registerUsername(accountId: string, username: string, password: string) {
     const hasRingNs = this.jamiSwig.registerName(accountId, password, username);
     if (!hasRingNs) {
-      log.error('Jami does not have NS');
       throw new Error('Jami does not have NS');
     }
-    return firstValueFrom(
-      this.events.onNameRegistrationEnded.pipe(
-        filter(({ accountId: registeredAccountId }) => registeredAccountId === accountId)
-      )
-    );
+    return firstValueFrom(this.events.onNameRegistrationEnded.pipe(filter((value) => value.accountId === accountId)));
   }
 
   getDevices(accountId: string): Record<string, string> {
diff --git a/server/src/middleware/auth.ts b/server/src/middleware/auth.ts
index c6992d3..70eca75 100644
--- a/server/src/middleware/auth.ts
+++ b/server/src/middleware/auth.ts
@@ -22,29 +22,41 @@
 
 import { Vault } from '../vault.js';
 
-export async function authenticateToken(req: Request, res: Response, next: NextFunction) {
-  const publicKey = Container.get(Vault).publicKey;
+function createAuthenticationMiddleware(isAuthenticationRequired: boolean) {
+  return async (req: Request, res: Response, next: NextFunction) => {
+    const publicKey = Container.get(Vault).publicKey;
 
-  const authorizationHeader = req.headers.authorization;
-  if (!authorizationHeader) {
-    res.status(HttpStatusCode.Unauthorized).send('Missing Authorization header');
-    return;
-  }
+    const authorizationHeader = req.headers.authorization;
+    if (!authorizationHeader) {
+      if (isAuthenticationRequired) {
+        res.status(HttpStatusCode.Unauthorized).send('Missing Authorization header');
+      } else {
+        // Skip authentication if it is optional, in which case the Authorization header should not have been set
+        res.locals.accountId = undefined;
+        next();
+      }
+      return;
+    }
 
-  const token = authorizationHeader.split(' ')[1];
-  if (token === undefined) {
-    res.status(HttpStatusCode.BadRequest).send('Missing JSON web token');
-    return;
-  }
+    const token = authorizationHeader.split(' ')[1];
+    if (token === undefined) {
+      res.status(HttpStatusCode.BadRequest).send('Missing JSON web token');
+      return;
+    }
 
-  try {
-    const { payload } = await jwtVerify(token, publicKey, {
-      issuer: 'urn:example:issuer',
-      audience: 'urn:example:audience',
-    });
-    res.locals.accountId = payload.id as string;
-    next();
-  } catch (err) {
-    res.sendStatus(HttpStatusCode.Unauthorized);
-  }
+    try {
+      const { payload } = await jwtVerify(token, publicKey, {
+        issuer: 'urn:example:issuer',
+        audience: 'urn:example:audience',
+      });
+      res.locals.accountId = payload.id as string;
+      next();
+    } catch (err) {
+      res.sendStatus(HttpStatusCode.Unauthorized);
+    }
+  };
 }
+
+export const authenticateToken = createAuthenticationMiddleware(true);
+
+export const authenticateOptionalToken = createAuthenticationMiddleware(false);
diff --git a/server/src/routers/nameserver-router.ts b/server/src/routers/nameserver-router.ts
new file mode 100644
index 0000000..6e49b52
--- /dev/null
+++ b/server/src/routers/nameserver-router.ts
@@ -0,0 +1,66 @@
+/*
+ * 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 { Router } from 'express';
+import asyncHandler from 'express-async-handler';
+import { HttpStatusCode } from 'jami-web-common';
+import { Container } from 'typedi';
+
+import { Jamid } from '../jamid/jamid.js';
+import { authenticateOptionalToken } from '../middleware/auth.js';
+
+const jamid = Container.get(Jamid);
+
+export const nameserverRouter = Router();
+
+nameserverRouter.use(authenticateOptionalToken);
+
+nameserverRouter.get(
+  '/username/:username',
+  asyncHandler(async (req, res) => {
+    const result = await jamid.lookupUsername(req.params.username, res.locals.accountId);
+    switch (result.state) {
+      case 0:
+        res.json(result);
+        break;
+      case 1:
+        res.sendStatus(HttpStatusCode.BadRequest);
+        break;
+      default:
+        res.sendStatus(HttpStatusCode.NotFound);
+        break;
+    }
+  })
+);
+
+nameserverRouter.get(
+  '/address/:address',
+  asyncHandler(async (req, res) => {
+    const result = await jamid.lookupAddress(req.params.address, res.locals.accountId);
+    switch (result.state) {
+      case 0:
+        res.json(result);
+        break;
+      case 1:
+        res.sendStatus(HttpStatusCode.BadRequest);
+        break;
+      default:
+        res.sendStatus(HttpStatusCode.NotFound);
+        break;
+    }
+  })
+);