Add AuthProvider to provide account and token
When logged in, the token and account info can be retrieved using
`useAuthContext` hook.
Fix jamid.node path in jamid.ts
Fix some eslint warnings.
Change-Id: I1ea4d537693df807b4ea67a277addfecfc749e4a
diff --git a/client/src/components/AccountPreferences.tsx b/client/src/components/AccountPreferences.tsx
index 2eedc97..8ca098e 100644
--- a/client/src/components/AccountPreferences.tsx
+++ b/client/src/components/AccountPreferences.tsx
@@ -35,11 +35,11 @@
Typography,
} from '@mui/material';
import { motion } from 'framer-motion';
-import { Account } from 'jami-web-common';
-import { AccountDetails } from 'jami-web-common';
+import { Account, AccountDetails } from 'jami-web-common';
import { useState } from 'react';
import authManager from '../AuthManager';
+import { useAuthContext } from '../contexts/AuthProvider';
import ConversationAvatar from './ConversationAvatar';
import ConversationsOverviewCard from './ConversationsOverviewCard';
import JamiIdCard from './JamiIdCard';
@@ -57,10 +57,17 @@
};
type AccountPreferencesProps = {
- account: Account;
+ // TODO: Remove account prop after migration to new server
+ account?: Account;
};
-export default function AccountPreferences({ account }: AccountPreferencesProps) {
+export default function AccountPreferences({ account: _account }: AccountPreferencesProps) {
+ const authContext = useAuthContext(true);
+ const account = _account ?? authContext?.account;
+ if (!account) {
+ throw new Error('Account not defined');
+ }
+
const devices: string[][] = [];
const accountDevices = account.getDevices();
for (const i in accountDevices) devices.push([i, accountDevices[i]]);
@@ -241,7 +248,7 @@
</ListItemAvatar>
<ListItemText primary={moderator.getDisplayName()} />
<ListItemSecondaryAction>
- <IconButton onClick={(e) => removeModerator(moderator.getUri())} size="large">
+ <IconButton onClick={() => removeModerator(moderator.getUri())} size="large">
<DeleteRounded />
</IconButton>
</ListItemSecondaryAction>
diff --git a/client/src/components/Button.tsx b/client/src/components/Button.tsx
index 9264355..c33def2 100644
--- a/client/src/components/Button.tsx
+++ b/client/src/components/Button.tsx
@@ -213,7 +213,7 @@
<IconButton {...props} disableRipple={true}>
<Icon fontSize="inherit" />
</IconButton>
-))(({ theme }) => ({
+))(() => ({
color: '#7E7E7E',
fontSize: '25px',
height: '36px',
@@ -278,7 +278,7 @@
<IconButton {...props} disableRipple={true}>
{emoji}
</IconButton>
-))(({ theme }) => ({
+))(() => ({
color: 'white',
fontSize: '20px',
height: '20px',
@@ -288,7 +288,7 @@
type SelectEmojiButtonProps = {
onEmojiSelected: (emoji: string) => void;
};
-export const SelectEmojiButton = ({ onEmojiSelected, ...props }: SelectEmojiButtonProps) => {
+export const SelectEmojiButton = ({ onEmojiSelected }: SelectEmojiButtonProps) => {
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
const handleOpenEmojiPicker = useCallback(
diff --git a/client/src/components/ContactList.jsx b/client/src/components/ContactList.jsx
index 56f5f9c..c35c4f0 100644
--- a/client/src/components/ContactList.jsx
+++ b/client/src/components/ContactList.jsx
@@ -37,7 +37,7 @@
};
export default function ContactList() {
- const { accountId, account } = useAppSelector((state) => state.userInfo);
+ const { accountId } = useAppSelector((state) => state.userInfo);
const dispatch = useAppDispatch();
const [contacts, setContacts] = useState([]);
diff --git a/client/src/components/ConversationListItem.tsx b/client/src/components/ConversationListItem.tsx
index b432a45..9a928e7 100644
--- a/client/src/components/ConversationListItem.tsx
+++ b/client/src/components/ConversationListItem.tsx
@@ -92,8 +92,8 @@
const [modalDetailsIsOpen, setModalDetailsIsOpen] = useState(false);
const [modalDeleteIsOpen, setModalDeleteIsOpen] = useState(false);
const [blockOrRemove, setBlockOrRemove] = useState(true);
- const [userId, setUserId] = useState(conversation?.getFirstMember()?.contact.getUri());
- const [isSwarm, setIsSwarm] = useState(true);
+ const [userId] = useState(conversation?.getFirstMember()?.contact.getUri());
+ const [isSwarm] = useState(true);
const navigateUrlPrefix = `/deprecated-account/${conversation.getAccountId()}`;
@@ -134,7 +134,7 @@
method: 'DELETE',
})
.then((res) => res.json())
- .then((result) => {
+ .then(() => {
console.log('propre');
dispatch(setRefreshFromSlice());
})
diff --git a/client/src/components/Header.tsx b/client/src/components/Header.tsx
index 2a00e7f..445dbae 100644
--- a/client/src/components/Header.tsx
+++ b/client/src/components/Header.tsx
@@ -19,9 +19,12 @@
import { MouseEvent, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
+import { useAuthContext } from '../contexts/AuthProvider';
import { setAccessToken } from '../utils/auth';
export default function Header() {
+ const authContext = useAuthContext(true);
+
const navigate = useNavigate();
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
const handleClick = (event: MouseEvent<HTMLButtonElement>) => setAnchorEl(event.currentTarget);
@@ -31,10 +34,12 @@
const goToContacts = () => navigate(`/contacts`);
const goToAccountSettings = () => navigate(`/deprecated-account/${params.accountId}/settings`);
- const logout = () => {
+ const deprecatedLogout = () => {
setAccessToken('');
- navigate('/', { replace: true });
+ navigate('/deprecated-account', { replace: true });
};
+ // TODO: Remove deprecated_logout once migration to new server is complete
+ const logout = authContext?.logout ?? deprecatedLogout;
return (
<Box>
diff --git a/client/src/contexts/AuthProvider.tsx b/client/src/contexts/AuthProvider.tsx
new file mode 100644
index 0000000..50b1723
--- /dev/null
+++ b/client/src/contexts/AuthProvider.tsx
@@ -0,0 +1,107 @@
+/*
+ * 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 { Account } from 'jami-web-common/dist/Account';
+import { HttpStatusCode } from 'jami-web-common/dist/enums/http-status-code';
+import { createContext, useCallback, useContext, useEffect, useState } from 'react';
+import { Outlet, useNavigate } from 'react-router-dom';
+
+import ProcessingRequest from '../components/ProcessingRequest';
+import { apiUrl } from '../utils/constants';
+
+interface IAuthContext {
+ token: string;
+ account: Account;
+ logout: () => void;
+}
+
+const AuthContext = createContext<IAuthContext | undefined>(undefined);
+
+export default () => {
+ const [token, setToken] = useState<string | undefined>();
+ const [account, setAccount] = useState<Account | undefined>();
+ const navigate = useNavigate();
+
+ const logout = useCallback(() => {
+ localStorage.removeItem('accessToken');
+ navigate('/');
+ }, [navigate]);
+
+ useEffect(() => {
+ const accessToken = localStorage.getItem('accessToken');
+
+ if (!accessToken) {
+ console.warn('Missing authentication JWT. Redirecting to login page...');
+ logout();
+ } else {
+ setToken(accessToken);
+ }
+ }, [logout]);
+
+ useEffect(() => {
+ if (token) {
+ const getAccount = async () => {
+ const url = new URL('/account', apiUrl);
+ const response = await fetch(url, {
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+
+ if (response.status === HttpStatusCode.Ok) {
+ const serializedAccount = await response.json();
+ const account = Account.from(serializedAccount);
+ setAccount(account);
+ } else {
+ throw new Error(response.statusText);
+ }
+ };
+
+ getAccount().catch((e) => {
+ console.error('Error while retrieving account: ', e);
+ logout();
+ });
+ }
+ }, [token, logout]);
+
+ if (!token || !account) {
+ return <ProcessingRequest open />;
+ }
+
+ return (
+ <AuthContext.Provider
+ value={{
+ token,
+ logout,
+ account,
+ }}
+ >
+ <Outlet />
+ </AuthContext.Provider>
+ );
+};
+
+export function useAuthContext(dontThrowIfUndefined: true): IAuthContext | undefined;
+export function useAuthContext(): IAuthContext;
+export function useAuthContext(dontThrowIfUndefined?: true) {
+ const authContext = useContext(AuthContext);
+ if (!authContext && !dontThrowIfUndefined) {
+ throw new Error('AuthContext is not provided');
+ }
+ return authContext;
+}
diff --git a/client/src/index.tsx b/client/src/index.tsx
index e95a74d..968e62b 100644
--- a/client/src/index.tsx
+++ b/client/src/index.tsx
@@ -29,15 +29,16 @@
import App from './App';
import ContactList from './components/ContactList';
+import AuthProvider from './contexts/AuthProvider';
import { SocketProvider } from './contexts/Socket';
import AccountSelection from './pages/AccountSelection';
import AccountSettings from './pages/AccountSettings';
import CallInterface from './pages/CallInterface';
import DeprecatedAccountSettings from './pages/DeprecatedAccountSettings';
-import Home from './pages/Home';
import JamiMessenger from './pages/JamiMessenger';
import Messenger from './pages/Messenger';
import ServerSetup from './pages/ServerSetup';
+import Welcome from './pages/Welcome';
import { store } from './redux/store';
import defaultTheme from './themes/Default';
import { ThemeDemonstrator } from './themes/ThemeDemonstrator';
@@ -55,11 +56,13 @@
const router = createBrowserRouter(
createRoutesFromElements(
<Route path="/" element={<App />}>
- <Route index element={<Home />} />
+ <Route index element={<Welcome />} />
<Route path="theme" element={<ThemeDemonstrator />} />
- <Route path="account" element={<JamiMessenger />} />
- <Route path="settings" element={<AccountSettings />} />
- <Route path="contacts" element={<ContactList />} />
+ <Route element={<AuthProvider />}>
+ <Route path="account" element={<JamiMessenger />} />
+ <Route path="settings" element={<AccountSettings />} />
+ <Route path="contacts" element={<ContactList />} />
+ </Route>
<Route path="setup" element={<ServerSetup />} />
{/* TODO: Remove this block after migration to new server*/}
<Route path="deprecated-account" element={<AccountSelection />} />
diff --git a/client/src/pages/AccountSelection.tsx b/client/src/pages/AccountSelection.tsx
index 708f1eb..f8daf0a 100644
--- a/client/src/pages/AccountSelection.tsx
+++ b/client/src/pages/AccountSelection.tsx
@@ -36,7 +36,7 @@
const AccountSelection = () => {
const navigate = useNavigate();
const [loaded, setLoaded] = useState(false);
- const [error, setError] = useState(false);
+ const [, setError] = useState(false);
const [accounts, setAccounts] = useState<Account[]>([]);
authManager.authenticate('admin', 'admin');
diff --git a/client/src/pages/AccountSettings.tsx b/client/src/pages/AccountSettings.tsx
index b09fb65..f7b5e4f 100644
--- a/client/src/pages/AccountSettings.tsx
+++ b/client/src/pages/AccountSettings.tsx
@@ -16,67 +16,16 @@
* <https://www.gnu.org/licenses/>.
*/
import { Container } from '@mui/material';
-import { Account, HttpStatusCode } from 'jami-web-common';
-import { useEffect } from 'react';
-import { useNavigate } from 'react-router-dom';
import AccountPreferences from '../components/AccountPreferences';
import Header from '../components/Header';
-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';
export default function AccountSettings() {
- const dispatch = useAppDispatch();
- const navigate = useNavigate();
-
- const { account } = useAppSelector((state) => state.userInfo);
- const accessToken = getAccessToken();
-
- useEffect(() => {
- if (accessToken) {
- const getAccount = async () => {
- const url = new URL('/account', apiUrl);
- let response: Response;
-
- 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 ? <AccountPreferences account={account} /> : <ProcessingRequest open={true} />}
+ <AccountPreferences />
</Container>
);
}
diff --git a/client/src/pages/DeprecatedAccountSettings.tsx b/client/src/pages/DeprecatedAccountSettings.tsx
index 4a37cd9..f69334d 100644
--- a/client/src/pages/DeprecatedAccountSettings.tsx
+++ b/client/src/pages/DeprecatedAccountSettings.tsx
@@ -23,7 +23,7 @@
import authManager from '../AuthManager';
import AccountPreferences from '../components/AccountPreferences';
import Header from '../components/Header';
-import { setAccount, setAccountId } from '../redux/appSlice';
+import { setAccountId } from '../redux/appSlice';
import { useAppDispatch } from '../redux/hooks';
type AccountSettingsProps = {
@@ -55,7 +55,6 @@
console.log(result);
const account = Account.from(result);
account.setDevices(result.devices);
- dispatch(setAccount(account));
setLocalAccount(account);
})
.catch((e) => console.log(e));
diff --git a/client/src/pages/Messenger.tsx b/client/src/pages/Messenger.tsx
index 0d13d5a..c38294a 100644
--- a/client/src/pages/Messenger.tsx
+++ b/client/src/pages/Messenger.tsx
@@ -83,7 +83,7 @@
contact.setRegisteredName(response.name);
setSearchResults(contact ? Conversation.fromSingleContact(accountId, contact) : undefined);
})
- .catch((e) => {
+ .catch(() => {
setSearchResults(undefined);
});
// return () => controller.abort() // crash on React18
diff --git a/client/src/pages/Home.tsx b/client/src/pages/Welcome.tsx
similarity index 95%
rename from client/src/pages/Home.tsx
rename to client/src/pages/Welcome.tsx
index cdebb42..59899ea 100644
--- a/client/src/pages/Home.tsx
+++ b/client/src/pages/Welcome.tsx
@@ -27,7 +27,7 @@
const borderRadius = 30;
-export default function Home() {
+export default function Welcome() {
const theme: Theme = useTheme();
const [isRegistrationDisplayed, setIsRegistrationDisplayed] = useState<boolean>(false);
@@ -90,9 +90,7 @@
sx={{ mt: theme.typography.pxToRem(30), mb: theme.typography.pxToRem(20) }}
/>
)}
- <Box className="home-child" sx={{ height: `${isMobile ? 'auto' : '100%'}` }}>
- {child}
- </Box>
+ <Box sx={{ height: `${isMobile ? 'auto' : '100%'}` }}>{child}</Box>
</Grid>
</Grid>
</Paper>
diff --git a/client/src/redux/appSlice.ts b/client/src/redux/appSlice.ts
index b04d5db..895415b 100644
--- a/client/src/redux/appSlice.ts
+++ b/client/src/redux/appSlice.ts
@@ -16,14 +16,12 @@
* <https://www.gnu.org/licenses/>.
*/
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
-import { Account } from 'jami-web-common';
// Define a type for the slice state
export interface AppState {
// TODO: Remove accountId when account endpoints available.
// Left for backwards compatibility, not necessary, included in token.
accountId: string;
- account?: Account;
// TODO : Evaluate need for this when WebSocket will be available.
refresh: boolean;
}
@@ -31,7 +29,6 @@
// Define the initial state using that type
const initialState: AppState = {
accountId: '',
- account: undefined,
refresh: true,
};
@@ -43,16 +40,13 @@
setAccountId: (state, action: PayloadAction<string>) => {
state.accountId = action.payload;
},
- setAccount: (state, action: PayloadAction<Account | undefined>) => {
- state.account = action.payload;
- },
setRefreshFromSlice: (state) => {
state.refresh = !state.refresh;
},
},
});
-export const { setAccountId, setAccount, setRefreshFromSlice } = userInfoSlice.actions;
+export const { setAccountId, setRefreshFromSlice } = userInfoSlice.actions;
// Other code such as selectors can use the imported `RootState` type
// export const selectCount = (state: RootState) => state.app.value;