From 864404bf7a18df6bdab30c942908e9c0883c342d Mon Sep 17 00:00:00 2001 From: aldbr Date: Tue, 5 Dec 2023 11:17:58 +0100 Subject: [PATCH 1/3] fix: simplify multi-vo login a bit --- src/components/applications/LoginForm.tsx | 18 +++++------------- src/components/applications/UserDashboard.tsx | 4 ++-- src/components/layout/OIDCProvider.tsx | 16 +++++----------- src/components/layout/OIDCSecure.tsx | 4 ++-- src/components/ui/DashboardButton.tsx | 4 ++-- src/components/ui/ProfileButton.tsx | 14 +++++++++----- src/contexts/OIDCConfigurationProvider.tsx | 9 --------- src/hooks/jobs.tsx | 4 ++-- 8 files changed, 27 insertions(+), 46 deletions(-) diff --git a/src/components/applications/LoginForm.tsx b/src/components/applications/LoginForm.tsx index 72068e9b..399ba9a6 100644 --- a/src/components/applications/LoginForm.tsx +++ b/src/components/applications/LoginForm.tsx @@ -32,20 +32,16 @@ export function LoginForm() { const { data, error, isLoading } = useMetadata(); const [selectedVO, setSelectedVO] = useState(null); const [selectedGroup, setSelectedGroup] = useState(null); - const { - configuration, - setConfiguration, - configurationName, - setConfigurationName, - } = useOIDCContext(); - const { isAuthenticated, login } = useOidc(configurationName); + const { configuration, setConfiguration } = useOIDCContext(); + const { isAuthenticated, login } = useOidc(configuration?.scope); // Login if not authenticated useEffect(() => { - if (configurationName && isAuthenticated === false) { + if (configuration && configuration.scope && isAuthenticated === false) { + sessionStorage.setItem("oidcScope", JSON.stringify(configuration.scope)); login(); } - }, [configurationName, isAuthenticated, login]); + }, [configuration, isAuthenticated, login]); // Get default group const getDefaultGroup = (data: Metadata | undefined, vo: string): string => { @@ -87,10 +83,6 @@ export function LoginForm() { ...configuration, scope: newScope, }); - setConfigurationName(newScope); - - sessionStorage.setItem("oidcConfigName", JSON.stringify(newScope)); - login(); } }; // Redirect to dashboard if already authenticated diff --git a/src/components/applications/UserDashboard.tsx b/src/components/applications/UserDashboard.tsx index a3fd9c99..51fb7403 100644 --- a/src/components/applications/UserDashboard.tsx +++ b/src/components/applications/UserDashboard.tsx @@ -13,8 +13,8 @@ import { useOIDCContext } from "@/hooks/oidcConfiguration"; */ export default function UserDashboard() { const theme = useMUITheme(); - const { configurationName } = useOIDCContext(); - const { accessTokenPayload } = useOidcAccessToken(configurationName); + const { configuration } = useOIDCContext(); + const { accessTokenPayload } = useOidcAccessToken(configuration?.scope); return ( diff --git a/src/components/layout/OIDCProvider.tsx b/src/components/layout/OIDCProvider.tsx index e0b3959a..d5da3960 100644 --- a/src/components/layout/OIDCProvider.tsx +++ b/src/components/layout/OIDCProvider.tsx @@ -15,22 +15,16 @@ interface OIDCProviderProps { * @returns the wrapper around OidcProvider */ export function OIDCProvider(props: OIDCProviderProps) { - const { - configuration, - setConfiguration, - configurationName, - setConfigurationName, - } = useOIDCContext(); + const { configuration, setConfiguration } = useOIDCContext(); const diracxUrl = useDiracxUrl(); useEffect(() => { - if (!configuration && !configurationName && diracxUrl) { + if (!configuration && diracxUrl) { // Get the OIDC configuration name from the session storage if it exists - let scope = sessionStorage.getItem("oidcConfigName") || ``; + let scope = sessionStorage.getItem("oidcScope") || ``; if (scope) { scope = scope.replace(/^"|"$/g, ""); - setConfigurationName(scope); } // Set the OIDC configuration @@ -41,7 +35,7 @@ export function OIDCProvider(props: OIDCProviderProps) { redirect_uri: `${diracxUrl}/#authentication-callback`, }); } - }, [diracxUrl, configuration, setConfiguration]); + }, [diracxUrl, configuration]); const withCustomHistory = () => { return { @@ -61,7 +55,7 @@ export function OIDCProvider(props: OIDCProviderProps) { <>
{props.children}
diff --git a/src/components/layout/OIDCSecure.tsx b/src/components/layout/OIDCSecure.tsx index 5ef6c167..6ca7ea5a 100644 --- a/src/components/layout/OIDCSecure.tsx +++ b/src/components/layout/OIDCSecure.tsx @@ -14,8 +14,8 @@ interface OIDCProps { * @returns The children if the user is authenticated, null otherwise */ export function OIDCSecure({ children }: OIDCProps) { - const { configurationName } = useOIDCContext(); - const { isAuthenticated } = useOidc(configurationName); + const { configuration } = useOIDCContext(); + const { isAuthenticated } = useOidc(configuration?.scope); const router = useRouter(); // Redirect to login page if not authenticated diff --git a/src/components/ui/DashboardButton.tsx b/src/components/ui/DashboardButton.tsx index 9cc05b2c..c69b3d6f 100644 --- a/src/components/ui/DashboardButton.tsx +++ b/src/components/ui/DashboardButton.tsx @@ -9,8 +9,8 @@ import Link from "next/link"; * @returns a Button */ export function DashboardButton() { - const { configurationName } = useOIDCContext(); - const { isAuthenticated } = useOidc(configurationName); + const { configuration } = useOIDCContext(); + const { isAuthenticated } = useOidc(configuration?.scope); // Render null if the OIDC configuration is not ready or no access token is available if (!isAuthenticated) { diff --git a/src/components/ui/ProfileButton.tsx b/src/components/ui/ProfileButton.tsx index 1fb5a3fb..86deec93 100644 --- a/src/components/ui/ProfileButton.tsx +++ b/src/components/ui/ProfileButton.tsx @@ -21,9 +21,9 @@ import React from "react"; * @returns a Button */ export function ProfileButton() { - const { configurationName, setConfigurationName } = useOIDCContext(); - const { accessTokenPayload } = useOidcAccessToken(configurationName); - const { logout, isAuthenticated } = useOidc(configurationName); + const { configuration, setConfiguration } = useOIDCContext(); + const { accessTokenPayload } = useOidcAccessToken(configuration?.scope); + const { logout, isAuthenticated } = useOidc(configuration?.scope); const [anchorEl, setAnchorEl] = React.useState(null); const open = Boolean(anchorEl); @@ -35,9 +35,13 @@ export function ProfileButton() { setAnchorEl(null); }; const handleLogout = () => { + if (!configuration) { + return; + } + // Remove the OIDC configuration name from the session storage - setConfigurationName(undefined); - sessionStorage.removeItem("oidcConfigName"); + setConfiguration({ ...configuration, scope: `` }); + sessionStorage.removeItem("oidcScope"); logout(); }; diff --git a/src/contexts/OIDCConfigurationProvider.tsx b/src/contexts/OIDCConfigurationProvider.tsx index de3701eb..301e2296 100644 --- a/src/contexts/OIDCConfigurationProvider.tsx +++ b/src/contexts/OIDCConfigurationProvider.tsx @@ -13,13 +13,9 @@ import { OIDCProvider } from "@/components/layout/OIDCProvider"; export const OIDCConfigurationContext = createContext<{ configuration: OidcConfiguration | null; setConfiguration: (config: OidcConfiguration | null) => void; - configurationName: string | undefined; - setConfigurationName: (name: string | undefined) => void; }>({ configuration: null, setConfiguration: () => {}, - configurationName: undefined, - setConfigurationName: () => {}, }); /** @@ -35,17 +31,12 @@ export const OIDCConfigurationProvider = ({ const [configuration, setConfiguration] = useState( null, ); - const [configurationName, setConfigurationName] = useState< - string | undefined - >(undefined); return ( {children} diff --git a/src/hooks/jobs.tsx b/src/hooks/jobs.tsx index 07bcddca..872991b4 100644 --- a/src/hooks/jobs.tsx +++ b/src/hooks/jobs.tsx @@ -8,9 +8,9 @@ import { useOIDCContext } from "./oidcConfiguration"; * @returns the jobs */ export function useJobs() { - const { configurationName } = useOIDCContext(); + const { configuration } = useOIDCContext(); const diracxUrl = useDiracxUrl(); - const { accessToken } = useOidcAccessToken(configurationName); + const { accessToken } = useOidcAccessToken(configuration?.scope); const url = `${diracxUrl}/api/jobs/search?page=0&per_page=100`; const { data, error } = useSWR([url, accessToken, "POST"], fetcher); From 0b5dd67a81139c2b276d39d64b376355fb37dd88 Mon Sep 17 00:00:00 2001 From: aldbr Date: Tue, 5 Dec 2023 11:41:45 +0100 Subject: [PATCH 2/3] fix: theme persistence --- src/contexts/ThemeProvider.tsx | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/contexts/ThemeProvider.tsx b/src/contexts/ThemeProvider.tsx index 15ce516b..0f888191 100644 --- a/src/contexts/ThemeProvider.tsx +++ b/src/contexts/ThemeProvider.tsx @@ -1,6 +1,6 @@ "use client"; import { useMediaQuery } from "@mui/material"; -import { createContext, useState } from "react"; +import { createContext, useEffect, useState } from "react"; /** * Theme context type @@ -8,7 +8,7 @@ import { createContext, useState } from "react"; * @property toggleTheme - function to toggle the theme mode */ type ThemeContextType = { - theme: "light" | "dark"; + theme: string; toggleTheme: () => void; }; @@ -30,12 +30,22 @@ export const ThemeContext = createContext( * ThemeProvider component to provide the theme context to its children */ export const ThemeProvider = ({ children }: ThemeProviderProps) => { - // State to manage the current theme mode - const [theme, setTheme] = useState<"light" | "dark">( - useMediaQuery("(prefers-color-scheme: dark)") ? "dark" : "light", - ); + // Read the initial theme from localStorage + const prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)"); + const storedTheme = localStorage.getItem("theme"); + const defaultTheme = storedTheme + ? storedTheme + : prefersDarkMode + ? "dark" + : "light"; + + const [theme, setTheme] = useState(defaultTheme); + + // Update localStorage when the theme changes + useEffect(() => { + localStorage.setItem("theme", theme); + }, [theme]); - // Function to toggle the theme mode const toggleTheme = () => { setTheme((prevTheme) => (prevTheme === "light" ? "dark" : "light")); }; From 141c59bcad56accdc718c9ae0dc5a2aa50705492 Mon Sep 17 00:00:00 2001 From: aldbr Date: Tue, 19 Dec 2023 10:19:03 +0100 Subject: [PATCH 3/3] fix: tests --- src/hooks/theme.tsx | 4 +++- ...LoginButton.test.tsx => ProfileButton.test.tsx} | 14 ++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) rename test/unit-tests/{LoginButton.test.tsx => ProfileButton.test.tsx} (84%) diff --git a/src/hooks/theme.tsx b/src/hooks/theme.tsx index dc854096..9ab24661 100644 --- a/src/hooks/theme.tsx +++ b/src/hooks/theme.tsx @@ -1,4 +1,5 @@ import { ThemeContext } from "@/contexts/ThemeProvider"; +import { PaletteMode } from "@mui/material"; import { createTheme } from "@mui/material/styles"; import { useContext } from "react"; @@ -20,13 +21,14 @@ export const useTheme = () => { * @returns the Material-UI theme * @throws an error if the hook is not used within a ThemeProvider */ + export const useMUITheme = () => { const { theme } = useTheme(); // Create a Material-UI theme based on the current mode const muiTheme = createTheme({ palette: { - mode: theme, + mode: theme as PaletteMode, primary: { main: "#ffffff", }, diff --git a/test/unit-tests/LoginButton.test.tsx b/test/unit-tests/ProfileButton.test.tsx similarity index 84% rename from test/unit-tests/LoginButton.test.tsx rename to test/unit-tests/ProfileButton.test.tsx index dd8f2015..5d0714bc 100644 --- a/test/unit-tests/LoginButton.test.tsx +++ b/test/unit-tests/ProfileButton.test.tsx @@ -2,6 +2,7 @@ import React from "react"; import { render, fireEvent } from "@testing-library/react"; import { ProfileButton } from "@/components/ui/ProfileButton"; import { useOidcAccessToken, useOidc } from "@axa-fr/react-oidc"; +import { OIDCConfigurationContext } from "@/contexts/OIDCConfigurationProvider"; // Mocking the hooks jest.mock("@axa-fr/react-oidc"); @@ -51,11 +52,20 @@ describe("", () => { logout: mockLogout, }); (useOidcAccessToken as jest.Mock).mockReturnValue({ - accessToken: "mockAccessToken", accessTokenPayload: { preferred_username: "John" }, }); - const { getByText } = render(); + // Mock context value + const mockContextValue = { + configuration: { scope: "mockScope" }, + setConfiguration: jest.fn(), + }; + + const { getByText } = render( + + + , + ); // Open the menu by clicking the avatar fireEvent.click(getByText("J"));