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

feat(frontend): authenticate with google #82

Merged
merged 9 commits into from
May 24, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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