Skip to content

Commit

Permalink
feat(frontend): authenticate with google (#82)
Browse files Browse the repository at this point in the history
Adding google authentication to Nesis. 
This introduces a google login button which will allow users to sign in
with their google credentials.
_Given the recommended way of implementing the google Oauth SDK, the
Google button will only display once the google client id is specified
in the configurations and the google oath flag is set to true._
  • Loading branch information
akizito committed May 24, 2024
1 parent f912646 commit 7624464
Show file tree
Hide file tree
Showing 17 changed files with 969 additions and 34 deletions.
288 changes: 269 additions & 19 deletions nesis/frontend/client/package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions nesis/frontend/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.15.3",
"@mui/material": "^5.15.3",
"@react-oauth/google": "^0.12.1",
"axios": "^1.5.1",
"babel-plugin-named-exports-order": "^0.0.2",
"babel-plugin-styled-components": "^2.1.4",
Expand Down
28 changes: 28 additions & 0 deletions nesis/frontend/client/src/GoogleAuthContext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from 'react';
import { GoogleOAuthProvider } from '@react-oauth/google';
import { useConfig } from './ConfigContext';

const DefaultConfigContext = React.createContext({});

export default function GoogleContextProvider({ children }) {
const config = useConfig();
const google_client_id = config?.auth?.OAUTH_GOOGLE_CLIENT_ID;
const googleAuthEnabled =
google_client_id && config?.auth?.OAUTH_GOOGLE_ENABLED;
return (
<>
{googleAuthEnabled ? (
<GoogleOAuthProvider
value={google_client_id}
clientId={google_client_id}
>
{children}
</GoogleOAuthProvider>
) : (
<DefaultConfigContext.Provider value={googleAuthEnabled}>
{children}
</DefaultConfigContext.Provider>
)}
</>
);
}
8 changes: 7 additions & 1 deletion nesis/frontend/client/src/SessionContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { useHistory } from 'react-router-dom';
import useSessionStorage from './utils/useSessionStorage';
import apiClient from './utils/httpClient';
import { PublicClientApplication } from '@azure/msal-browser';
import { googleLogout } from '@react-oauth/google';

const SessionContext = React.createContext({
session: null,
Expand Down Expand Up @@ -67,6 +68,7 @@ export function useSignOut(client, config) {
clearToken();
setSession(null);
logoutMicrosoft(config);
logoutGoogle();
history.push('/signin');
},
[setSession, history],
Expand All @@ -84,12 +86,16 @@ async function logoutMicrosoft(config) {
},
});
await msalInstance.initialize();
msalInstance.logoutRedirect();
await msalInstance.logoutRedirect();
} catch (e) {
/* ignored */
}
}

async function logoutGoogle() {
googleLogout();
}

function logoutNesis(client) {
if (!client) {
return;
Expand Down
57 changes: 57 additions & 0 deletions nesis/frontend/client/src/components/GoogleButton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React, { useEffect } from 'react';
import { useGoogleLogin } from '@react-oauth/google';
import useClient from '../utils/useClient';
import parseApiErrorMessage from '../utils/parseApiErrorMessage';
import GoogleIcon from '../images/GoogleIcon.png';
import classes from '../styles/SignInPage.module.css';
import { useConfig } from '../ConfigContext';

export default function GoogleButton({ onFailure, onSuccess }) {
const client = useClient();
const config = useConfig();

const googleLogin = useGoogleLogin({
ux_mode: 'redirect',
flow: 'auth-code',
redirect_uri: config?.auth?.OAUTH_GOOGLE_REDIRECTURI,
});

useEffect(() => {
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
const codeParam = urlParams.get('code');

if (codeParam) {
client
.post('sessions', {
google: codeParam,
})
.then((response) => {
onSuccess(response?.body?.email, response);
})
.catch((error) => {
onFailure(parseApiErrorMessage(error));
});
} else {
const error = 'Failed to Login';
handleFailure(error);
}
}, []);

function handleFailure(error) {
onFailure('Could not login using Google');
}

return (
<>
<button className={`${classes.orloginbutton} my-3`} onClick={googleLogin}>
<img
alt="Sign in with Google"
className={`${classes.loginorimg} mx-1`}
src={GoogleIcon}
/>
Sign in with Google
</button>
</>
);
}
11 changes: 11 additions & 0 deletions nesis/frontend/client/src/components/GoogleButton.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React from 'react';
import { renderWithContext, renderWithRouter } from '../utils/testUtils';
import GoogleButton from './GoogleButton';

describe('<GoogleButton>', () => {
it('render google authenticate option on login and register screen', async () => {
const { getByText } = renderWithContext(<GoogleButton />);
const buttonComponent = getByText('Sign in With Google');
expect(buttonComponent).toBeInTheDocument();
});
});
Binary file added nesis/frontend/client/src/images/GoogleIcon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions nesis/frontend/client/src/images/GoogleIcon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 9 additions & 4 deletions nesis/frontend/client/src/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { createRoot } from 'react-dom/client';
import './index.css';
import App from './App';
import { BrowserRouter as Router, Route } from 'react-router-dom';
Expand All @@ -8,18 +8,23 @@ import theme from './utils/theme';
import { SessionProvider } from './SessionContext';
import { ToasterContextProvider } from './ToasterContext';
import { ConfigContextProvider } from './ConfigContext';
import GoogleContextProvider from './GoogleAuthContext';

ReactDOM.render(
const container = document.getElementById('root');
const root = createRoot(container);

root.render(
<SessionProvider>
<ToasterContextProvider>
<ThemeProvider theme={theme}>
<Router basename={process.env.PUBLIC_URL}>
<ConfigContextProvider>
<Route path="/" component={App} />
<GoogleContextProvider>
<Route path="/" component={App} />
</GoogleContextProvider>
</ConfigContextProvider>
</Router>
</ThemeProvider>
</ToasterContextProvider>
</SessionProvider>,
document.getElementById('root'),
);
24 changes: 17 additions & 7 deletions nesis/frontend/client/src/pages/SignInPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { Col, Container, Row, Form } from 'react-bootstrap';
import classes from '../styles/SignInPage.module.css';
import AzureButton from '../components/AzureButton';
import { Toggles } from 'react-bootstrap-icons';
import GoogleButton from '../components/GoogleButton';

const LogoContainer = styled.div`
margin-top: 32px;
Expand Down Expand Up @@ -94,7 +95,10 @@ const SignInPage = () => {
const history = useHistory();
const config = useConfig();
const azureAuthEnabled = config?.auth?.OAUTH_AZURE_ENABLED;
const oauthEnabled = azureAuthEnabled;
const googleAuthEnabled =
config?.auth?.OAUTH_GOOGLE_ENABLED &&
config?.auth?.OAUTH_GOOGLE_CLIENT_ID !== undefined;
const oauthEnabled = azureAuthEnabled || googleAuthEnabled;

function submit(session, actions) {
client
Expand Down Expand Up @@ -142,6 +146,16 @@ const SignInPage = () => {
)}
</Col>
</Row>
<Row>
<Col className={`${classes.colsign} px-1`} lg={10}>
{googleAuthEnabled && !toggleCreds && (
<GoogleButton
onFailure={setError}
onSuccess={handleSuccess}
/>
)}
</Col>
</Row>
</Container>
{(!oauthEnabled || toggleCreds) && (
<div>
Expand Down Expand Up @@ -184,13 +198,9 @@ const SignInPage = () => {
</div>
)}
{oauthEnabled && (
<div style={{ padding: '10px' }}>
<div className={classes.toggleCredsDiv}>
<span
style={{
cursor: 'pointer',
backgroundColor: '#cccccc',
padding: '5px',
}}
className={classes.toggleCreds}
onClick={() => setToggleCreds(!toggleCreds)}
>
Use {!toggleCreds ? 'password' : 'Azure'}
Expand Down
26 changes: 24 additions & 2 deletions nesis/frontend/client/src/styles/SignInPage.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,33 @@
}

.orloginbutton {
border: 1px solid rgb(0, 18, 30);
border: 1px solid rgb(145 156 162 / 88%);
padding: 15px 3px;
border-radius: 1px;
border-radius: 13px;
background-color: white;
color: black;
width: 100%;
font-size: 12px;
}

.toggleCredsDiv {
position: relative;
padding: 20px;
width: 100%;
}

.toggleCreds {
-webkit-border-radius: 12;
-moz-border-radius: 12;
border-radius: 12px;
-webkit-box-shadow: 0px 0px 3px rgb(145 156 162);
-moz-box-shadow: 0px 0px 3px rgb(145 156 162);
box-shadow: 0px 0px 3px rgb(145 156 162);
/*color: #fafafa; */
font-size: 13px;
padding: 11px;
text-decoration: none;
cursor: pointer;
min-width: 50%;
background-color: #d5dee896;
}
51 changes: 51 additions & 0 deletions nesis/frontend/client/src/utils/testUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React from 'react';
import { createMemoryHistory } from 'history';
import { Router } from 'react-router-dom';
import { render } from '@testing-library/react';
import { ToasterContextProvider } from '../ToasterContext';
import { QueryClient, QueryClientProvider } from 'react-query';
import { ConfigContextProvider } from '../ConfigContext';
import GoogleContextProvider from './GoogleAuthContext';

export function renderWithRouter(
ui,
{
route = '/',
history = createMemoryHistory({ initialEntries: [route] }),
} = {},
) {
const Wrapper = ({ children }) => (
<Router history={history}>{children}</Router>
);
return {
...render(ui, { wrapper: Wrapper }),
history,
};
}

const queryClient = new QueryClient();

export function renderWithContext(ui, options) {
return {
...renderWithRouter(
<ToasterContextProvider>
<ConfigContextProvider>
<GoogleContextProvider>{ui}</GoogleContextProvider>
</ConfigContextProvider>
</ToasterContextProvider>,
options,
),
};
}

export function mockGetImplementation(config) {
return function get(path) {
if (config[path]) {
return Promise.resolve({
body: config[path],
});
} else {
throw new Error('Unexpected request path=' + path);
}
};
}
Loading

0 comments on commit 7624464

Please sign in to comment.