Create new interfaces for objects transmitted using the REST API

Changes:
- Create new IContact, IAccount, and IConversation interfaces in common/
    - These interfaces represent the serialized versions of the models which are transferred
    - The client models are classes which implement these interfaces
- Create new LookupResult interface for nameserver lookup results
- Create new IConversationMember interface for conversation members
    - The client interface ConversationMember extends this interface to have a Contact field rather than IContact
- Create new ConversationInfos interface for conversation infos
- Create new ContactDetails interface for contact details (used by contacts routes)
- Move request and response body interfaces into common/
- Merge AccountConfig into AccountDetails interface
- Create interfaces for server-only objects:
    - ConversationMemberInfos
    - ConversationRequestMetadata
- Ensure interfaces in jami-signal-interfaces.ts do not contain fields with JamiSwig types
- Rename models/ filenames to camelCase as they are not components
- Rewrite client models to have proper TypeScript accessors and remove unused getters
- Rewrite how client models are initialized from the serialized interface using .fromInterface static methods
- Make client models implement the interfaces in common/ for consistency
- Remove unneeded _next parameter for Express.js route handlers
- Use Partial<T> for all Express.js request body types on server
- Type all Axios response body types with interfaces

GitLab: #92
Change-Id: I4b2c75ac632ec5d9bf12a874a5ba04467c76fa6d
diff --git a/client/src/App.tsx b/client/src/App.tsx
index f113e25..afd9817 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -25,7 +25,7 @@
 
 export async function checkSetupStatus(): Promise<boolean> {
   try {
-    const { data } = await axios.get('/setup/check', { baseURL: apiUrl });
+    const { data } = await axios.get<{ isSetupComplete: boolean }>('/setup/check', { baseURL: apiUrl });
     return data.isSetupComplete;
   } catch (e) {
     throw new Error('Cannot connect to server', { cause: e });
diff --git a/client/src/components/AccountPreferences.tsx b/client/src/components/AccountPreferences.tsx
index 6788535..da926d0 100644
--- a/client/src/components/AccountPreferences.tsx
+++ b/client/src/components/AccountPreferences.tsx
@@ -39,7 +39,6 @@
 import { useState } from 'react';
 
 import { useAuthContext } from '../contexts/AuthProvider';
-import { Account } from '../models/Account';
 import ConversationAvatar from './ConversationAvatar';
 import ConversationsOverviewCard from './ConversationsOverviewCard';
 import JamiIdCard from './JamiIdCard';
@@ -60,17 +59,17 @@
   const { account, axiosInstance } = useAuthContext();
 
   const devices: string[][] = [];
-  const accountDevices = account.getDevices();
+  const accountDevices = account.devices;
   for (const i in accountDevices) devices.push([i, accountDevices[i]]);
 
   console.log(devices);
 
-  const isJamiAccount = account.getType() === Account.TYPE_JAMI;
+  const isJamiAccount = account.getType() === 'RING';
   const alias = isJamiAccount ? 'Jami account' : 'SIP account';
-  const moderators = account.getDefaultModerators();
+  const moderators = account.defaultModerators;
   const [defaultModeratorUri, setDefaultModeratorUri] = useState('');
 
-  const [details, setDetails] = useState(account.getDetails());
+  const [details, setDetails] = useState(account.details);
 
   const addModerator = async () => {
     if (defaultModeratorUri) {
@@ -87,7 +86,8 @@
     newDetails[key] = value ? 'true' : 'false';
     console.log(newDetails);
     await axiosInstance.patch('/account', newDetails);
-    setDetails({ ...account.updateDetails(newDetails) });
+    account.updateDetails(newDetails);
+    setDetails(account.details);
   };
 
   return (
@@ -219,19 +219,19 @@
                   </IconButton>
                 </ListItemSecondaryAction>
               </ListItem>
-              {!moderators || moderators.length === 0 ? (
+              {moderators.length === 0 ? (
                 <ListItem key="placeholder">
                   <ListItemText primary="No default moderator" />
                 </ListItem>
               ) : (
                 moderators.map((moderator) => (
-                  <ListItem key={moderator.getUri()}>
+                  <ListItem key={moderator.uri}>
                     <ListItemAvatar>
                       <ConversationAvatar displayName={moderator.getDisplayName()} />
                     </ListItemAvatar>
                     <ListItemText primary={moderator.getDisplayName()} />
                     <ListItemSecondaryAction>
-                      <IconButton onClick={() => removeModerator(moderator.getUri())} size="large">
+                      <IconButton onClick={() => removeModerator(moderator.uri)} size="large">
                         <DeleteRounded />
                       </IconButton>
                     </ListItemSecondaryAction>
diff --git a/client/src/components/ConversationList.tsx b/client/src/components/ConversationList.tsx
index 6b03224..08553b7 100644
--- a/client/src/components/ConversationList.tsx
+++ b/client/src/components/ConversationList.tsx
@@ -22,7 +22,7 @@
 import { useContext, useEffect } from 'react';
 
 import { MessengerContext } from '../contexts/MessengerProvider';
-import { Conversation } from '../models/Conversation';
+import { Conversation } from '../models/conversation';
 import { useAppSelector } from '../redux/hooks';
 import ConversationListItem from './ConversationListItem';
 
@@ -48,7 +48,7 @@
           </div>
         )}
         {conversations.map((conversation) => (
-          <ConversationListItem key={conversation.getId()} conversation={conversation} />
+          <ConversationListItem key={conversation.id} conversation={conversation} />
         ))}
         {conversations.length === 0 && (
           <div className="list-placeholder">
diff --git a/client/src/components/ConversationListItem.tsx b/client/src/components/ConversationListItem.tsx
index 41cfbe4..d94733d 100644
--- a/client/src/components/ConversationListItem.tsx
+++ b/client/src/components/ConversationListItem.tsx
@@ -16,6 +16,7 @@
  * <https://www.gnu.org/licenses/>.
  */
 import { Box, ListItem, ListItemAvatar, ListItemText } from '@mui/material';
+import { ContactDetails } from 'jami-web-common';
 import { QRCodeCanvas } from 'qrcode.react';
 import { useCallback, useContext, useMemo, useState } from 'react';
 import { useTranslation } from 'react-i18next';
@@ -25,7 +26,7 @@
 import { useConversationContext } from '../contexts/ConversationProvider';
 import { MessengerContext } from '../contexts/MessengerProvider';
 import { useStartCall } from '../hooks/useStartCall';
-import { Conversation } from '../models/Conversation';
+import { Conversation } from '../models/conversation';
 import { setRefreshFromSlice } from '../redux/appSlice';
 import { useAppDispatch } from '../redux/hooks';
 import ContextMenu, { ContextMenuHandler, useContextMenuHandler } from './ContextMenu';
@@ -56,10 +57,10 @@
   const isSelected = conversation.getDisplayUri() === pathId;
 
   const navigate = useNavigate();
-  const userId = conversation?.getFirstMember()?.contact.getUri();
+  const userId = conversation?.getFirstMember()?.contact.uri;
 
   const onClick = useCallback(() => {
-    const newConversationId = conversation.getId();
+    const newConversationId = conversation.id;
     if (newConversationId) {
       navigate(`/conversation/${newConversationId}`);
     } else {
@@ -116,7 +117,7 @@
   const getContactDetails = useCallback(async () => {
     const controller = new AbortController();
     try {
-      const { data } = await axiosInstance.get(`/contacts/${userId}`, {
+      const { data } = await axiosInstance.get<ContactDetails>(`/contacts/${userId}`, {
         signal: controller.signal,
       });
       console.log('CONTACT LIST - DETAILS: ', data);
@@ -125,7 +126,7 @@
     }
   }, [axiosInstance, userId]);
 
-  const conversationId = conversation.getId();
+  const conversationId = conversation.id;
 
   const menuOptions: PopoverListItemData[] = useMemo(
     () => [
diff --git a/client/src/components/ConversationView.tsx b/client/src/components/ConversationView.tsx
index 075ca6e..c9a19a2 100644
--- a/client/src/components/ConversationView.tsx
+++ b/client/src/components/ConversationView.tsx
@@ -22,7 +22,7 @@
 import { useAuthContext } from '../contexts/AuthProvider';
 import { useConversationContext } from '../contexts/ConversationProvider';
 import { useStartCall } from '../hooks/useStartCall';
-import { ConversationMember } from '../models/Conversation';
+import { ConversationMember } from '../models/conversation';
 import ChatInterface from '../pages/ChatInterface';
 import { translateEnumeration, TranslateEnumerationOptions } from '../utils/translations';
 import { AddParticipantButton, ShowOptionsMenuButton, StartAudioCallButton, StartVideoCallButton } from './Button';
@@ -46,7 +46,7 @@
   const { conversation, conversationId } = useConversationContext();
   const { t } = useTranslation();
 
-  const members = conversation.getMembers();
+  const members = conversation.members;
   const adminTitle = conversation.infos.title as string;
 
   const title = useMemo(() => {
diff --git a/client/src/components/ConversationsOverviewCard.tsx b/client/src/components/ConversationsOverviewCard.tsx
index cdc1c79..67b4b68 100644
--- a/client/src/components/ConversationsOverviewCard.tsx
+++ b/client/src/components/ConversationsOverviewCard.tsx
@@ -16,11 +16,11 @@
  * <https://www.gnu.org/licenses/>.
  */
 import { Card, CardActionArea, CardContent, CircularProgress, Typography } from '@mui/material';
+import { IConversation } from 'jami-web-common';
 import { useEffect, useState } from 'react';
 import { useNavigate } from 'react-router';
 
 import { useAuthContext } from '../contexts/AuthProvider';
-import { Conversation } from '../models/Conversation';
 
 export default function ConversationsOverviewCard() {
   const { axiosInstance, account } = useAuthContext();
@@ -28,12 +28,12 @@
 
   const [conversationCount, setConversationCount] = useState<number | undefined>();
 
-  const accountId = account.getId();
+  const accountId = account.id;
 
   useEffect(() => {
     const controller = new AbortController();
     axiosInstance
-      .get<Conversation[]>('/conversations', {
+      .get<IConversation[]>('/conversations', {
         signal: controller.signal,
       })
       .then(({ data }) => {
diff --git a/client/src/components/JamiIdCard.tsx b/client/src/components/JamiIdCard.tsx
index 73c038c..bfa8045 100644
--- a/client/src/components/JamiIdCard.tsx
+++ b/client/src/components/JamiIdCard.tsx
@@ -17,7 +17,7 @@
  */
 import { Box, Card, CardContent, Typography } from '@mui/material';
 
-import { Account } from '../models/Account';
+import { Account } from '../models/account';
 
 type JamiIdCardProps = {
   account: Account;
diff --git a/client/src/components/Message.tsx b/client/src/components/Message.tsx
index 669f041..8807295 100644
--- a/client/src/components/Message.tsx
+++ b/client/src/components/Message.tsx
@@ -23,8 +23,8 @@
 import { useTranslation } from 'react-i18next';
 
 import dayjs from '../dayjsInitializer';
-import { Account } from '../models/Account';
-import { Contact } from '../models/Contact';
+import { Account } from '../models/account';
+import { Contact } from '../models/contact';
 import { EmojiButton, MoreButton, ReplyMessageButton } from './Button';
 import ConversationAvatar from './ConversationAvatar';
 import PopoverList, { PopoverListItemData } from './PopoverList';
diff --git a/client/src/components/MessageList.tsx b/client/src/components/MessageList.tsx
index 538acc7..356d3e6 100644
--- a/client/src/components/MessageList.tsx
+++ b/client/src/components/MessageList.tsx
@@ -23,7 +23,7 @@
 import { Waypoint } from 'react-waypoint';
 
 import { useAuthContext } from '../contexts/AuthProvider';
-import { ConversationMember } from '../models/Conversation';
+import { ConversationMember } from '../models/conversation';
 import { MessageRow } from './Message';
 import { ArrowDownIcon } from './SvgIcon';
 
@@ -56,7 +56,7 @@
               if (isAccountMessage) {
                 author = account;
               } else {
-                const member = members.find((member) => message.author === member.contact.getUri());
+                const member = members.find((member) => message.author === member.contact.uri);
                 author = member?.contact;
               }
               if (!author) {
diff --git a/client/src/components/SendMessageForm.tsx b/client/src/components/SendMessageForm.tsx
index d1267aa..7bb7a7f 100644
--- a/client/src/components/SendMessageForm.tsx
+++ b/client/src/components/SendMessageForm.tsx
@@ -21,7 +21,7 @@
 import { useTranslation } from 'react-i18next';
 
 import { useAuthContext } from '../contexts/AuthProvider';
-import { ConversationMember } from '../models/Conversation';
+import { ConversationMember } from '../models/conversation';
 import { translateEnumeration, TranslateEnumerationOptions } from '../utils/translations';
 import {
   RecordVideoMessageButton,
diff --git a/client/src/components/UsernameChooser.jsx b/client/src/components/UsernameChooser.jsx
index 19b8380..b098d1d 100644
--- a/client/src/components/UsernameChooser.jsx
+++ b/client/src/components/UsernameChooser.jsx
@@ -20,7 +20,7 @@
 import axios from 'axios';
 import { useEffect, useState } from 'react';
 
-import { apiUrl } from '../utils/constants.js';
+import { apiUrl } from '../utils/constants';
 
 const isInputValid = (input) => input && input.length > 2;
 
diff --git a/client/src/contexts/AuthProvider.tsx b/client/src/contexts/AuthProvider.tsx
index d0d88cc..94f3dcf 100644
--- a/client/src/contexts/AuthProvider.tsx
+++ b/client/src/contexts/AuthProvider.tsx
@@ -16,13 +16,13 @@
  * <https://www.gnu.org/licenses/>.
  */
 import axios, { AxiosInstance } from 'axios';
-import { HttpStatusCode } from 'jami-web-common';
+import { HttpStatusCode, IAccount } from 'jami-web-common';
 import { useCallback, useEffect, useMemo, useState } from 'react';
 import { useNavigate } from 'react-router-dom';
 
 import ProcessingRequest from '../components/ProcessingRequest';
 import { createOptionalContext } from '../hooks/createOptionalContext';
-import { Account } from '../models/Account';
+import { Account } from '../models/account';
 import { apiUrl } from '../utils/constants';
 import { WithChildren } from '../utils/utils';
 
@@ -91,7 +91,7 @@
       return;
     }
 
-    axiosInstance.get('/account').then(({ data }) => setAccount(Account.from(data)));
+    axiosInstance.get<IAccount>('/account').then(({ data }) => setAccount(Account.fromInterface(data)));
   }, [axiosInstance, logout]);
 
   if (!token || !account || !axiosInstance) {
@@ -104,7 +104,7 @@
         token,
         logout,
         account,
-        accountId: account.getId(),
+        accountId: account.id,
         axiosInstance,
       }}
     >
diff --git a/client/src/contexts/CallProvider.tsx b/client/src/contexts/CallProvider.tsx
index 5464228..88951a9 100644
--- a/client/src/contexts/CallProvider.tsx
+++ b/client/src/contexts/CallProvider.tsx
@@ -156,7 +156,7 @@
   // TODO: This logic will have to change to support multiple people in a call. Could we move this logic to the server?
   //       The client could make a single request with the conversationId, and the server would be tasked with sending
   //       all the individual requests to the members of the conversation.
-  const contactUri = useMemo(() => conversation.getFirstMember().contact.getUri(), [conversation]);
+  const contactUri = useMemo(() => conversation.getFirstMember().contact.uri, [conversation]);
 
   useEffect(() => {
     if (callStatus !== CallStatus.InCall) {
diff --git a/client/src/contexts/ConversationProvider.tsx b/client/src/contexts/ConversationProvider.tsx
index b927278..8d5f7b5 100644
--- a/client/src/contexts/ConversationProvider.tsx
+++ b/client/src/contexts/ConversationProvider.tsx
@@ -21,7 +21,7 @@
 import LoadingPage from '../components/Loading';
 import { createOptionalContext } from '../hooks/createOptionalContext';
 import { useUrlParams } from '../hooks/useUrlParams';
-import { Conversation } from '../models/Conversation';
+import { Conversation } from '../models/conversation';
 import { ConversationRouteParams } from '../router';
 import { useConversationQuery } from '../services/conversationQueries';
 import { WithChildren } from '../utils/utils';
@@ -51,7 +51,7 @@
 
   useEffect(() => {
     if (conversationQuery.isSuccess) {
-      const conversation = Conversation.from(accountId, conversationQuery.data);
+      const conversation = Conversation.fromInterface(conversationQuery.data);
       setConversation(conversation);
     }
   }, [accountId, conversationQuery.isSuccess, conversationQuery.data]);
diff --git a/client/src/contexts/MessengerProvider.tsx b/client/src/contexts/MessengerProvider.tsx
index 3ec6164..51a19fb 100644
--- a/client/src/contexts/MessengerProvider.tsx
+++ b/client/src/contexts/MessengerProvider.tsx
@@ -15,11 +15,11 @@
  * License along with this program.  If not, see
  * <https://www.gnu.org/licenses/>.
  */
-import { ConversationMessage, WebSocketMessageType } from 'jami-web-common';
+import { ConversationMessage, IConversation, LookupResult, WebSocketMessageType } from 'jami-web-common';
 import { createContext, ReactNode, useContext, useEffect, useState } from 'react';
 
-import { Contact } from '../models/Contact';
-import { Conversation } from '../models/Conversation';
+import { Contact } from '../models/contact';
+import { Conversation } from '../models/conversation';
 import { setRefreshFromSlice } from '../redux/appSlice';
 import { useAppDispatch, useAppSelector } from '../redux/hooks';
 import { SetState } from '../utils/utils';
@@ -61,11 +61,13 @@
   useEffect(() => {
     const controller = new AbortController();
     axiosInstance
-      .get<Conversation[]>('/conversations', {
+      .get<IConversation[]>('/conversations', {
         signal: controller.signal,
       })
       .then(({ data }) => {
-        setConversations(Object.values(data).map((c) => Conversation.from(accountId, c)));
+        setConversations(
+          Object.values(data).map((conversationInterface) => Conversation.fromInterface(conversationInterface))
+        );
       });
     // return () => controller.abort()
   }, [axiosInstance, accountId, refresh]);
@@ -89,15 +91,13 @@
   useEffect(() => {
     if (!searchQuery) return;
     const controller = new AbortController();
-    // TODO: Type properly https://git.jami.net/savoirfairelinux/jami-web/-/issues/92
     axiosInstance
-      .get<{ state: number; address: string; username: string }>(`/ns/username/${searchQuery}`, {
+      .get<LookupResult>(`/ns/username/${searchQuery}`, {
         signal: controller.signal,
       })
       .then(({ data }) => {
-        const contact = new Contact(data.address);
-        contact.setRegisteredName(data.username);
-        setSearchResults(contact ? Conversation.fromSingleContact(accountId, contact) : undefined);
+        const contact = new Contact(data.address, data.username);
+        setSearchResults(contact ? Conversation.fromSingleContact(contact) : undefined);
       })
       .catch(() => {
         setSearchResults(undefined);
diff --git a/client/src/contexts/WebRtcProvider.tsx b/client/src/contexts/WebRtcProvider.tsx
index 1ce3ff6..b7733bc 100644
--- a/client/src/contexts/WebRtcProvider.tsx
+++ b/client/src/contexts/WebRtcProvider.tsx
@@ -66,17 +66,17 @@
     if (!webRtcConnection && account) {
       const iceServers: RTCIceServer[] = [];
 
-      if (account.getDetails()['TURN.enable'] === 'true') {
+      if (account.details['TURN.enable'] === 'true') {
         iceServers.push({
-          urls: 'turn:' + account.getDetails()['TURN.server'],
-          username: account.getDetails()['TURN.username'],
-          credential: account.getDetails()['TURN.password'],
+          urls: 'turn:' + account.details['TURN.server'],
+          username: account.details['TURN.username'],
+          credential: account.details['TURN.password'],
         });
       }
 
-      if (account.getDetails()['STUN.enable'] === 'true') {
+      if (account.details['STUN.enable'] === 'true') {
         iceServers.push({
-          urls: 'stun:' + account.getDetails()['STUN.server'],
+          urls: 'stun:' + account.details['STUN.server'],
         });
       }
 
@@ -123,7 +123,7 @@
   const [iceCandidateQueue, setIceCandidateQueue] = useState<RTCIceCandidate[]>([]);
 
   // TODO: This logic will have to change to support multiple people in a call
-  const contactUri = useMemo(() => conversation.getFirstMember().contact.getUri(), [conversation]);
+  const contactUri = useMemo(() => conversation.getFirstMember().contact.uri, [conversation]);
 
   const getMediaDevices = useCallback(async (): Promise<MediaDevicesInfo> => {
     try {
diff --git a/client/src/models/Account.ts b/client/src/models/Account.ts
deleted file mode 100644
index 7471e5e..0000000
--- a/client/src/models/Account.ts
+++ /dev/null
@@ -1,151 +0,0 @@
-/*
- * 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 { AccountDetails, VolatileDetails } from 'jami-web-common';
-
-import { Contact } from './Contact.js';
-import { Conversation } from './Conversation.js';
-
-export class Account {
-  private readonly id: string;
-  private _details: AccountDetails;
-  private _volatileDetails: VolatileDetails;
-  private _contacts: Contact[];
-  private readonly conversations: Record<string, Conversation>;
-  private defaultModerators: Contact[];
-  private devices: Record<string, string>;
-
-  static readonly TYPE_JAMI: string = 'RING';
-  static readonly TYPE_SIP: string = 'SIP';
-
-  constructor(id: string, details: AccountDetails, volatileDetails: VolatileDetails) {
-    this.id = id;
-    this._details = details || {};
-    this._volatileDetails = volatileDetails || {};
-    this._contacts = [];
-    this.conversations = {};
-    this.defaultModerators = [];
-    this.devices = {};
-  }
-
-  static from(object: any) {
-    const account = new Account(object.id, object.details, object.volatileDetails);
-    if (object.defaultModerators) account.defaultModerators = object.defaultModerators.map((m: any) => Contact.from(m));
-    return account;
-  }
-
-  getId() {
-    return this.id;
-  }
-
-  getType() {
-    return this._details['Account.type'];
-  }
-
-  getUri() {
-    return this._details['Account.username'];
-  }
-
-  getRegisteredName() {
-    return this._volatileDetails['Account.registeredName'];
-  }
-
-  isRendezVous() {
-    return this._details['Account.rendezVous'] === 'true';
-  }
-
-  isPublicIn() {
-    return this._details['DHT.PublicInCalls'] === 'true';
-  }
-
-  setDetail(detail: keyof AccountDetails, value: string) {
-    this._details[detail] = value;
-  }
-
-  updateDetails(details: Partial<AccountDetails>) {
-    return Object.assign(this._details, details);
-  }
-
-  getDetails() {
-    return this._details;
-  }
-
-  getDisplayName() {
-    return this._details['Account.displayName'] || this.getDisplayUri();
-  }
-
-  getDisplayUri() {
-    return this.getRegisteredName() || this.getUri();
-  }
-
-  getDisplayNameNoFallback() {
-    return this._details['Account.displayName'] || this.getRegisteredName();
-  }
-
-  getConversationIds() {
-    return Object.keys(this.conversations);
-  }
-
-  getConversations() {
-    return this.conversations;
-  }
-
-  getConversation(conversationId: string) {
-    return this.conversations[conversationId];
-  }
-
-  addConversation(conversation: Conversation) {
-    const conversationId = conversation.getId();
-    if (conversationId != null) {
-      this.conversations[conversationId] = conversation;
-    } else {
-      throw new Error('Conversation ID cannot be undefined');
-    }
-  }
-
-  removeConversation(conversationId: string) {
-    delete this.conversations[conversationId];
-  }
-
-  getContacts() {
-    return this._contacts;
-  }
-
-  set contacts(contacts: Contact[]) {
-    this._contacts = contacts;
-  }
-
-  getDefaultModerators() {
-    return this.defaultModerators;
-  }
-
-  set details(value: AccountDetails) {
-    this._details = value;
-  }
-
-  set volatileDetails(value: VolatileDetails) {
-    this._volatileDetails = value;
-  }
-
-  setDevices(devices: Record<string, string>) {
-    this.devices = { ...devices };
-  }
-
-  getDevices() {
-    return this.devices;
-  }
-}
diff --git a/client/src/models/Contact.ts b/client/src/models/Contact.ts
deleted file mode 100644
index bd526ae..0000000
--- a/client/src/models/Contact.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * 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/>.
- */
-export class Contact {
-  private readonly uri: string;
-  private readonly displayName: string | undefined;
-  private registeredName: string | undefined;
-
-  constructor(uri: string) {
-    this.uri = uri;
-    this.displayName = undefined;
-    this.registeredName = undefined;
-  }
-
-  static from(object: any) {
-    const contact = new Contact(object.uri);
-    if (object.registeredName) contact.setRegisteredName(object.registeredName);
-    return contact;
-  }
-
-  getUri() {
-    return this.uri;
-  }
-
-  getRegisteredName() {
-    return this.registeredName;
-  }
-
-  setRegisteredName(name: string | undefined) {
-    this.registeredName = name;
-  }
-
-  getDisplayName() {
-    return this.getDisplayNameNoFallback() || this.getUri();
-  }
-
-  getDisplayNameNoFallback() {
-    return this.displayName || this.getRegisteredName();
-  }
-}
diff --git a/client/src/models/Conversation.ts b/client/src/models/Conversation.ts
deleted file mode 100644
index fa72af9..0000000
--- a/client/src/models/Conversation.ts
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * 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 { Message } from 'jami-web-common';
-
-import { Contact } from './Contact';
-
-export interface ConversationMember {
-  contact: Contact;
-  role?: 'admin' | 'member' | 'invited' | 'banned' | 'left';
-}
-
-type ConversationInfos = Record<string, unknown>;
-
-export class Conversation {
-  private readonly id: string | undefined;
-  private readonly accountId: string;
-  private readonly members: ConversationMember[];
-  private messages: Message[];
-  private _infos: ConversationInfos;
-
-  constructor(id: string | undefined, accountId: string, members?: ConversationMember[]) {
-    this.id = id;
-    this.accountId = accountId;
-    this.members = members || [];
-
-    this.messages = [];
-    this._infos = {};
-  }
-
-  static from(accountId: string, object: any) {
-    const conversation = new Conversation(
-      object.id,
-      accountId,
-      object.members.map((member: any) => {
-        member.contact = Contact.from(member.contact);
-        return member;
-      })
-    );
-    conversation.messages = object.messages;
-    conversation.infos = object.infos;
-    return conversation;
-  }
-  static fromSingleContact(accountId: string, contact: Contact) {
-    return new Conversation(undefined, accountId, [{ contact }]);
-  }
-
-  getId() {
-    return this.id;
-  }
-
-  getAccountId() {
-    return this.accountId;
-  }
-
-  getDisplayName() {
-    if (this.members.length !== 0) {
-      return this.members[0].contact.getDisplayName();
-    }
-    return this.getDisplayUri();
-  }
-
-  getDisplayNameNoFallback() {
-    if (this.members.length !== 0) {
-      return this.members[0].contact.getDisplayNameNoFallback();
-    }
-  }
-
-  getDisplayUri() {
-    return this.getId() || this.getFirstMember().contact.getUri();
-  }
-
-  getFirstMember() {
-    return this.members[0];
-  }
-
-  getMembers() {
-    return this.members;
-  }
-
-  addMessage(message: Message) {
-    if (this.messages.length === 0) this.messages.push(message);
-    else if (message.id === this.messages[this.messages.length - 1].linearizedParent) {
-      this.messages.push(message);
-    } else if (message.linearizedParent === this.messages[0].id) {
-      this.messages.unshift(message);
-    } else {
-      console.log("Can't insert message " + message.id);
-    }
-  }
-
-  addLoadedMessages(messages: Message[]) {
-    messages.forEach((message) => this.addMessage(message));
-  }
-
-  getMessages() {
-    return this.messages;
-  }
-
-  get infos() {
-    return this._infos;
-  }
-
-  set infos(infos: ConversationInfos) {
-    this._infos = infos;
-  }
-}
diff --git a/client/src/models/account.ts b/client/src/models/account.ts
new file mode 100644
index 0000000..60ac1d8
--- /dev/null
+++ b/client/src/models/account.ts
@@ -0,0 +1,97 @@
+/*
+ * 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 { AccountDetails, Devices, IAccount, VolatileDetails } from 'jami-web-common';
+
+import { Contact } from './contact';
+import { Conversation } from './conversation';
+
+export type AccountType = 'RING' | 'SIP';
+
+export class Account implements IAccount {
+  readonly id: string;
+  details: AccountDetails;
+  volatileDetails: VolatileDetails;
+  defaultModerators: Contact[] = [];
+  devices: Devices = {};
+  contacts: Contact[] = [];
+
+  private _conversations: Record<string, Conversation> = {};
+
+  constructor(id: string, details: AccountDetails, volatileDetails: VolatileDetails) {
+    this.id = id;
+    this.details = details;
+    this.volatileDetails = volatileDetails;
+  }
+
+  static fromInterface(accountInterface: IAccount) {
+    const account = new Account(accountInterface.id, accountInterface.details, accountInterface.volatileDetails);
+    account.defaultModerators = accountInterface.defaultModerators.map(Contact.fromInterface);
+    return account;
+  }
+
+  getType(): AccountType {
+    return this.details['Account.type'] as AccountType;
+  }
+
+  getUri() {
+    return this.details['Account.username'];
+  }
+
+  getRegisteredName() {
+    return this.volatileDetails['Account.registeredName'];
+  }
+
+  isRendezVous() {
+    return this.details['Account.rendezVous'] === 'true';
+  }
+
+  isPublicIn() {
+    return this.details['DHT.PublicInCalls'] === 'true';
+  }
+
+  updateDetails(details: Partial<AccountDetails>) {
+    this.details = { ...this.details, ...details };
+  }
+
+  getDisplayUri() {
+    return this.getRegisteredName() ?? this.getUri();
+  }
+
+  getDisplayName() {
+    return this.details['Account.displayName'] ?? this.getDisplayUri();
+  }
+
+  getDisplayNameNoFallback() {
+    return this.details['Account.displayName'] ?? this.getRegisteredName();
+  }
+
+  get conversations() {
+    return this._conversations;
+  }
+
+  addConversation(conversation: Conversation) {
+    if (conversation.id === undefined) {
+      throw new Error('Conversation ID cannot be undefined');
+    }
+    this._conversations[conversation.id] = conversation;
+  }
+
+  removeConversation(conversationId: string) {
+    delete this.conversations[conversationId];
+  }
+}
diff --git a/client/src/models/contact.ts b/client/src/models/contact.ts
new file mode 100644
index 0000000..e696db0
--- /dev/null
+++ b/client/src/models/contact.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 { IContact } from 'jami-web-common';
+
+export class Contact implements IContact {
+  readonly uri: string;
+  registeredName?: string;
+  displayName?: string;
+
+  constructor(uri: string, registeredName?: string) {
+    this.uri = uri;
+    this.registeredName = registeredName;
+  }
+
+  static fromInterface(contactInterface: IContact) {
+    return new Contact(contactInterface.uri, contactInterface.registeredName);
+  }
+
+  getDisplayName() {
+    return this.getDisplayNameNoFallback() ?? this.uri;
+  }
+
+  getDisplayNameNoFallback() {
+    return this.displayName ?? this.registeredName;
+  }
+}
diff --git a/client/src/models/conversation.ts b/client/src/models/conversation.ts
new file mode 100644
index 0000000..484f166
--- /dev/null
+++ b/client/src/models/conversation.ts
@@ -0,0 +1,94 @@
+/*
+ * 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 { ConversationInfos, IConversation, IConversationMember, Message } from 'jami-web-common';
+
+import { Contact } from './contact';
+
+export interface ConversationMember extends IConversationMember {
+  contact: Contact;
+}
+
+export class Conversation implements IConversation {
+  readonly id: string;
+  members: ConversationMember[];
+  messages: Message[] = [];
+  infos: ConversationInfos = {};
+
+  constructor(id: string, members?: ConversationMember[]) {
+    this.id = id;
+    this.members = members ?? [];
+  }
+
+  static fromInterface(conversationInterface: IConversation) {
+    const conversation = new Conversation(
+      conversationInterface.id,
+      conversationInterface.members.map((member) => {
+        const contact = Contact.fromInterface(member.contact);
+        return { contact } as ConversationMember;
+      })
+    );
+
+    conversation.messages = conversationInterface.messages;
+    conversation.infos = conversationInterface.infos;
+
+    return conversation;
+  }
+
+  static fromSingleContact(contact: Contact) {
+    return new Conversation('', [{ contact } as ConversationMember]);
+  }
+
+  getDisplayUri() {
+    return this.id ?? this.getFirstMember().contact.uri;
+  }
+
+  getDisplayName() {
+    if (this.members.length !== 0) {
+      return this.getFirstMember().contact.registeredName;
+    }
+    return this.getDisplayUri();
+  }
+
+  getDisplayNameNoFallback() {
+    if (this.members.length !== 0) {
+      return this.getFirstMember().contact.registeredName;
+    }
+  }
+
+  getFirstMember() {
+    return this.members[0];
+  }
+
+  addMessage(message: Message) {
+    if (this.messages.length === 0) {
+      this.messages.push(message);
+    } else if (message.id === this.messages[this.messages.length - 1].linearizedParent) {
+      this.messages.push(message);
+    } else if (message.linearizedParent === this.messages[0].id) {
+      this.messages.unshift(message);
+    } else {
+      console.log('Could not insert message', message.id);
+    }
+  }
+
+  addMessages(messages: Message[]) {
+    for (const message of messages) {
+      this.addMessage(message);
+    }
+  }
+}
diff --git a/client/src/pages/AddContactPage.tsx b/client/src/pages/AddContactPage.tsx
index 5e3c375..fb7e4bb 100644
--- a/client/src/pages/AddContactPage.tsx
+++ b/client/src/pages/AddContactPage.tsx
@@ -17,6 +17,7 @@
  */
 import GroupAddRounded from '@mui/icons-material/GroupAddRounded';
 import { Box, Card, CardContent, Container, Fab, Typography } from '@mui/material';
+import { ContactDetails } from 'jami-web-common';
 import { useTranslation } from 'react-i18next';
 import { useNavigate } from 'react-router-dom';
 
@@ -36,7 +37,7 @@
   const dispatch = useAppDispatch();
 
   const handleClick = async () => {
-    const { data } = await axiosInstance.put(`/contacts/${contactId}`);
+    const { data } = await axiosInstance.put<ContactDetails>(`/contacts/${contactId}`);
     dispatch(setRefreshFromSlice());
 
     if (data.conversationId) {
diff --git a/client/src/pages/CallInterface.tsx b/client/src/pages/CallInterface.tsx
index 10b1d3c..3d5b366 100644
--- a/client/src/pages/CallInterface.tsx
+++ b/client/src/pages/CallInterface.tsx
@@ -214,7 +214,7 @@
   const { callStartTime } = useContext(CallContext);
   const { conversation } = useConversationContext();
   const [elapsedTime, setElapsedTime] = useState(0);
-  const memberName = useMemo(() => conversation.getFirstMember().contact.getRegisteredName(), [conversation]);
+  const memberName = useMemo(() => conversation.getFirstMember().contact.registeredName, [conversation]);
 
   useEffect(() => {
     if (callStartTime) {
diff --git a/client/src/pages/CallPending.tsx b/client/src/pages/CallPending.tsx
index 2cd57c6..54fb916 100644
--- a/client/src/pages/CallPending.tsx
+++ b/client/src/pages/CallPending.tsx
@@ -149,7 +149,7 @@
   const { callStatus } = useContext(CallContext);
   const { t } = useTranslation();
   const { conversation } = useConversationContext();
-  const memberName = useMemo(() => conversation.getFirstMember().contact.getRegisteredName(), [conversation]);
+  const memberName = useMemo(() => conversation.getFirstMember().contact.registeredName, [conversation]);
 
   let title = t('loading');
 
@@ -183,7 +183,7 @@
 
   const { t } = useTranslation();
   const { conversation } = useConversationContext();
-  const memberName = useMemo(() => conversation.getFirstMember().contact.getRegisteredName(), [conversation]);
+  const memberName = useMemo(() => conversation.getFirstMember().contact.registeredName, [conversation]);
 
   let title = t('loading');
 
diff --git a/client/src/pages/ChatInterface.tsx b/client/src/pages/ChatInterface.tsx
index 4ae9ce6..00aeb3e 100644
--- a/client/src/pages/ChatInterface.tsx
+++ b/client/src/pages/ChatInterface.tsx
@@ -105,7 +105,7 @@
     return <div>Error loading {conversationId}</div>;
   }
 
-  const members = conversation.getMembers();
+  const members = conversation.members;
 
   return (
     <Stack flex={1} overflow="hidden" {...getRootProps()} paddingBottom="16px">
diff --git a/client/src/pages/SetupLogin.tsx b/client/src/pages/SetupLogin.tsx
index d34d366..2ae33b9 100644
--- a/client/src/pages/SetupLogin.tsx
+++ b/client/src/pages/SetupLogin.tsx
@@ -18,7 +18,7 @@
 import GroupAddRounded from '@mui/icons-material/GroupAddRounded';
 import { Box, Card, CardContent, Container, Fab, Input, Typography } from '@mui/material';
 import axios from 'axios';
-import { HttpStatusCode } from 'jami-web-common';
+import { AccessToken, HttpStatusCode } from 'jami-web-common';
 import { FormEvent, useEffect, useState } from 'react';
 import { useTranslation } from 'react-i18next';
 import { useNavigate } from 'react-router-dom';
@@ -45,7 +45,7 @@
 
   const loginAdmin = async (password: string) => {
     try {
-      const { data } = await axios.post('/setup/admin/login', { password }, { baseURL: apiUrl });
+      const { data } = await axios.post<AccessToken>('/setup/admin/login', { password }, { baseURL: apiUrl });
       localStorage.setItem('adminAccessToken', data.accessToken);
     } catch (e: any) {
       if (e.response?.status === HttpStatusCode.Forbidden) {
diff --git a/client/src/services/conversationQueries.ts b/client/src/services/conversationQueries.ts
index 92cc80d..44c0aae 100644
--- a/client/src/services/conversationQueries.ts
+++ b/client/src/services/conversationQueries.ts
@@ -16,6 +16,7 @@
  * <https://www.gnu.org/licenses/>.
  */
 import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+import { IConversation, Message } from 'jami-web-common';
 
 import { useAuthContext } from '../contexts/AuthProvider';
 
@@ -24,7 +25,7 @@
   return useQuery(
     ['conversation', conversationId],
     async () => {
-      const { data } = await axiosInstance.get(`/conversations/${conversationId}`);
+      const { data } = await axiosInstance.get<IConversation>(`/conversations/${conversationId}`);
       return data;
     },
     {
@@ -38,7 +39,7 @@
   return useQuery(
     ['messages', conversationId],
     async () => {
-      const { data } = await axiosInstance.get(`/conversations/${conversationId}/messages`);
+      const { data } = await axiosInstance.get<Message[]>(`/conversations/${conversationId}/messages`);
       return data;
     },
     {
diff --git a/client/src/utils/auth.ts b/client/src/utils/auth.ts
index 7b3a088..07797a4 100644
--- a/client/src/utils/auth.ts
+++ b/client/src/utils/auth.ts
@@ -17,7 +17,7 @@
  */
 import axios from 'axios';
 import { passwordStrength } from 'check-password-strength';
-import { HttpStatusCode } from 'jami-web-common';
+import { AccessToken, HttpStatusCode } from 'jami-web-common';
 
 import { PasswordStrength } from '../enums/passwordStrength';
 import { apiUrl } from './constants';
@@ -75,7 +75,7 @@
 
 export async function loginUser(username: string, password: string, isJams: boolean): Promise<string> {
   try {
-    const { data } = await axios.post('/auth/login', { username, password, isJams }, { baseURL: apiUrl });
+    const { data } = await axios.post<AccessToken>('/auth/login', { username, password, isJams }, { baseURL: apiUrl });
     return data.accessToken;
   } catch (e: any) {
     switch (e.response?.status) {