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): add oauth with microsoft #71

Merged
merged 19 commits into from
May 18, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
13 changes: 8 additions & 5 deletions nesis/api/core/services/management.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,14 @@ def create(self, **kwargs) -> UserSession:
self.__LOG.debug(f"Received session object {kwargs}")
email = user_session.get("email")
password = user_session.get("password")

# Hard code these to make it easier to test
session_oauth_token_value = user_session.get(
os.environ.get("NESIS_OAUTH_TOKEN_KEY")
os.environ.get("NESIS_OAUTH_TOKEN_KEY") or "___nesis_oauth_token_key___"
)
oauth_token_value = (
os.environ.get("NESIS_OAUTH_TOKEN_VALUE") or "___nesis_oauth_token_value___"
)
oauth_token_value = os.environ.get("NESIS_OAUTH_TOKEN_VALUE")

try:

Expand All @@ -85,7 +89,6 @@ def create(self, **kwargs) -> UserSession:
user_password = password.encode("utf-8")
if not bcrypt.checkpw(user_password, db_pass):
raise UnauthorizedAccess("Invalid email/password")
# update last login details.
db_user = users[0]

return self.__create_user_session(db_user)
Expand Down Expand Up @@ -114,15 +117,15 @@ def create(self, **kwargs) -> UserSession:

def __create_user_session(self, db_user: User):
user_dict = db_user.to_dict()
token = SG("[\l\d]{128}").render()
token = SG(r"[\l\d]{128}").render()
session_token = self.__cache_key(token)
expiry = (
self.__config["memcache"].get("session", {"expiry": 0}).get("expiry", 0)
)
if self.__cache.get(session_token) is None:
self.__cache.set(session_token, user_dict, time=expiry)
while self.__cache.get(session_token)["id"] != user_dict["id"]:
token = SG("[\l\d]{128}").render()
token = SG(r"[\l\d]{128}").render()
session_token = self.__cache_key(token)
self.__cache.set(session_token, user_dict, time=expiry)
return UserSession(token=token, expiry=expiry, user=db_user)
Expand Down
269 changes: 211 additions & 58 deletions nesis/frontend/client/package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion nesis/frontend/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@azure/msal-browser": "^3.14.0",
"@azure/msal-react": "^2.0.16",
"@emotion/react": "^11.11.3",
"@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.15.3",
Expand All @@ -19,6 +21,7 @@
"query-string": "^8.1.0",
"react": "^18.2.0",
"react-bootstrap": "^2.9.0",
"react-bootstrap-icons": "^1.11.4",
"react-chartjs-2": "^5.2.0",
"react-dom": "^18.2.0",
"react-icons": "^4.11.0",
Expand All @@ -27,7 +30,6 @@
"react-scripts": "5.0.1",
"react-select": "^5.7.7",
"styled-components": "^6.0.8",
"react-bootstrap-icons": "^1.0.1-alpha3",
"superagent": "^8.1.2",
"uuid": "^9.0.1",
"web-vitals": "^2.1.4"
Expand Down
31 changes: 31 additions & 0 deletions nesis/frontend/client/src/ConfigContext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React, { useContext, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import useClient from './utils/useClient';

const ConfigContext = React.createContext({});

ConfigContextProvider.propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]).isRequired,
};

export default ConfigContext;

export function ConfigContextProvider({ children }) {
const client = useClient();
const [config, setConfig] = useState(null);
useEffect(() => {
client.get('config').then((res) => {
setConfig(JSON.parse(res.text));
});
}, [setConfig, client]);
return (
<ConfigContext.Provider value={config}>{children}</ConfigContext.Provider>
);
}

export function useConfig() {
return useContext(ConfigContext);
}
32 changes: 21 additions & 11 deletions nesis/frontend/client/src/SessionContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { clearToken } from './utils/tokenStorage';
import { useHistory } from 'react-router-dom';
import useSessionStorage from './utils/useSessionStorage';
import apiClient from './utils/httpClient';
import { PublicClientApplication } from '@azure/msal-browser';

const SessionContext = React.createContext({
session: null,
setSession: () => '',
Expand All @@ -24,7 +26,7 @@ SessionProvider.propTypes = {

export default SessionContext;

const SESSION_KEY = 'AMETNES_CURRENT_SESSION';
const SESSION_KEY = 'NESIS_CURRENT_SESSION';

export function SessionProvider({ children }) {
const [session, setSession] = useSessionStorage(SESSION_KEY);
Expand Down Expand Up @@ -55,32 +57,40 @@ export function useCurrentUser() {
return context && context.user;
}

export function useSignOut(client) {
export function useSignOut(client, config) {
const context = useContext(SessionContext);
const setSession = context?.setSession;
const history = useHistory();
return useCallback(
function () {
logoutFromGoogle();
logoutFromOptimAI(client);
logoutNesis(client);
clearToken();
setSession(null);
logoutMicrosoft(config);
history.push('/signin');
},
[setSession, history],
);
}

function logoutFromGoogle() {
if (window.gapi && window.gapi.auth2) {
const auth2 = window.gapi.auth2.getAuthInstance();
if (auth2 != null) {
auth2.signOut().then(auth2.disconnect());
}
async function logoutMicrosoft(config) {
try {
const msalInstance = new PublicClientApplication({
auth: {
clientId: config?.auth?.OAUTH_AZURE_CLIENT_ID,
authority: config?.auth?.OAUTH_AZURE_AUTHORITY,
redirectUri: config?.auth?.OAUTH_AZURE_REDIRECTURI,
postLogoutRedirectUri: 'http://localhost:3000/',
},
});
await msalInstance.initialize();
msalInstance.logoutRedirect();
} catch (e) {
/* ignored */
}
}

function logoutFromOptimAI(client) {
function logoutNesis(client) {
if (!client) {
return;
}
Expand Down
70 changes: 70 additions & 0 deletions nesis/frontend/client/src/components/AzureButton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React, { useContext, useEffect, useState } from 'react';
import useClient from '../utils/useClient';
import { useConfig } from '../ConfigContext';
import parseApiErrorMessage from '../utils/parseApiErrorMessage';
import { PublicClientApplication } from '@azure/msal-browser';
import MicrosoftIcon from '../images/MicrosoftIcon.png';
import classes from '../styles/SignInPage.module.css';

export default function AzureButton({ onFailure, onSuccess }) {
const config = useConfig();
const client = useClient();
const msalInstance = new PublicClientApplication({
auth: {
clientId: config?.auth?.OAUTH_AZURE_CLIENT_ID,
authority: config?.auth?.OAUTH_AZURE_AUTHORITY,
redirectUri: config?.auth?.OAUTH_AZURE_REDIRECTURI,
navigateToLoginRequestUrl: true,
},
cache: {
cacheLocation: config?.auth?.OAUTH_AZURE_CACHELOCATION,
storeAuthStateInCookie: config?.auth?.OAUTH_AZURE_STOREAUTHSTATEINCOOKIE,
},
});

useEffect(() => {
const initialize = async () => {
await msalInstance.initialize();
await msalInstance
.handleRedirectPromise()
.then((response) => {
if (response?.accessToken) {
client
.post('sessions', {
azure: response,
})
.then((response) => {
onSuccess(response?.body?.email, response);
})
.catch((error) => {
onFailure(parseApiErrorMessage(error));
});
}
})
.catch((error) => {
onFailure(parseApiErrorMessage(error));
});
};
try {
initialize();
} catch (e) {
onFailure('Error log in in with Azure');
}
}, []);

const handleLogin = async (evt) => {
evt.preventDefault();

await msalInstance.initialize();
await msalInstance.loginRedirect();
};

return (
<>
<button className={`${classes.orloginbutton} my-3`} onClick={handleLogin}>
<img className={`${classes.loginorimg} mx-1`} src={MicrosoftIcon} />
Sign in with Microsoft
</button>
</>
);
}
11 changes: 11 additions & 0 deletions nesis/frontend/client/src/components/AzureButton.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 AzureButton from './AzureButton';

describe('<AzureButton>', () => {
it('render azure authenticate option on login and register screen', async () => {
const { getByText } = renderWithContext(<AzureButton />);
const buttonComponent = getByText('Continue With Microsoft');
expect(buttonComponent).toBeInTheDocument();
});
});
9 changes: 6 additions & 3 deletions nesis/frontend/client/src/components/Menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import styled from 'styled-components/macro';
import { NavLink } from 'react-router-dom';
import { device } from '../utils/breakpoints';
import { useSignOut } from '../SessionContext';
import { useConfig } from '../ConfigContext';
import MenuHeader from './MenuHeader';
import { GearFill, FileEarmarkPost, ArrowBarLeft } from 'react-bootstrap-icons';
import client from '../utils/httpClient';

const sideMenuWidth = 300;

Expand Down Expand Up @@ -56,7 +58,7 @@ const MainContainer = styled.div`
flex-direction: column;
`;

function Optim({ children, className }) {
function Nesis({ children, className }) {
const [mobileModalVisible, setModalVisible] = useState(false);
const background = 'white';

Expand Down Expand Up @@ -133,7 +135,8 @@ const MenuTitle = styled.div`
`;

function SideMenu({ onClose }) {
const signOut = useSignOut();
const config = useConfig();
const signOut = useSignOut(client, config);

function closeMenu() {
if (onClose) {
Expand Down Expand Up @@ -170,4 +173,4 @@ function SideMenu({ onClose }) {
);
}

export default Optim;
export default Nesis;
9 changes: 4 additions & 5 deletions nesis/frontend/client/src/components/MenuHeader.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { ReactComponent as Logo } from '../images/Nesis.svg';
import styled from 'styled-components/macro';
import { device } from '../utils/breakpoints';
import client from '../utils/httpClient';
import { useConfig } from '../ConfigContext';
import { LightSquareButton } from './inputs/SquareButton';

const Main = styled.div`
Expand Down Expand Up @@ -78,7 +79,8 @@ const HeaderToolBarIcon = styled.div`

export default function MenuHeader({ onMobileMenuClick }) {
const history = useHistory();
const signOut = useSignOut(client);
const config = useConfig();
const signOut = useSignOut(client, config);
const session = useCurrentSession();
const iconSize = 20;

Expand All @@ -94,10 +96,7 @@ export default function MenuHeader({ onMobileMenuClick }) {
{session && (
<UserControlsRow>
<HeaderToolBarIcon>
<a
href="https://github.com/ametnes/nesis/blob/main/docs/README.md"
target="_blank"
>
<a href="https://ametnes.github.io/nesis/" target="_blank">
<QuestionSquare size={iconSize} className="help-icon" /> Help
</a>
</HeaderToolBarIcon>
Expand Down
Binary file added nesis/frontend/client/src/images/MicrosoftIcon.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/MicrosoftIcon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 4 additions & 1 deletion nesis/frontend/client/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@ import { ThemeProvider } from 'styled-components/macro';
import theme from './utils/theme';
import { SessionProvider } from './SessionContext';
import { ToasterContextProvider } from './ToasterContext';
import { ConfigContextProvider } from './ConfigContext';

ReactDOM.render(
<SessionProvider>
<ToasterContextProvider>
<ThemeProvider theme={theme}>
<Router basename={process.env.PUBLIC_URL}>
<Route path="/" component={App} />
<ConfigContextProvider>
<Route path="/" component={App} />
</ConfigContextProvider>
</Router>
</ThemeProvider>
</ToasterContextProvider>
Expand Down
7 changes: 3 additions & 4 deletions nesis/frontend/client/src/pages/DocumentGPT/ChatPage.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React, { useState } from 'react';
import { useHistory, Route, Switch, useRouteMatch } from 'react-router-dom';
import { GearFill } from 'react-bootstrap-icons';
import Optim from '../../components/Menu';
import Nesis from '../../components/Menu';
import styled from 'styled-components/macro';
import { FileEarmarkPost } from 'react-bootstrap-icons';
import {
Expand Down Expand Up @@ -140,7 +139,7 @@ const ChatPage = () => {
};

return (
<Optim>
<Nesis>
<Heading>
<FileEarmarkPost size={25} color="white" className="mr-2" /> Chat with
your Documents
Expand Down Expand Up @@ -220,7 +219,7 @@ const ChatPage = () => {
</Box>
</Container>
</div>
</Optim>
</Nesis>
);
};

Expand Down
6 changes: 3 additions & 3 deletions nesis/frontend/client/src/pages/Settings/SettingPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { GearFill } from 'react-bootstrap-icons';
import DatasourcesPage from './Datasources/DatasourcesPage';
import UsersPage from './Users/UsersPage';
import RolesPage from './Roles/RolesPage';
import Optim from '../../components/Menu';
import Nesis from '../../components/Menu';
import styled from 'styled-components/macro';

const Heading = styled.h1`
Expand All @@ -23,7 +23,7 @@ const SettingsPage = () => {
const history = useHistory();
const match = useRouteMatch();
return (
<Optim>
<Nesis>
<Heading>
<GearFill size={25} className="mr-2" /> Settings
</Heading>
Expand Down Expand Up @@ -68,7 +68,7 @@ const SettingsPage = () => {
<Route exact path={`${match.path}/roles`} component={RolesPage} />
</Switch>
</div>
</Optim>
</Nesis>
);
};

Expand Down
Loading