Skip to content
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
2 changes: 1 addition & 1 deletion .github/chatmodes/fixer.chatmode.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ You MUST check task output readiness before debugging, testing, or declaring wor
- Frontend: Vite provides HMR; changes in the frontend are picked up automatically without restarting the task.
- Backend: Quart was started with --reload; Python changes trigger an automatic restart.
- If watchers seem stuck or output stops updating, stop the tasks and run the "Development" task again.
- To interact with a running application, use the Playwright MCP server
- To interact with a running application, use the Playwright MCP server. If testing login, you will need to navigate to 'localhost' instead of '127.0.0.1' since that's the URL allowed by the Entra application.

## Committing the change

Expand Down
63 changes: 54 additions & 9 deletions app/frontend/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ import { createHashRouter, RouterProvider } from "react-router-dom";
import { I18nextProvider } from "react-i18next";
import { HelmetProvider } from "react-helmet-async";
import { initializeIcons } from "@fluentui/react";
import { MsalProvider } from "@azure/msal-react";
import { AuthenticationResult, EventType, PublicClientApplication } from "@azure/msal-browser";

import "./index.css";

import Chat from "./pages/chat/Chat";
import LayoutWrapper from "./layoutWrapper";
import i18next from "./i18n/config";
import { msalConfig, useLogin } from "./authConfig";

initializeIcons();

Expand All @@ -34,12 +37,54 @@ const router = createHashRouter([
}
]);

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<I18nextProvider i18n={i18next}>
<HelmetProvider>
<RouterProvider router={router} />
</HelmetProvider>
</I18nextProvider>
</React.StrictMode>
);
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement);

// Bootstrap the app once; conditionally wrap with MsalProvider when login is enabled
(async () => {
let msalInstance: PublicClientApplication | undefined;

if (useLogin) {
msalInstance = new PublicClientApplication(msalConfig);
try {
await msalInstance.initialize();

// Default active account to the first one if none is set
if (!msalInstance.getActiveAccount() && msalInstance.getAllAccounts().length > 0) {
msalInstance.setActiveAccount(msalInstance.getAllAccounts()[0]);
}

// Keep active account in sync on login success
msalInstance.addEventCallback(event => {
if (event.eventType === EventType.LOGIN_SUCCESS && event.payload) {
const result = event.payload as AuthenticationResult;
if (result.account) {
msalInstance!.setActiveAccount(result.account);
}
}
});
} catch (e) {
// Non-fatal: render the app even if MSAL initialization fails
// eslint-disable-next-line no-console
console.error("MSAL initialize failed", e);
msalInstance = undefined;
}
}

const appTree = (
<React.StrictMode>
<I18nextProvider i18n={i18next}>
<HelmetProvider>
{useLogin && msalInstance ? (
<MsalProvider instance={msalInstance}>
<RouterProvider router={router} />
</MsalProvider>
) : (
<RouterProvider router={router} />
)}
</HelmetProvider>
</I18nextProvider>
</React.StrictMode>
);

root.render(appTree);
})();
55 changes: 20 additions & 35 deletions app/frontend/src/layoutWrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,48 +1,33 @@
import { AccountInfo, EventType, PublicClientApplication } from "@azure/msal-browser";
import { checkLoggedIn, msalConfig, useLogin } from "./authConfig";
import { useEffect, useState } from "react";
import { MsalProvider } from "@azure/msal-react";
import { useEffect, useRef, useState } from "react";
import { useMsal } from "@azure/msal-react";
import { useLogin, checkLoggedIn } from "./authConfig";
import { LoginContext } from "./loginContext";
import Layout from "./pages/layout/Layout";

const LayoutWrapper = () => {
const [loggedIn, setLoggedIn] = useState(false);
if (useLogin) {
var msalInstance = new PublicClientApplication(msalConfig);

// Default to using the first account if no account is active on page load
if (!msalInstance.getActiveAccount() && msalInstance.getAllAccounts().length > 0) {
// Account selection logic is app dependent. Adjust as needed for different use cases.
msalInstance.setActiveAccount(msalInstance.getActiveAccount());
}

// Listen for sign-in event and set active account
msalInstance.addEventCallback(event => {
if (event.eventType === EventType.LOGIN_SUCCESS && event.payload) {
const account = event.payload as AccountInfo;
msalInstance.setActiveAccount(account);
}
});

const { instance } = useMsal();
// Keep track of the mounted state to avoid setting state in an unmounted component
const mounted = useRef<boolean>(true);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new mounted ref is to avoid setState calls inside the async init() below if the component unmounts during all that. Don't know how common that is.

useEffect(() => {
const fetchLoggedIn = async () => {
setLoggedIn(await checkLoggedIn(msalInstance));
mounted.current = true;
checkLoggedIn(instance)
.then(isLoggedIn => {
if (mounted.current) setLoggedIn(isLoggedIn);
})
.catch(e => {
console.error("checkLoggedIn failed", e);
});
return () => {
mounted.current = false;
};

fetchLoggedIn();
}, []);
}, [instance]);

return (
<MsalProvider instance={msalInstance}>
<LoginContext.Provider
value={{
loggedIn,
setLoggedIn
}}
>
<Layout />
</LoginContext.Provider>
</MsalProvider>
<LoginContext.Provider value={{ loggedIn, setLoggedIn }}>
<Layout />
</LoginContext.Provider>
);
} else {
return (
Expand Down
Loading