Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[REF] Switch experimental auth to auth0 #380

Merged
merged 13 commits into from
Dec 2, 2024
4 changes: 2 additions & 2 deletions cypress/component/AuthDialog.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ describe('AuthDialog', () => {
it('Displays a MUI dialog with the title and "sing in with google" button', () => {
cy.mount(
<GoogleOAuthProvider clientId="mock-client-id">
<AuthDialog open onClose={props.onClose} onAuth={props.onAuth} />
<AuthDialog open onClose={props.onClose} />
</GoogleOAuthProvider>
);
cy.get('[data-cy="auth-dialog"]').should('be.visible');
cy.get('[data-cy="auth-dialog"]').should('contain', 'You must log in');
cy.get('[data-cy="auth-dialog"]').within(() => {
cy.contains('Google');
cy.contains('Neurobagel');
});
cy.get('[data-cy="close-auth-dialog-button"]').should('be.visible');
});
Expand Down
13 changes: 1 addition & 12 deletions cypress/component/Navbar.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,12 @@ import Navbar from '../../src/components/Navbar';

const props = {
isLoggedIn: true,
name: 'john doe',
profilePic: 'johndoe.png',
onLogout: () => {},
onLogin: () => {},
};

describe('Navbar', () => {
it('Displays a MUI Toolbar with logo, title, subtitle, documentation link, and GitHub link', () => {
cy.mount(
<Navbar
isLoggedIn={props.isLoggedIn}
name={props.name}
profilePic={props.profilePic}
onLogout={props.onLogout}
onLogin={props.onLogin}
/>
);
cy.mount(<Navbar isLoggedIn={props.isLoggedIn} onLogin={props.onLogin} />);
cy.get("[data-cy='navbar']").should('be.visible');
cy.get("[data-cy='navbar'] img").should('exist');
cy.get("[data-cy='navbar'] h5").should('contain', 'Neurobagel Query');
Expand Down
5 changes: 5 additions & 0 deletions cypress/e2e/Feedback.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ describe('Feedback', () => {
it('Displays and closes small screen size dialog', () => {
cy.viewport(766, 500);
cy.visit('/');
// TODO: remove this
// Bit of a hacky way to close the auth dialog
// But we need to do it until we make auth an always-on feature
// Because the auth dialog will overlap a lot of the UI and thus fail the tests
cy.get('[data-cy="close-auth-dialog-button"]').click();
cy.get('[data-cy="small-screen-size-dialog"]').should('be.visible');
cy.get('[data-cy="close-small-screen-size-dialog-button"]').click();
cy.get('[data-cy="small-screen-size-dialog"]').should('not.exist');
Expand Down
18 changes: 18 additions & 0 deletions package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"prepare": "husky install"
},
"dependencies": {
"@auth0/auth0-react": "^2.2.4",
"@emotion/react": "^11.13.5",
"@emotion/styled": "^11.13.0",
"@fontsource/roboto": "^5.1.0",
Expand Down
62 changes: 24 additions & 38 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ import { Alert, Grow, IconButton } from '@mui/material';
import useMediaQuery from '@mui/material/useMediaQuery';
import CloseIcon from '@mui/icons-material/Close';
import { SnackbarKey, SnackbarProvider, closeSnackbar, enqueueSnackbar } from 'notistack';
import { jwtDecode } from 'jwt-decode';
import { googleLogout } from '@react-oauth/google';
import { useAuth0 } from '@auth0/auth0-react';
import { queryURL, baseAPIURL, nodesURL, enableAuth, enableChatbot } from './utils/constants';
import {
RetrievedAttributeOption,
Expand All @@ -18,7 +17,6 @@ import {
Pipelines,
NodeError,
QueryResponse,
GoogleJWT,
} from './utils/types';
import QueryForm from './components/QueryForm';
import ResultContainer from './components/ResultContainer';
Expand Down Expand Up @@ -60,11 +58,28 @@ function App() {
const [pipelineName, setPipelineName] = useState<FieldInput>(null);
const [searchParams, setSearchParams] = useSearchParams();

const [isLoggedIn, setIsLoggedIn] = useState<boolean>(false);
const [openAuthDialog, setOpenAuthDialog] = useState(true);
const [name, setName] = useState<string>('');
const [profilePic, setProfilePic] = useState<string>('');
const [openAuthDialog, setOpenAuthDialog] = useState(false);
const [IDToken, setIDToken] = useState<string | undefined>('');
const { isAuthenticated, isLoading, getIdTokenClaims } = useAuth0();

// Extract the raw OIDC ID token from the Auth0 SDK
useEffect(() => {
if (enableAuth && !isLoading) {
if (isAuthenticated) {
(async () => {
const tokenClaims = await getIdTokenClaims();
// eslint-disable-next-line no-underscore-dangle
setIDToken(tokenClaims?.__raw);
})();
setOpenAuthDialog(false);
}
if (!isAuthenticated) {
setOpenAuthDialog(true);
} else {
setOpenAuthDialog(false);
}
}
}, [isAuthenticated, isLoading, getIdTokenClaims]);

const selectedNode: FieldInputOption[] = availableNodes
.filter((option) => searchParams.getAll('node').includes(option.NodeName))
Expand Down Expand Up @@ -415,45 +430,16 @@ function App() {
setLoading(false);
}

function login(credential: string | undefined) {
setIsLoggedIn(true);
setOpenAuthDialog(false);
const jwt: GoogleJWT = credential ? jwtDecode(credential) : ({} as GoogleJWT);
setIDToken(credential);
setName(jwt.given_name);
setProfilePic(jwt.picture);
}

function logout() {
googleLogout();
setIsLoggedIn(false);
setIDToken('');
setName('');
setProfilePic('');
}

return (
<>
{enableAuth && (
<AuthDialog
open={openAuthDialog}
onAuth={(credential) => login(credential)}
onClose={() => setOpenAuthDialog(false)}
/>
)}
<AuthDialog open={openAuthDialog} onClose={() => setOpenAuthDialog(false)} />
<SmallScreenSizeDialog open={isScreenSizeSmall} onClose={() => setIsScreenSizeSmall(false)} />
<SnackbarProvider
autoHideDuration={6000}
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
maxSnack={7}
/>
<Navbar
isLoggedIn={isLoggedIn}
name={name}
profilePic={profilePic}
onLogout={() => logout()}
onLogin={() => setOpenAuthDialog(true)}
/>
<Navbar isLoggedIn={isAuthenticated} onLogin={() => setOpenAuthDialog(true)} />
{showAlert() && (
<>
<Grow in={!alertDismissed}>
Expand Down
20 changes: 8 additions & 12 deletions src/components/AuthDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,25 @@ import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import DialogActions from '@mui/material/DialogActions';
import Button from '@mui/material/Button';
import { GoogleLogin } from '@react-oauth/google';
import { useAuth0 } from '@auth0/auth0-react';

function AuthDialog({ open, onClose }: { open: boolean; onClose: () => void }) {
const { loginWithRedirect } = useAuth0();

function AuthDialog({
open,
onAuth,
onClose,
}: {
open: boolean;
onAuth: (credential: string | undefined) => void;
onClose: () => void;
}) {
return (
<Dialog open={open} onClose={onClose} data-cy="auth-dialog">
<DialogTitle>
You must log in to a trusted identity provider in order to query all available nodes!
</DialogTitle>
<DialogContent>
<div className="flex flex-col items-center justify-center">
<GoogleLogin onSuccess={(response) => onAuth(response.credential)} />
<Button variant="contained" onClick={() => loginWithRedirect()}>
Sign in to Neurobagel
</Button>
</div>
</DialogContent>
<DialogActions>
<Button onClick={onClose} data-cy="close-auth-dialog-button">
<Button variant="outlined" onClick={onClose} data-cy="close-auth-dialog-button">
Close
</Button>
</DialogActions>
Expand Down
31 changes: 14 additions & 17 deletions src/components/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,17 @@ import Article from '@mui/icons-material/Article';
import Logout from '@mui/icons-material/Logout';
import Login from '@mui/icons-material/Login';
import Avatar from '@mui/material/Avatar';
import { useAuth0 } from '@auth0/auth0-react';
import { enableAuth } from '../utils/constants';
import logo from '../assets/logo.png';

function Navbar({
isLoggedIn,
name,
profilePic,
onLogout,
onLogin,
}: {
isLoggedIn: boolean;
name: string;
profilePic: string;
onLogout: () => void;
onLogin: () => void;
}) {
function Navbar({ isLoggedIn, onLogin }: { isLoggedIn: boolean; onLogin: () => void }) {
const [latestReleaseTag, setLatestReleaseTag] = useState('');
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const openAccountMenu = Boolean(anchorEl);

const { user, logout } = useAuth0();

useEffect(() => {
const GHApiURL = 'https://api.github.com/repos/neurobagel/query-tool/releases/latest';
axios
Expand Down Expand Up @@ -85,7 +76,11 @@ function Navbar({
{enableAuth && (
<>
<IconButton onClick={handleClick}>
<Avatar src={profilePic} sx={{ width: 30, height: 30 }} alt={name} />
<Avatar
src={user?.picture ?? ''}
sx={{ width: 30, height: 30 }}
alt={user?.name ?? ''}
/>
</IconButton>
<Menu
anchorEl={anchorEl}
Expand All @@ -103,15 +98,17 @@ function Navbar({
>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<MenuItem>
<Avatar src={profilePic} alt={name} />
<Avatar src={user?.picture ?? ''} alt={user?.name ?? ''} />
</MenuItem>
</div>
{isLoggedIn ? (
<>
<MenuItem>
<Typography>Logged in as {name}</Typography>
<Typography>Logged in as {user?.name ?? ''}</Typography>
</MenuItem>
<MenuItem onClick={onLogout}>
<MenuItem
onClick={() => logout({ logoutParams: { returnTo: window.location.origin } })}
>
<ListItemIcon className="mr-[-8px]">
<Logout fontSize="small" />
</ListItemIcon>
Expand Down
16 changes: 14 additions & 2 deletions src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import ReactDOM from 'react-dom/client';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import { StyledEngineProvider, ThemeProvider } from '@mui/material/styles';
import { GoogleOAuthProvider } from '@react-oauth/google';
import { Auth0Provider } from '@auth0/auth0-react';
import App from './App';
import { appBasePath, enableAuth, clientID } from './utils/constants';
import './index.css';
Expand All @@ -27,5 +27,17 @@ const app = (
);

ReactDOM.createRoot(document.getElementById('root')!).render(
enableAuth ? <GoogleOAuthProvider clientId={clientID}>{app}</GoogleOAuthProvider> : app
enableAuth ? (
<Auth0Provider
domain="neurobagel.ca.auth0.com" // TODO: Replace with customizable domain
clientId={clientID}
authorizationParams={{
redirect_uri: window.location.origin, // TODO: ensure that users end up where they started, including query params
surchs marked this conversation as resolved.
Show resolved Hide resolved
}}
>
{app}
</Auth0Provider>
) : (
app
)
);
5 changes: 5 additions & 0 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,10 @@ export default defineConfig(({ mode }) => {
},
plugins: [react()],
envPrefix: 'NB_',
// Excluding the Auth0 library from the bundle to avoid issues with
// Cypress component tests. TODO: understand why this is necessary
optimizeDeps: {
exclude: ['@auth0/auth0-react'],
},
};
});