Integrate new server authentication to client

Changes:
- Use server authentication REST API
- Log in automatically after registration
- Store token in localStorage
- Give feedback to user if registration or login fails

GitLab: #75
Change-Id: Ib90e5b911621567c6825af5e275920d703cdfe88
diff --git a/client/.env.development b/client/.env.development
index 2accbba..6d453cd 100644
--- a/client/.env.development
+++ b/client/.env.development
@@ -1 +1,2 @@
 ESLINT_NO_DEV_ERRORS=true
+VITE_API_URL=http://localhost:5000
\ No newline at end of file
diff --git a/client/src/components/AccountPreferences.tsx b/client/src/components/AccountPreferences.tsx
index db3e62d..2eedc97 100644
--- a/client/src/components/AccountPreferences.tsx
+++ b/client/src/components/AccountPreferences.tsx
@@ -58,7 +58,6 @@
 
 type AccountPreferencesProps = {
   account: Account;
-  onAccountChanged?: (account: Account) => void;
 };
 
 export default function AccountPreferences({ account }: AccountPreferencesProps) {
diff --git a/client/src/components/AlertSnackbar.tsx b/client/src/components/AlertSnackbar.tsx
new file mode 100644
index 0000000..c32eca8
--- /dev/null
+++ b/client/src/components/AlertSnackbar.tsx
@@ -0,0 +1,46 @@
+/*
+ * 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 { Alert, AlertColor, AlertProps, AlertTitle, Snackbar, SnackbarProps } from '@mui/material';
+import { useTranslation } from 'react-i18next';
+
+type AlertSnackbarProps = AlertProps & {
+  severity: AlertColor;
+  open?: boolean;
+  snackBarProps?: Partial<SnackbarProps>;
+};
+
+export function AlertSnackbar({ severity, open, snackBarProps, children, ...alertProps }: AlertSnackbarProps) {
+  const { t } = useTranslation();
+
+  return (
+    <Snackbar
+      open={open}
+      {...snackBarProps}
+      anchorOrigin={{
+        vertical: 'top',
+        horizontal: 'center',
+        ...snackBarProps?.anchorOrigin,
+      }}
+    >
+      <Alert severity={severity} {...alertProps}>
+        <AlertTitle>{t(`severity_${severity}`)}</AlertTitle>
+        {children}
+      </Alert>
+    </Snackbar>
+  );
+}
diff --git a/client/src/components/ContactList.jsx b/client/src/components/ContactList.jsx
index 2aee98b..56f5f9c 100644
--- a/client/src/components/ContactList.jsx
+++ b/client/src/components/ContactList.jsx
@@ -37,7 +37,7 @@
 };
 
 export default function ContactList() {
-  const { accountId, accountObject } = useAppSelector((state) => state.app);
+  const { accountId, account } = useAppSelector((state) => state.userInfo);
   const dispatch = useAppDispatch();
 
   const [contacts, setContacts] = useState([]);
diff --git a/client/src/components/ConversationList.tsx b/client/src/components/ConversationList.tsx
index 9ad47f6..316792b 100644
--- a/client/src/components/ConversationList.tsx
+++ b/client/src/components/ConversationList.tsx
@@ -31,7 +31,7 @@
   search?: Conversation;
 };
 export default function ConversationList(props: ConversationListProps) {
-  const { refresh } = useAppSelector((state) => state.app);
+  const { refresh } = useAppSelector((state) => state.userInfo);
 
   useEffect(() => {
     console.log('refresh list');
diff --git a/client/src/components/Header.tsx b/client/src/components/Header.tsx
index 303fc67..f2a0c37 100644
--- a/client/src/components/Header.tsx
+++ b/client/src/components/Header.tsx
@@ -19,7 +19,7 @@
 import { MouseEvent, useState } from 'react';
 import { useNavigate, useParams } from 'react-router-dom';
 
-import authManager from '../AuthManager';
+import { setAccessToken } from '../utils/auth';
 
 export default function Header() {
   const navigate = useNavigate();
@@ -28,20 +28,23 @@
   const handleClose = () => setAnchorEl(null);
   const params = useParams();
 
-  const goToAccountSelection = () => navigate(`/account`);
-  const goToContacts = () => navigate(`/Contacts`);
+  const goToContacts = () => navigate(`/contacts`);
   const goToAccountSettings = () => navigate(`/account/${params.accountId}/settings`);
 
+  const logout = () => {
+    setAccessToken('');
+    navigate('/', { replace: true });
+  };
+
   return (
     <Box>
       <Button aria-controls="simple-menu" aria-haspopup="true" onClick={handleClick}>
         Menu
       </Button>
       <Menu id="simple-menu" anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleClose}>
-        <MenuItem onClick={goToAccountSelection}>Change account</MenuItem>
         <MenuItem onClick={goToContacts}>Contacts</MenuItem>
         {params.accountId && <MenuItem onClick={goToAccountSettings}>Account settings</MenuItem>}
-        <MenuItem onClick={() => authManager.disconnect()}>Log out</MenuItem>
+        <MenuItem onClick={logout}>Log out</MenuItem>
       </Menu>
     </Box>
   );
diff --git a/client/src/components/Input.tsx b/client/src/components/Input.tsx
index ae640cf..063b350 100644
--- a/client/src/components/Input.tsx
+++ b/client/src/components/Input.tsx
@@ -53,7 +53,13 @@
   tooltipTitle: string;
 };
 
-export const UsernameInput = ({ infoButtonProps, onChange: _onChange, tooltipTitle, ...props }: InputProps) => {
+export const UsernameInput = ({
+  infoButtonProps,
+  onChange: _onChange,
+  success,
+  tooltipTitle,
+  ...props
+}: InputProps) => {
   const [isSelected, setIsSelected] = useState(false);
   const [input, setInput] = useState(props.defaultValue);
   const [startAdornment, setStartAdornment] = useState<ReactElement | undefined>();
@@ -73,13 +79,13 @@
     let visibility = 'visible';
     if (props.error) {
       Icon = StyledRoundSaltireIconError;
-    } else if (props.success) {
+    } else if (success) {
       Icon = StyledCheckedIconSuccess;
     } else if (!isSelected && !input) {
       visibility = 'hidden'; // keep icon's space so text does not move
     }
     setStartAdornment(<Icon sx={{ visibility }} />);
-  }, [props.error, props.success, isSelected, input]);
+  }, [props.error, success, isSelected, input]);
 
   return (
     <>
@@ -88,7 +94,7 @@
       </RulesDialog>
       <TextField
         {...props}
-        color={inputColor(props.error, props.success)}
+        color={inputColor(props.error, success)}
         label={'Choose an identifier'}
         variant="standard"
         InputLabelProps={{ shrink: !!(isSelected || input) }}
@@ -106,7 +112,13 @@
   );
 };
 
-export const PasswordInput = ({ infoButtonProps, onChange: _onChange, tooltipTitle, ...props }: InputProps) => {
+export const PasswordInput = ({
+  infoButtonProps,
+  onChange: _onChange,
+  success,
+  tooltipTitle,
+  ...props
+}: InputProps) => {
   const [showPassword, setShowPassword] = useState(false);
   const [isSelected, setIsSelected] = useState(false);
   const [input, setInput] = useState(props.defaultValue);
@@ -131,13 +143,13 @@
     let visibility = 'visible';
     if (props.error) {
       Icon = StyledRoundSaltireIconError;
-    } else if (props.success) {
+    } else if (success) {
       Icon = StyledCheckedIconSuccess;
     } else if (!isSelected && !input) {
       visibility = 'hidden'; // keep icon's space so text does not move
     }
     setStartAdornment(<Icon sx={{ visibility }} />);
-  }, [props.error, props.success, isSelected, input]);
+  }, [props.error, success, isSelected, input]);
 
   return (
     <>
@@ -146,7 +158,7 @@
       </RulesDialog>
       <TextField
         {...props}
-        color={inputColor(props.error, props.success)}
+        color={inputColor(props.error, success)}
         label="Password"
         type={showPassword ? 'text' : 'password'}
         variant="standard"
diff --git a/client/src/locale/en/translation.json b/client/src/locale/en/translation.json
index b2a2935..4a1dfa8 100644
--- a/client/src/locale/en/translation.json
+++ b/client/src/locale/en/translation.json
@@ -32,5 +32,10 @@
   "password_input_weak_helper_text": "Weak",
   "password_input_medium_helper_text": "Medium",
   "password_input_strong_helper_text": "Strong",
-  "password_input_registration_failed_helper_text": "Choose another password!"
+  "password_input_registration_failed_helper_text": "Choose another password!",
+  "login_username_not_found": "Username not found",
+  "login_invalid_password": "Incorrect password",
+  "severity_error": "Error",
+  "severity_success": "Success",
+  "registration_success": "You've successfully registered! — Logging you in..."
 }
diff --git a/client/src/pages/AccountSettings.tsx b/client/src/pages/AccountSettings.tsx
index fe98277..b09fb65 100644
--- a/client/src/pages/AccountSettings.tsx
+++ b/client/src/pages/AccountSettings.tsx
@@ -15,62 +15,68 @@
  * License along with this program.  If not, see
  * <https://www.gnu.org/licenses/>.
  */
-import { CircularProgress, Container } from '@mui/material';
-import { Account } from 'jami-web-common';
-import { useEffect, useState } from 'react';
-import { useParams } from 'react-router';
+import { Container } from '@mui/material';
+import { Account, HttpStatusCode } from 'jami-web-common';
+import { useEffect } from 'react';
+import { useNavigate } from 'react-router-dom';
 
-import authManager from '../AuthManager';
 import AccountPreferences from '../components/AccountPreferences';
 import Header from '../components/Header';
-import { setAccountId, setAccountObject } from '../redux/appSlice';
-import { useAppDispatch } from '../redux/hooks';
+import ProcessingRequest from '../components/ProcessingRequest';
+import { setAccount } from '../redux/appSlice';
+import { useAppDispatch, useAppSelector } from '../redux/hooks';
+import { getAccessToken, setAccessToken } from '../utils/auth';
+import { apiUrl } from '../utils/constants';
 
-type AccountSettingsProps = {
-  accountId?: string;
-  account?: Account;
-};
-
-const AccountSettings = (props: AccountSettingsProps) => {
-  console.log('ACCOUNT SETTINGS', props.account);
-  const params = useParams();
-  const accountId = props.accountId || params.accountId;
-
-  if (accountId == null) {
-    throw new Error('Missing accountId');
-  }
-
+export default function AccountSettings() {
   const dispatch = useAppDispatch();
+  const navigate = useNavigate();
 
-  const [account, setAccount] = useState<Account | null>(null);
+  const { account } = useAppSelector((state) => state.userInfo);
+  const accessToken = getAccessToken();
 
   useEffect(() => {
-    dispatch(setAccountId(accountId));
+    if (accessToken) {
+      const getAccount = async () => {
+        const url = new URL('/account', apiUrl);
+        let response: Response;
 
-    const controller = new AbortController();
-    authManager
-      .fetch(`/api/accounts/${accountId}`, { signal: controller.signal })
-      .then((res) => res.json())
-      .then((result) => {
-        const account = Account.from(result);
-        account.setDevices(result.devices);
-        dispatch(setAccountObject(account));
-        setAccount(account);
-      })
-      .catch((e) => console.log(e));
-    // return () => controller.abort() // crash on React18
-  }, [accountId, dispatch]);
+        try {
+          response = await fetch(url, {
+            method: 'GET',
+            mode: 'cors',
+            headers: {
+              Authorization: `Bearer ${accessToken}`,
+            },
+            referrerPolicy: 'no-referrer',
+          });
+        } catch (err) {
+          setAccessToken('');
+          dispatch(setAccount(undefined));
+          navigate('/', { replace: true });
+          return;
+        }
 
+        if (response.status === HttpStatusCode.Ok) {
+          const serializedAccount = await response.json();
+          const account = Account.from(serializedAccount);
+          dispatch(setAccount(account));
+        } else if (response.status === HttpStatusCode.Unauthorized) {
+          setAccessToken('');
+          dispatch(setAccount(undefined));
+          navigate('/', { replace: true });
+        }
+      };
+
+      getAccount();
+    }
+  }, [accessToken, dispatch, navigate]);
+
+  // TODO: Improve component and sub-components UI.
   return (
     <Container maxWidth="sm">
       <Header />
-      {account != null ? (
-        <AccountPreferences account={account} onAccountChanged={(account: Account) => setAccount(account)} />
-      ) : (
-        <CircularProgress />
-      )}
+      {account ? <AccountPreferences account={account} /> : <ProcessingRequest open={true} />}
     </Container>
   );
-};
-
-export default AccountSettings;
+}
diff --git a/client/src/pages/Home.tsx b/client/src/pages/Home.tsx
index 8a5e3d6..cdebb42 100644
--- a/client/src/pages/Home.tsx
+++ b/client/src/pages/Home.tsx
@@ -18,8 +18,10 @@
 import { Box, Grid, Paper, useMediaQuery } from '@mui/material';
 import { Theme, useTheme } from '@mui/material/styles';
 import { useState } from 'react';
+import { Navigate } from 'react-router-dom';
 
 import JamiWelcomeLogo from '../components/JamiWelcomeLogo';
+import { getAccessToken } from '../utils/auth';
 import JamiLogin from './JamiLogin';
 import JamiRegistration from './JamiRegistration';
 
@@ -38,6 +40,11 @@
   const isDesktopOrLaptop: boolean = useMediaQuery(theme.breakpoints.up('md'));
   const isMobile: boolean = useMediaQuery(theme.breakpoints.only('xs'));
 
+  const accessToken = getAccessToken();
+
+  if (accessToken) {
+    return <Navigate to="/settings" replace />;
+  }
   return (
     <Box
       sx={{
diff --git a/client/src/pages/JamiLogin.tsx b/client/src/pages/JamiLogin.tsx
index 66fcf3e..06a607b 100644
--- a/client/src/pages/JamiLogin.tsx
+++ b/client/src/pages/JamiLogin.tsx
@@ -17,12 +17,16 @@
  */
 import { Box, Button, Stack, Typography, useMediaQuery } from '@mui/material';
 import { Theme, useTheme } from '@mui/material/styles';
-import { ChangeEvent, FormEvent, MouseEvent, useState } from 'react';
-import { Form } from 'react-router-dom';
+import { ChangeEvent, FormEvent, MouseEvent, ReactNode, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { Form, useNavigate } from 'react-router-dom';
 
+import { AlertSnackbar } from '../components/AlertSnackbar';
 import { PasswordInput, UsernameInput } from '../components/Input';
 import ProcessingRequest from '../components/ProcessingRequest';
+import { loginUser, setAccessToken } from '../utils/auth';
 import { inputWidth } from '../utils/constants';
+import { InvalidPassword, UsernameNotFound } from '../utils/errors';
 
 type JamiLoginProps = {
   register: () => void;
@@ -30,9 +34,13 @@
 
 export default function JamiLogin(props: JamiLoginProps) {
   const theme: Theme = useTheme();
+  const navigate = useNavigate();
+  const { t } = useTranslation();
+
   const [username, setUsername] = useState<string>('');
   const [password, setPassword] = useState<string>('');
   const [isLoggingInUser, setIsLoggingInUser] = useState<boolean>(false);
+  const [errorAlertContent, setErrorAlertContent] = useState<ReactNode>(undefined);
 
   const handleUsername = (event: ChangeEvent<HTMLInputElement>) => {
     setUsername(event.target.value);
@@ -52,10 +60,20 @@
     if (username.length > 0 && password.length > 0) {
       setIsLoggingInUser(true);
 
-      // TODO: Replace with login logic (https://git.jami.net/savoirfairelinux/jami-web/-/issues/75).
-      await new Promise((resolve) => setTimeout(resolve, 2000));
-      console.log('Login');
-      setIsLoggingInUser(false);
+      try {
+        const accessToken = await loginUser(username, password);
+        setAccessToken(accessToken);
+        navigate('/settings', { replace: true });
+      } catch (err) {
+        setIsLoggingInUser(false);
+        if (err instanceof UsernameNotFound) {
+          setErrorAlertContent(t('login_username_not_found'));
+        } else if (err instanceof InvalidPassword) {
+          setErrorAlertContent(t('login_invalid_password'));
+        } else {
+          throw err;
+        }
+      }
     }
   };
 
@@ -65,6 +83,10 @@
     <>
       <ProcessingRequest open={isLoggingInUser} />
 
+      <AlertSnackbar severity={'error'} open={!!errorAlertContent} onClose={() => setErrorAlertContent(undefined)}>
+        {errorAlertContent}
+      </AlertSnackbar>
+
       <Stack
         sx={{
           minHeight: `${isMobile ? 'auto' : '100%'}`,
diff --git a/client/src/pages/JamiRegistration.tsx b/client/src/pages/JamiRegistration.tsx
index ebfc5fd..2e8d23d 100644
--- a/client/src/pages/JamiRegistration.tsx
+++ b/client/src/pages/JamiRegistration.tsx
@@ -17,14 +17,23 @@
  */
 import { Box, Button, Stack, Typography, useMediaQuery } from '@mui/material';
 import { Theme, useTheme } from '@mui/material/styles';
-import { ChangeEvent, FormEvent, MouseEvent, useEffect, useState } from 'react';
+import { ChangeEvent, FormEvent, MouseEvent, ReactNode, useEffect, useState } from 'react';
 import { useTranslation } from 'react-i18next';
-import { Form } from 'react-router-dom';
+import { Form, useNavigate } from 'react-router-dom';
 
+import { AlertSnackbar } from '../components/AlertSnackbar';
 import { PasswordInput, UsernameInput } from '../components/Input';
 import ProcessingRequest from '../components/ProcessingRequest';
-import { checkPasswordStrength, isNameRegistered, StrengthValueCode } from '../utils/auth';
+import {
+  checkPasswordStrength,
+  isNameRegistered,
+  loginUser,
+  registerUser,
+  setAccessToken,
+  StrengthValueCode,
+} from '../utils/auth';
 import { inputWidth, jamiUsernamePattern } from '../utils/constants';
+import { InvalidPassword, UsernameNotFound } from '../utils/errors';
 
 const usernameTooltipTitle =
   'Choose a password hard to guess for others but easy to remember for you, ' +
@@ -44,14 +53,20 @@
 
 export default function JamiRegistration(props: JamiRegistrationProps) {
   const theme: Theme = useTheme();
+  const navigate = useNavigate();
   const { t } = useTranslation();
 
   const [isCreatingUser, setIsCreatingUser] = useState(false);
-  const [usernameValue, setUsernameValue] = useState('');
-  const [passwordValue, setPasswordValue] = useState('');
+
+  const [username, setUsername] = useState('');
+  const [password, setPassword] = useState('');
+
   const [usernameStatus, setUsernameStatus] = useState<NameStatus>('default');
   const [passwordStatus, setPasswordStatus] = useState<PasswordStatus>('default');
 
+  const [errorAlertContent, setErrorAlertContent] = useState<ReactNode>(undefined);
+  const [successAlertContent, setSuccessAlertContent] = useState<ReactNode>(undefined);
+
   const usernameError = usernameStatus !== 'success' && usernameStatus !== 'default';
   const usernameSuccess = usernameStatus === 'success';
   const passwordError = passwordStatus !== 'strong' && passwordStatus !== 'default';
@@ -59,9 +74,9 @@
 
   useEffect(() => {
     // To prevent lookup if field is empty, in error state or lookup already done
-    if (usernameValue.length > 0 && usernameStatus === 'default') {
+    if (username.length > 0 && usernameStatus === 'default') {
       const validateUsername = async () => {
-        if (await isNameRegistered(usernameValue)) {
+        if (await isNameRegistered(username)) {
           setUsernameStatus('taken');
         } else {
           setUsernameStatus('success');
@@ -71,13 +86,36 @@
 
       return () => clearTimeout(timeout);
     }
-  }, [usernameValue, usernameStatus]);
+  }, [username, usernameStatus]);
+
+  const firstUserLogin = async () => {
+    try {
+      const accessToken = await loginUser(username, password);
+      setAccessToken(accessToken);
+      navigate('/settings', { replace: true });
+    } catch (err) {
+      setIsCreatingUser(false);
+      if (err instanceof UsernameNotFound) {
+        setErrorAlertContent(t('login_username_not_found'));
+      } else if (err instanceof InvalidPassword) {
+        setErrorAlertContent(t('login_invalid_password'));
+      } else {
+        throw err;
+      }
+    }
+  };
+
+  const createAccount = async () => {
+    await registerUser(username, password);
+    setSuccessAlertContent(t('registration_success'));
+    await firstUserLogin();
+  };
 
   const handleUsername = async (event: ChangeEvent<HTMLInputElement>) => {
-    const username: string = event.target.value;
-    setUsernameValue(username);
+    const usernameValue: string = event.target.value;
+    setUsername(usernameValue);
 
-    if (username.length > 0 && !jamiUsernamePattern.test(username)) {
+    if (usernameValue.length > 0 && !jamiUsernamePattern.test(usernameValue)) {
       setUsernameStatus('invalid');
     } else {
       setUsernameStatus('default');
@@ -85,11 +123,11 @@
   };
 
   const handlePassword = (event: ChangeEvent<HTMLInputElement>) => {
-    const password: string = event.target.value;
-    setPasswordValue(password);
+    const passwordValue: string = event.target.value;
+    setPassword(passwordValue);
 
-    if (password.length > 0) {
-      const checkResult = checkPasswordStrength(password);
+    if (passwordValue.length > 0) {
+      const checkResult = checkPasswordStrength(passwordValue);
       setPasswordStatus(checkResult.valueCode);
     } else {
       setPasswordStatus('default');
@@ -107,12 +145,9 @@
 
     if (canCreate) {
       setIsCreatingUser(true);
-      // TODO: Replace with registration logic (https://git.jami.net/savoirfairelinux/jami-web/-/issues/75).
-      await new Promise((resolve) => setTimeout(resolve, 2000));
-      console.log('Account created');
-      setIsCreatingUser(false);
+      createAccount();
     } else {
-      if (usernameError || usernameValue.length === 0) {
+      if (usernameError || username.length === 0) {
         setUsernameStatus('registration_failed');
       }
       if (!passwordSuccess) {
@@ -127,6 +162,18 @@
     <>
       <ProcessingRequest open={isCreatingUser} />
 
+      <AlertSnackbar
+        severity={'success'}
+        open={!!successAlertContent}
+        onClose={() => setSuccessAlertContent(undefined)}
+      >
+        {successAlertContent}
+      </AlertSnackbar>
+
+      <AlertSnackbar severity={'error'} open={!!errorAlertContent} onClose={() => setErrorAlertContent(undefined)}>
+        {errorAlertContent}
+      </AlertSnackbar>
+
       <Stack
         sx={{
           minHeight: `${isMobile ? 'auto' : '100%'}`,
@@ -144,7 +191,7 @@
         <Form method="post" id="register-form">
           <div>
             <UsernameInput
-              value={usernameValue}
+              value={username}
               onChange={handleUsername}
               error={usernameError}
               success={usernameSuccess}
@@ -155,7 +202,7 @@
           </div>
           <div>
             <PasswordInput
-              value={passwordValue}
+              value={password}
               onChange={handlePassword}
               error={passwordError}
               success={passwordSuccess}
diff --git a/client/src/pages/Messenger.tsx b/client/src/pages/Messenger.tsx
index 6aeb840..0d13d5a 100644
--- a/client/src/pages/Messenger.tsx
+++ b/client/src/pages/Messenger.tsx
@@ -37,7 +37,7 @@
 };
 
 const Messenger = (props: MessengerProps) => {
-  const { refresh } = useAppSelector((state) => state.app);
+  const { refresh } = useAppSelector((state) => state.userInfo);
 
   const [conversations, setConversations] = useState<Conversation[] | undefined>(undefined);
   const [searchQuery, setSearchQuery] = useState('');
diff --git a/client/src/redux/appSlice.ts b/client/src/redux/appSlice.ts
index d5c01ee..b04d5db 100644
--- a/client/src/redux/appSlice.ts
+++ b/client/src/redux/appSlice.ts
@@ -19,29 +19,32 @@
 import { Account } from 'jami-web-common';
 
 // Define a type for the slice state
-export interface appState {
+export interface AppState {
+  // TODO: Remove accountId when account endpoints available.
+  // Left for backwards compatibility, not necessary, included in token.
   accountId: string;
-  accountObject: Account | null;
+  account?: Account;
+  // TODO : Evaluate need for this when WebSocket will be available.
   refresh: boolean;
 }
 
 // Define the initial state using that type
-const initialState: appState = {
+const initialState: AppState = {
   accountId: '',
-  accountObject: null,
+  account: undefined,
   refresh: true,
 };
 
-export const appSlice = createSlice({
-  name: 'app',
+export const userInfoSlice = createSlice({
+  name: 'userInfo',
   // `createSlice` will infer the state type from the `initialState` argument
   initialState,
   reducers: {
     setAccountId: (state, action: PayloadAction<string>) => {
       state.accountId = action.payload;
     },
-    setAccountObject: (state, action: PayloadAction<Account>) => {
-      state.accountObject = action.payload;
+    setAccount: (state, action: PayloadAction<Account | undefined>) => {
+      state.account = action.payload;
     },
     setRefreshFromSlice: (state) => {
       state.refresh = !state.refresh;
@@ -49,9 +52,9 @@
   },
 });
 
-export const { setAccountId, setAccountObject, setRefreshFromSlice } = appSlice.actions;
+export const { setAccountId, setAccount, setRefreshFromSlice } = userInfoSlice.actions;
 
 // Other code such as selectors can use the imported `RootState` type
 // export const selectCount = (state: RootState) => state.app.value;
 
-export default appSlice.reducer;
+export default userInfoSlice.reducer;
diff --git a/client/src/redux/store.ts b/client/src/redux/store.ts
index be2b4a5..d0dbd89 100644
--- a/client/src/redux/store.ts
+++ b/client/src/redux/store.ts
@@ -21,7 +21,7 @@
 
 export const store = configureStore({
   reducer: {
-    app: appReducer,
+    userInfo: appReducer,
   },
 });
 
diff --git a/client/src/utils/auth.ts b/client/src/utils/auth.ts
index 98ecdba..8404eee 100644
--- a/client/src/utils/auth.ts
+++ b/client/src/utils/auth.ts
@@ -16,9 +16,11 @@
  * <https://www.gnu.org/licenses/>.
  */
 import { passwordStrength } from 'check-password-strength';
-import { HttpStatusCode, LookupResolveValue } from 'jami-web-common';
+import { HttpStatusCode } from 'jami-web-common';
 
 import { PasswordStrength } from '../enums/password-strength';
+import { apiUrl } from './constants';
+import { InvalidPassword, UsernameNotFound } from './errors';
 
 interface PasswordStrengthResult {
   id: number;
@@ -36,21 +38,17 @@
 
 const idToStrengthValueCode: StrengthValueCode[] = ['too_weak', 'weak', 'medium', 'strong'];
 
-// TODO: Find a way to do it differently or remove this check from account creation.
-// It doesn't work if the server has secured this path, so I tweaked the server for test.
-// The tweak is to remove secured of apiRouter middleware in the server (app.ts).
 export async function isNameRegistered(name: string): Promise<boolean> {
-  try {
-    const response: Response = await fetch(`api/ns/name/${name}`);
-    if (response.status === HttpStatusCode.Ok) {
-      const data: LookupResolveValue = await response.json();
-      return data.name === name;
-    } else if (response.status === HttpStatusCode.NotFound) {
+  const url = new URL(`/ns/username/${name}`, apiUrl);
+  const response = await fetch(url);
+
+  switch (response.status) {
+    case HttpStatusCode.Ok:
+      return true;
+    case HttpStatusCode.NotFound:
       return false;
-    }
-    return true;
-  } catch (err) {
-    return true;
+    default:
+      throw new Error(await response.text());
   }
 }
 
@@ -64,3 +62,52 @@
 
   return checkResult;
 }
+
+export async function registerUser(username: string, password: string): Promise<void> {
+  const url = new URL('/auth/new-account', apiUrl);
+  const response: Response = await fetch(url, {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+    },
+    body: JSON.stringify({ username, password }),
+  });
+
+  if (response.status !== HttpStatusCode.Created) {
+    throw new Error(await response.text());
+  }
+}
+
+export async function loginUser(username: string, password: string): Promise<string> {
+  const url = new URL('/auth/login', apiUrl);
+  const response = await fetch(url, {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+    },
+    body: JSON.stringify({ username, password }),
+  });
+
+  switch (response.status) {
+    case HttpStatusCode.Ok:
+      break;
+    case HttpStatusCode.NotFound:
+      throw new UsernameNotFound();
+    case HttpStatusCode.Unauthorized:
+      throw new InvalidPassword();
+    default:
+      throw new Error(await response.text());
+  }
+
+  const data: { accessToken: string } = await response.json();
+  return data.accessToken;
+}
+
+export function getAccessToken(): string {
+  const accessToken: string | null = localStorage.getItem('accessToken');
+  return accessToken ?? '';
+}
+
+export function setAccessToken(accessToken: string): void {
+  localStorage.setItem('accessToken', accessToken);
+}
diff --git a/client/src/utils/constants.ts b/client/src/utils/constants.ts
index 341622f..f5c5423 100644
--- a/client/src/utils/constants.ts
+++ b/client/src/utils/constants.ts
@@ -20,3 +20,5 @@
 export const inputWidth = 260;
 
 export const jamiLogoDefaultSize = '512px';
+
+export const apiUrl = new URL(import.meta.env.VITE_API_URL);
diff --git a/client/src/utils/errors.ts b/client/src/utils/errors.ts
new file mode 100644
index 0000000..30ec7aa
--- /dev/null
+++ b/client/src/utils/errors.ts
@@ -0,0 +1,20 @@
+/*
+ * 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 UsernameNotFound extends Error {}
+
+export class InvalidPassword extends Error {}
diff --git a/client/vite.config.ts b/client/vite.config.ts
index f62be3c..e984a81 100644
--- a/client/vite.config.ts
+++ b/client/vite.config.ts
@@ -24,7 +24,7 @@
     host: '0.0.0.0',
     port: 3000,
     proxy: {
-      '^/(api)|^/(auth)|^/(setup)': {
+      '^/(api)|^/(setup)': {
         target: 'http://localhost:3001',
         secure: false,
       },