Skip to content

Commit

Permalink
feat: sign-in via OIDC
Browse files Browse the repository at this point in the history
  • Loading branch information
rushtong committed Sep 9, 2024
1 parent a9b8b1f commit db33dbc
Show file tree
Hide file tree
Showing 10 changed files with 113 additions and 118 deletions.
9 changes: 2 additions & 7 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,18 +65,13 @@ function App() {
setUserIsLogged();
});

const signIn = async () => {
await Storage.setUserIsLogged(true);
await setIsLoggedIn(true);
};

return (
<div className="body">
<div className="wrap">
<div className="main">
<DuosHeader onSignIn={signIn} />
<DuosHeader/>
<Spinner name="mainSpinner" group="duos" loadingImage={loadingImage} />
<Routes onSignIn={signIn} isLogged={isLoggedIn} env={env} />
<Routes isLogged={isLoggedIn} env={env} />
</div>
</div>
<DuosFooter />
Expand Down
7 changes: 1 addition & 6 deletions src/components/DuosHeader.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,6 @@ export const headerTabsConfig = [

const NavigationTabsComponent = (props) => {
const {
onSignIn,
history,
orientation,
makeNotifications,
Expand Down Expand Up @@ -230,7 +229,6 @@ const NavigationTabsComponent = (props) => {
<SignInButton
customStyle={undefined}
props={props}
onSignIn={onSignIn}
history={history}/>
</li>}
</ul>
Expand All @@ -250,7 +248,6 @@ const NavigationTabsComponent = (props) => {
<SignInButton
customStyle={undefined}
props={props}
onSignIn={onSignIn}
history={history}/>
</div>
}
Expand Down Expand Up @@ -338,7 +335,7 @@ const navbarDuosText = {
};

const DuosHeader = (props) => {
const {location, classes, onSignIn, history} = props;
const {location, classes, history} = props;
const [state, setState] = useState({
showSupportRequestModal: false,
hover: false,
Expand Down Expand Up @@ -506,7 +503,6 @@ const DuosHeader = (props) => {
<div className="row no-margin" style={{ width: '100%' }}>
{/* Standard navbar for medium sized displays and higher (pre-existing navbar) */}
<NavigationTabsComponent
onSignIn={onSignIn}
history={history}
goToLink={goToLink}
makeNotifications={makeNotifications}
Expand Down Expand Up @@ -555,7 +551,6 @@ const DuosHeader = (props) => {
onClose={() => toggleDrawer(false)}
>
<NavigationTabsComponent
onSignIn={onSignIn}
history={history}
goToLink={goToLink}
// Notifications are already displayed underneath the expanded drawer, no need to render them twice.
Expand Down
151 changes: 63 additions & 88 deletions src/components/SignInButton.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import React, { useEffect, useState } from 'react';
import { isEmpty, isNil } from 'lodash/fp';
import { Alert } from './Alert';
import React, {useState} from 'react';
import {isEmpty, isNil} from 'lodash/fp';
import {Alert} from './Alert';
import {Auth} from '../libs/auth/auth';
import { ToS } from '../libs/ajax/ToS';
import { User } from '../libs/ajax/User';
import { Metrics } from '../libs/ajax/Metrics';
import { Config } from '../libs/config';
import { Storage } from '../libs/storage';
import { Navigation, setUserRoleStatuses } from '../libs/utils';
import {ToS} from '../libs/ajax/ToS';
import {User} from '../libs/ajax/User';
import {Metrics} from '../libs/ajax/Metrics';
import {Storage} from '../libs/storage';
import {Navigation, setUserRoleStatuses} from '../libs/utils';
import loadingIndicator from '../images/loading-indicator.svg';
import { Spinner } from './Spinner';
import ReactTooltip from 'react-tooltip';
import { GoogleIS } from '../libs/googleIS';
import eventList from '../libs/events';
import { StackdriverReporter } from '../libs/stackdriverReporter';
import { History } from 'history';
import {StackdriverReporter} from '../libs/stackdriverReporter';
import {History} from 'history';
import CSS from 'csstype';
import {OidcUser} from '../libs/auth/oidcBroker';
import {DuosUserResponse} from '../types/responseTypes';


interface SignInButtonProps {
customStyle: CSS.Properties | undefined;
Expand All @@ -30,57 +30,23 @@ interface ErrorInfo {
msg?: string;
}

type ErrorDisplay = ErrorInfo | JSX.Element;
type ErrorDisplay = ErrorInfo | React.JSX.Element;

interface HttpError extends Error {
status?: number;
}

interface GoogleSuccessPayload {
accessToken: string;
}

declare global {
interface Window { google: any; }
}

export const SignInButton = (props: SignInButtonProps) => {
const [clientId, setClientId] = useState('');
const [errorDisplay, setErrorDisplay] = useState<ErrorDisplay>({});
const { onSignIn, history, customStyle } = props;

useEffect(() => {
// Using `isSubscribed` resolves the
// "To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function." warning
let isSubscribed = true;
const init = async () => {
if (isSubscribed) {
const googleClientId = await Config.getGoogleClientId();
setClientId(googleClientId);
if (window.google !== undefined && GoogleIS.client === null) {
await GoogleIS.initTokenClient(googleClientId, onSuccess, onFailure);
}
}
ReactTooltip.rebuild();
};
init();
return () => {
isSubscribed = false;
};
});
const {onSignIn, history} = props;
const [isLoading, setIsLoading] = useState<boolean>(false);

// Utility function called in the normal success case and in the undocumented 409 case
// Check for ToS Acceptance - redirect user if not set.
// Check for ToS Acceptance - sign out and redirect user if not set.
const checkToSAndRedirect = async (redirectPath: string | null) => {
// Check if the user has accepted ToS yet or not:
const user = await User.getMe();
if (!user.roles) {
await StackdriverReporter.report('roles not found for user: ' + user.email);
}
setUserRoleStatuses(user, Storage);
await onSignIn();
const userStatus = await ToS.getStatus();
const { tosAccepted } = userStatus;
const {tosAccepted} = userStatus;
if (!isEmpty(userStatus) && !tosAccepted) {
await Auth.signOut();
if (isNil(redirectPath)) {
Expand All @@ -90,22 +56,31 @@ export const SignInButton = (props: SignInButtonProps) => {
}
} else {
if (isNil(redirectPath)) {
Navigation.back(user, history);
Navigation.back(Storage.getCurrentUser(), history);
} else {
history.push(redirectPath);
}
}
};

const onSuccess = async (response: GoogleSuccessPayload) => {
Storage.setGoogleData(response);
// eslint-disable-next-line no-unused-vars
const onSuccess = async (_: OidcUser) => {
const duosUser: DuosUserResponse = await User.getMe();
Storage.setCurrentUser(duosUser);
setUserRoleStatuses(duosUser, Storage);
if (!duosUser.roles) {
await StackdriverReporter.report('roles not found for user: ' + duosUser.email);
}

const redirectTo = getRedirectTo();
const shouldRedirect = shouldRedirectTo(redirectTo);
Storage.setAnonymousId();
await Metrics.identify(Storage.getAnonymousId());
await Metrics.syncProfile();
await Metrics.captureEvent(eventList.userSignIn);

try {
await attemptSignInCheckToSAndRedirect(redirectTo, shouldRedirect);
await checkToSAndRedirect(shouldRedirect ? redirectTo : null);
} catch (error) {
await handleRegistration(redirectTo, shouldRedirect);
}
Expand All @@ -118,13 +93,6 @@ export const SignInButton = (props: SignInButtonProps) => {

const shouldRedirectTo = (page: string): boolean => page !== '/' && page !== '/home';

const attemptSignInCheckToSAndRedirect = async (redirectTo:string, shouldRedirect: boolean) => {
await checkToSAndRedirect(shouldRedirect ? redirectTo : null);
Metrics.identify(Storage.getAnonymousId());
Metrics.syncProfile();
Metrics.captureEvent(eventList.userSignIn);
};

const handleRegistration = async (redirectTo: string, shouldRedirect: boolean) => {
try {
await registerAndRedirectNewUser(redirectTo, shouldRedirect);
Expand All @@ -137,9 +105,6 @@ export const SignInButton = (props: SignInButtonProps) => {
const registeredUser = await User.registerUser();
setUserRoleStatuses(registeredUser, Storage);
await onSignIn();
Metrics.identify(Storage.getAnonymousId());
Metrics.syncProfile();
Metrics.captureEvent(eventList.userRegister);
history.push(`/tos_acceptance${shouldRedirect ? `?redirectTo=${redirectTo}` : ''}`);
};

Expand All @@ -148,13 +113,13 @@ export const SignInButton = (props: SignInButtonProps) => {

switch (status) {
case 400:
setErrorDisplay({ show: true, title: 'Error', msg: JSON.stringify(error) });
setErrorDisplay({show: true, title: 'Error', msg: JSON.stringify(error)});
break;
case 409:
await handleConflictError(redirectTo, shouldRedirect);
break;
default:
setErrorDisplay({ show: true, title: 'Error', msg: 'Unexpected error, please try again' });
setErrorDisplay({show: true, title: 'Error', msg: 'Unexpected error, please try again'});
break;
}
};
Expand All @@ -173,46 +138,56 @@ export const SignInButton = (props: SignInButtonProps) => {
setErrorDisplay(
<span>
Sign-in cancelled ...
<img height="20px" src={loadingIndicator} />
<img height="20px" src={loadingIndicator} alt={'loading'}/>
</span>
);
setTimeout(() => {
setErrorDisplay({});
}, 2000);
} else {
setErrorDisplay({ title: response.error, description: response.details });
setErrorDisplay({title: response.error, description: response.details});
}
};

const spinnerOrSignInButton = () => {
return (clientId === ''
? Spinner()
: (<div style={{ display: 'flex' }}>
{isNil(customStyle)
? GoogleIS.signInButton(clientId, onSuccess, onFailure)
: <button className={'btn-primary'} style={customStyle} onClick={() => {
GoogleIS.requestAccessToken(clientId, onSuccess, onFailure);
}}>
Submit a Data Access Request
</button>}
{isNil(customStyle) &&
const loadingElement = (): React.JSX.Element => {
return (
<span>
<img height='20px' src={loadingIndicator} alt={'loading'}/>
</span>
);
};

const signInElement = (): React.JSX.Element => {
return (
<div style={{display: 'flex'}}>
<button
className={'btn-secondary'}
onClick={async () => {
setIsLoading(true);
Auth.signIn(true).then(onSuccess, onFailure);
setIsLoading(false);
}}
disabled={isLoading}
>
{isLoading ? loadingElement() : 'Sign In'}
</button>
<a
className='navbar-duos-icon-help'
style={{ color: 'white', height: 16, width: 16, marginLeft: 5 }}
style={{color: 'white', height: 16, width: 16, marginLeft: 5}}
href='https://broad-duos.zendesk.com/hc/en-us/articles/6160103983771-How-to-Create-a-Google-Account-with-a-Non-Google-Email'
data-for="tip_google-help"
data-tip="No Google help? Click here!"
data-tip="Need account help? Click here!"
/>
}
<ReactTooltip id="tip_google-help" place="top" effect="solid" multiline={true} className="tooltip-wrapper" />
</div>));
<ReactTooltip id="tip_google-help" place="top" effect="solid" multiline={true} className="tooltip-wrapper"/>
</div>
);
};

return (
<div>
{isEmpty(errorDisplay)
? <div>
{spinnerOrSignInButton()}
{signInElement()}
</div>
: <div className="dialog-alert">
<Alert
Expand Down
4 changes: 4 additions & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@ import './index.css';
import './styles/bootstrap_replacement.css';
import App from './App';
import {Auth} from './libs/auth/auth';
import {OidcBroker} from './libs/auth/oidcBroker';
import {unregister} from './registerServiceWorker';
import {BrowserRouter} from 'react-router-dom';

const load = async () => {
unregister();
await Auth.initialize();
if (window.location.pathname.startsWith('/redirect-from-oauth')) {
await OidcBroker.getUserManager().signinPopupCallback(window.location.href);
}
const container = document.getElementById('root');
const root = createRoot(container!);
root.render(<BrowserRouter><App/></BrowserRouter>);
Expand Down
7 changes: 4 additions & 3 deletions src/libs/ajax/Metrics.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { getDefaultProperties } from '@databiosphere/bard-client';

import { Storage } from '../storage';
import { getBardApiUrl } from '../ajax';
import {Token} from '../config';

export const Metrics = {
captureEvent: (event, details, signal) => captureEventFn(event, details, signal).catch(() => { }),
Expand Down Expand Up @@ -42,7 +43,7 @@ const captureEventFn = async (event, details = {}, signal) => {
method: 'POST',
url: `${await getBardApiUrl()}/api/event`,
data: body,
headers: isRegistered ? { Authorization: `Bearer ${Storage.getGoogleData()?.accessToken}` } : undefined,
headers: isRegistered ? { Authorization: `Bearer ${Token.getToken()}` } : undefined,
signal,
};

Expand All @@ -59,7 +60,7 @@ const syncProfile = async (signal) => {
const config = {
method: 'POST',
url: `${await getBardApiUrl()}/api/syncProfile`,
headers: { Authorization: `Bearer ${Storage.getGoogleData()?.accessToken}` },
headers: { Authorization: `Bearer ${Token.getToken()}` },
signal,
};

Expand All @@ -80,7 +81,7 @@ const identify = async (anonId, signal) => {
method: 'POST',
url: `${await getBardApiUrl()}/api/identify`,
data: body,
headers: { Authorization: `Bearer ${Storage.getGoogleData()?.accessToken}` },
headers: { Authorization: `Bearer ${Token.getToken()}` },
signal,
};

Expand Down
Loading

0 comments on commit db33dbc

Please sign in to comment.