Skip to content

Commit

Permalink
Merge pull request #176 from thisyahlen-deriv/thisyahlen/implement-oi…
Browse files Browse the repository at this point in the history
…dc-logic

feat: oidc implementation
  • Loading branch information
shafin-deriv authored Dec 9, 2024
2 parents 15339ab + 3e6c4ff commit fd00b8b
Show file tree
Hide file tree
Showing 16 changed files with 249 additions and 31 deletions.
26 changes: 15 additions & 11 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"dependencies": {
"@deriv-com/quill-ui": "^1.16.21",
"@deriv-com/analytics": "^1.22.1",
"@deriv-com/auth-client": "^1.0.29",
"@deriv-com/auth-client": "^1.3.1",
"@deriv/deriv-api": "^1.0.11",
"@radix-ui/react-tooltip": "^1.0.7",
"@react-spring/web": "^9.7.3",
Expand Down
39 changes: 37 additions & 2 deletions src/components/UserNavbarItem/item.desktop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import useDeviceType from '@site/src/hooks/useDeviceType';

import { IUserNavbarItemProps } from './item.types';
import styles from './UserNavbarItem.module.scss';
import { useHandleLogin } from '@site/src/hooks/useHandleLogin';
import Cookies from 'js-cookie';

interface IActionProps {
handleClick: () => void;
Expand Down Expand Up @@ -62,12 +64,16 @@ const DashboardActions: React.FC<IActionProps> = ({ handleClick, isDesktop }) =>
const SignedInActions: React.FC<IActionProps> = ({ handleClick, isDesktop }) => {
const signedInButtonClasses = clsx('navbar__item', styles.UserNavbarItem, styles.SignedInButton);

const { handleLogin } = useHandleLogin({
onClickLogin: handleClick,
});

return (
<nav className='right-navigation'>
<Button
variant='secondary'
color='black'
onClick={handleClick}
onClick={handleLogin}
className={signedInButtonClasses}
data-testid='sa_login'
>
Expand All @@ -88,14 +94,43 @@ const SignedInActions: React.FC<IActionProps> = ({ handleClick, isDesktop }) =>
};

const UserNavbarDesktopItem = ({ authUrl, is_logged_in }: IUserNavbarItemProps) => {
const { logout } = useLogout();
const { deviceType } = useDeviceType();
const isDesktop = deviceType === 'desktop';

const handleClick = () => {
location.assign(authUrl);
};

const { handleLogin, isOAuth2Enabled } = useHandleLogin({
onClickLogin: handleClick,
});

const { logout } = useLogout();

const loggedState = Cookies.get('logged_state');

const loginAccountsSessionStorage = JSON.parse(sessionStorage.getItem('login-accounts'));

const isLoginAccountsPopulated =
loginAccountsSessionStorage && loginAccountsSessionStorage.length > 0;

React.useEffect(() => {
if (
loggedState === 'true' &&
isOAuth2Enabled &&
!isLoginAccountsPopulated &&
window.location.pathname !== '/callback'
) {
console.log('isLoginAccountsPopulated', !isLoginAccountsPopulated);

handleLogin();
}

if (loggedState === 'false' && isOAuth2Enabled && isLoginAccountsPopulated) {
logout();
}
}, [isOAuth2Enabled, loggedState, logout, handleLogin, isLoginAccountsPopulated]);

return is_logged_in ? (
<DashboardActions handleClick={logout} isDesktop={isDesktop} />
) : (
Expand Down
7 changes: 6 additions & 1 deletion src/features/Apiexplorer/LoginDialog/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import useLoginUrl from '@site/src/hooks/useLoginUrl';
import styles from './LoginDialog.module.scss';
import Translate, { translate } from '@docusaurus/Translate';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import { useHandleLogin } from '@site/src/hooks/useHandleLogin';

type TLoginDialog = {
setToggleModal: React.Dispatch<React.SetStateAction<boolean>>;
Expand All @@ -25,6 +26,10 @@ export const LoginDialog = ({ setToggleModal }: TLoginDialog) => {
location.assign(getUrl(currentLocale));
};

const { handleLogin } = useHandleLogin({
onClickLogin: handleClick,
});

const handleSignUp = () => {
location.assign('https://deriv.com/signup/');
};
Expand Down Expand Up @@ -57,7 +62,7 @@ export const LoginDialog = ({ setToggleModal }: TLoginDialog) => {
<Button color='tertiary' onClick={handleSignUp} className={styles.btn}>
<Translate>Sign up</Translate>
</Button>
<Button color='primary' onClick={handleClick} className={styles.btn}>
<Button color='primary' onClick={handleLogin} className={styles.btn}>
<Translate>Log in</Translate>
</Button>
</div>
Expand Down
22 changes: 22 additions & 0 deletions src/features/Callback/CallbackPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from 'react';
import { Callback } from '@deriv-com/auth-client';
import { transformAccountsFromResponseBody } from '@site/src/utils';
import useAuthContext from '@site/src/hooks/useAuthContext';

const CallbackPage = () => {
const { updateLoginAccounts } = useAuthContext();

return (
<Callback
onSignInSuccess={(tokens) => {
const accounts = transformAccountsFromResponseBody(tokens);

updateLoginAccounts(accounts);

window.location.href = '/';
}}
/>
);
};

export default CallbackPage;
3 changes: 3 additions & 0 deletions src/features/Callback/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import CallbackPage from './CallbackPage';

export default CallbackPage;
8 changes: 7 additions & 1 deletion src/features/Login/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import useLoginUrl from '@site/src/hooks/useLoginUrl';
import Footer from '@site/src/components/Footer';
import Translate from '@docusaurus/Translate';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import { useHandleLogin } from '@site/src/hooks/useHandleLogin';

export const Login = () => {
const { getUrl } = useLoginUrl();
Expand All @@ -15,6 +16,11 @@ export const Login = () => {
const handleClick = () => {
window.location.assign(getUrl(currentLocale));
};

const { handleLogin } = useHandleLogin({
onClickLogin: handleClick,
});

return (
<div>
<div className={styles.login} data-testid='login'>
Expand All @@ -26,7 +32,7 @@ export const Login = () => {
</Translate>
</Text>
<div className={styles.action}>
<Button color='primary' onClick={handleClick}>
<Button color='primary' onClick={handleLogin}>
<Translate>Log In</Translate>
</Button>
</div>
Expand Down
6 changes: 6 additions & 0 deletions src/features/dashboard/__tests__/dashboard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ mockReactTable.mockImplementation(() => ({
headerGroups: [],
}));

jest.mock('@docusaurus/BrowserOnly', () => ({
__esModule: true,
default: ({ children }: { children: () => JSX.Element }) => children(),
}));

describe('AppManager', () => {
it('shows the login screen', () => {
mockUseAuthContext.mockImplementation(() => ({
Expand All @@ -59,6 +64,7 @@ describe('AppManager', () => {
const login = screen.getByText(
/Log in to your Deriv account to get the API token and start using our API./i,
);

expect(login).toBeInTheDocument();
});

Expand Down
3 changes: 2 additions & 1 deletion src/features/dashboard/dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import useAuthContext from '@site/src/hooks/useAuthContext';
import useAppManager from '@site/src/hooks/useAppManager';
import ManageDashboard from './manage-dashboard';
import { Login } from '../Login/Login';
import BrowserOnly from '@docusaurus/BrowserOnly';

const Dashboard = () => {
const { is_logged_in } = useAuthContext();
Expand All @@ -16,7 +17,7 @@ const Dashboard = () => {
}, [setIsDashboard]);

if (is_logged_in) return <ManageDashboard />;
return <Login />;
return <BrowserOnly>{() => <Login />}</BrowserOnly>;
};

export default Dashboard;
39 changes: 39 additions & 0 deletions src/hooks/useHandleLogin/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import useGrowthbookGetFeatureValue from '../useGrowthbookGetFeatureValue/';
import {
requestOidcAuthentication,
TOAuth2EnabledAppList,
useIsOAuth2Enabled,
} from '@deriv-com/auth-client';

/**
* Handles the new login flow for the user using OIDC.
*
* If the user is not logged in and OAuth2 is enabled, it will redirect the user to the
* OAuth2 authorization page from the OIDC config endpoint. If OAuth2 is not enabled it will
* redirect the user to the legacy oauth url coming from the onClickLogin callback.
*
* @param {Object} props - The props object.
* @param {Function} props.onClickLogin - The callback to be called when the user is logged in.
* @returns {Object} - An object with the `handleLogin` function.
*/
export const useHandleLogin = ({ onClickLogin }: { onClickLogin?: () => void }) => {
const [OAuth2EnabledApps, OAuth2EnabledAppsInitialised] =
useGrowthbookGetFeatureValue<TOAuth2EnabledAppList>({
featureFlag: 'hydra_be',
});

const isOAuth2Enabled = useIsOAuth2Enabled(OAuth2EnabledApps, OAuth2EnabledAppsInitialised);

const handleLogin = async () => {
if (isOAuth2Enabled) {
await requestOidcAuthentication({
redirectCallbackUri: `${window.location.origin}/callback`,
});
}
if (onClickLogin) {
onClickLogin();
}
};

return { handleLogin, isOAuth2Enabled };
};
16 changes: 16 additions & 0 deletions src/hooks/useLogout/__tests__/useLogout.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,21 @@ mockUseAuthContext.mockImplementation(() => ({
updateCurrentLoginAccount: mockUpdateCurrentLoginAccount,
}));

jest.mock('@deriv-com/auth-client', () => ({
OAuth2Logout: jest.fn((WSLogoutAndRedirect) => {
const mockIframe = document.createElement('iframe');
mockIframe.id = 'logout-iframe';
document.body.appendChild(mockIframe);

setTimeout(() => {
const event = new MessageEvent('message', { data: 'logout_complete' });
window.dispatchEvent(event);
}, 100);

WSLogoutAndRedirect();
}),
}));

const logout_response = {
logout: 1,
req_id: 1,
Expand All @@ -44,6 +59,7 @@ describe('Login', () => {
});

await expect(wsServer).toReceiveMessage(logout_response);

wsServer.send({ logout: 1 });

expect(mockUpdateLoginAccounts).toBeCalledTimes(1);
Expand Down
13 changes: 5 additions & 8 deletions src/hooks/useLogout/index.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
import apiManager from '@site/src/configs/websocket';
import { useCallback } from 'react';
import useAuthContext from '../useAuthContext';
import useGrowthbookGetFeatureValue from '../useGrowthbookGetFeatureValue';
import { useOAuth2, TOAuth2EnabledAppList } from '@deriv-com/auth-client';
import { OAuth2Logout } from '@deriv-com/auth-client';

const useLogout = () => {
const { updateLoginAccounts, updateCurrentLoginAccount } = useAuthContext();
const [OAuth2EnabledApps, OAuth2EnabledAppsInitialised] =
useGrowthbookGetFeatureValue<TOAuth2EnabledAppList>({
featureFlag: 'hydra_be',
});

// we clean up everything related to the user here, for now it's just user's account
// later on we should clear user tokens as well
Expand All @@ -23,9 +18,11 @@ const useLogout = () => {
});
}, [updateCurrentLoginAccount, updateLoginAccounts]);

const { OAuth2Logout } = useOAuth2({ OAuth2EnabledApps, OAuth2EnabledAppsInitialised }, logout);
const handleLogout = () => {
OAuth2Logout(logout);
};

return { logout: OAuth2Logout };
return { logout: handleLogout };
};

export default useLogout;
5 changes: 3 additions & 2 deletions src/pages/auth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import useAuthParams from '../hooks/useAuthParams';
import { useEffect } from 'react';
import { Redirect, useLocation } from '@docusaurus/router';
import useAuthContext from '../hooks/useAuthContext';
import BrowserOnly from '@docusaurus/BrowserOnly';

export default function Auth(): JSX.Element {
const { search } = useLocation(); // to get the search params
Expand All @@ -19,7 +20,7 @@ export default function Auth(): JSX.Element {
useEffect(() => {
if (is_logged_in) {
const params = new URLSearchParams(search);
const redirect_route = params.get('route')?.replace(/%2F/g, '/') || '/';
const redirect_route = params.get('route')?.replace(/%2F/g, '/') || '/';
setRedirectRoute(redirect_route);
}
}, [is_logged_in, search]);
Expand All @@ -31,7 +32,7 @@ export default function Auth(): JSX.Element {
return (
<Layout title='Auth' description='Deriv API documentation'>
<main>
<Login />
<BrowserOnly>{() => <Login />}</BrowserOnly>
</main>
</Layout>
);
Expand Down
Loading

0 comments on commit fd00b8b

Please sign in to comment.