diff --git a/package-lock.json b/package-lock.json
index 3320690b..e899e558 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -102,11 +102,11 @@
}
},
"node_modules/@babel/code-frame": {
- "version": "7.22.13",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz",
- "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==",
+ "version": "7.23.4",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.4.tgz",
+ "integrity": "sha512-r1IONyb6Ia+jYR2vvIDhdWdlTGhqbBoFqLTQidzZ4kepUFH15ejXvFHxCVbtl7BOXIudsIubf4E81xeA3h3IXA==",
"dependencies": {
- "@babel/highlight": "^7.22.13",
+ "@babel/highlight": "^7.23.4",
"chalk": "^2.4.2"
},
"engines": {
@@ -232,12 +232,12 @@
}
},
"node_modules/@babel/generator": {
- "version": "7.23.0",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz",
- "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==",
+ "version": "7.23.4",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.4.tgz",
+ "integrity": "sha512-esuS49Cga3HcThFNebGhlgsrVLkvhqvYDTzgjfFFlHJcIfLe5jFmRRfCQ1KuBfc4Jrtn3ndLgKWAKjBE+IraYQ==",
"dev": true,
"dependencies": {
- "@babel/types": "^7.23.0",
+ "@babel/types": "^7.23.4",
"@jridgewell/gen-mapping": "^0.3.2",
"@jridgewell/trace-mapping": "^0.3.17",
"jsesc": "^2.5.1"
@@ -369,9 +369,9 @@
}
},
"node_modules/@babel/helper-string-parser": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz",
- "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==",
+ "version": "7.23.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz",
+ "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==",
"engines": {
"node": ">=6.9.0"
}
@@ -408,9 +408,9 @@
}
},
"node_modules/@babel/highlight": {
- "version": "7.22.20",
- "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz",
- "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==",
+ "version": "7.23.4",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz",
+ "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==",
"dependencies": {
"@babel/helper-validator-identifier": "^7.22.20",
"chalk": "^2.4.2",
@@ -485,9 +485,9 @@
}
},
"node_modules/@babel/parser": {
- "version": "7.23.0",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz",
- "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==",
+ "version": "7.23.4",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.4.tgz",
+ "integrity": "sha512-vf3Xna6UEprW+7t6EtOmFpHNAuxw3xqPZghy+brsnusscJRW5BMUzzHZc5ICjULee81WeUV2jjakG09MDglJXQ==",
"dev": true,
"bin": {
"parser": "bin/babel-parser.js"
@@ -699,19 +699,19 @@
}
},
"node_modules/@babel/traverse": {
- "version": "7.23.0",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.0.tgz",
- "integrity": "sha512-t/QaEvyIoIkwzpiZ7aoSKK8kObQYeF7T2v+dazAYCb8SXtp58zEVkWW7zAnju8FNKNdr4ScAOEDmMItbyOmEYw==",
+ "version": "7.23.4",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.4.tgz",
+ "integrity": "sha512-IYM8wSUwunWTB6tFC2dkKZhxbIjHoWemdK+3f8/wq8aKhbUscxD5MX72ubd90fxvFknaLPeGw5ycU84V1obHJg==",
"dev": true,
"dependencies": {
- "@babel/code-frame": "^7.22.13",
- "@babel/generator": "^7.23.0",
+ "@babel/code-frame": "^7.23.4",
+ "@babel/generator": "^7.23.4",
"@babel/helper-environment-visitor": "^7.22.20",
"@babel/helper-function-name": "^7.23.0",
"@babel/helper-hoist-variables": "^7.22.5",
"@babel/helper-split-export-declaration": "^7.22.6",
- "@babel/parser": "^7.23.0",
- "@babel/types": "^7.23.0",
+ "@babel/parser": "^7.23.4",
+ "@babel/types": "^7.23.4",
"debug": "^4.1.0",
"globals": "^11.1.0"
},
@@ -729,11 +729,11 @@
}
},
"node_modules/@babel/types": {
- "version": "7.23.0",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz",
- "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==",
+ "version": "7.23.4",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.4.tgz",
+ "integrity": "sha512-7uIFwVYpoplT5jp/kVv6EF93VaJ8H+Yn5IczYiaAi98ajzjfoZfslet/e0sLh+wVBjb2qqIut1b0S26VSafsSQ==",
"dependencies": {
- "@babel/helper-string-parser": "^7.22.5",
+ "@babel/helper-string-parser": "^7.23.4",
"@babel/helper-validator-identifier": "^7.22.20",
"to-fast-properties": "^2.0.0"
},
diff --git a/src/app/auth/page.tsx b/src/app/auth/page.tsx
index 1dd80041..9ae44a30 100644
--- a/src/app/auth/page.tsx
+++ b/src/app/auth/page.tsx
@@ -1,4 +1,4 @@
-import { LoginForm } from "@/components/ui/LoginForm";
+import { LoginForm } from "@/components/applications/LoginForm";
export default function Page() {
return ;
diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx
index 94e8f50b..dbab5b03 100644
--- a/src/app/dashboard/layout.tsx
+++ b/src/app/dashboard/layout.tsx
@@ -1,6 +1,6 @@
import React from "react";
import Dashboard from "@/components/layout/Dashboard";
-import { OIDCSecure } from "@/components/auth/OIDCUtils";
+import { OIDCSecure } from "@/components/layout/OIDCSecure";
export default function DashboardLayout({
children,
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 657ccd2e..d6f170ac 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,5 +1,5 @@
import { Inter } from "next/font/google";
-import { OIDCProvider } from "@/components/auth/OIDCUtils";
+import { OIDCConfigurationProvider } from "@/contexts/OIDCConfigurationProvider";
import { ThemeProvider } from "@/contexts/ThemeProvider";
const inter = Inter({ subsets: ["latin"] });
@@ -21,9 +21,9 @@ export default function RootLayout({
return (
-
+
{children}
-
+
);
diff --git a/src/components/applications/LoginForm.tsx b/src/components/applications/LoginForm.tsx
new file mode 100644
index 00000000..72068e9b
--- /dev/null
+++ b/src/components/applications/LoginForm.tsx
@@ -0,0 +1,237 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import Box from "@mui/material/Box";
+import Typography from "@mui/material/Typography";
+import FormControl from "@mui/material/FormControl";
+import InputLabel from "@mui/material/InputLabel";
+import Select, { SelectChangeEvent } from "@mui/material/Select";
+import MenuItem from "@mui/material/MenuItem";
+import Button from "@mui/material/Button";
+import Autocomplete from "@mui/material/Autocomplete";
+import TextField from "@mui/material/TextField";
+import { useMetadata, Metadata } from "@/hooks/metadata";
+import NextLink from "next/link";
+import Image from "next/image";
+import { CssBaseline, Stack } from "@mui/material";
+import { useMUITheme } from "@/hooks/theme";
+import { ThemeProvider as MUIThemeProvider } from "@mui/material/styles";
+import { useRouter } from "next/navigation";
+import { useOIDCContext } from "@/hooks/oidcConfiguration";
+import * as React from "react";
+import { useOidc } from "@axa-fr/react-oidc";
+import { deepOrange, lightGreen } from "@mui/material/colors";
+
+/**
+ * Login form
+ * @returns a form
+ */
+export function LoginForm() {
+ const theme = useMUITheme();
+ const router = useRouter();
+ 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);
+
+ // Login if not authenticated
+ useEffect(() => {
+ if (configurationName && isAuthenticated === false) {
+ login();
+ }
+ }, [configurationName, isAuthenticated, login]);
+
+ // Get default group
+ const getDefaultGroup = (data: Metadata | undefined, vo: string): string => {
+ if (!data) {
+ return "";
+ }
+
+ const defaultGroup = data.virtual_organizations[vo]?.default_group;
+ if (defaultGroup) {
+ return defaultGroup;
+ } else {
+ const groupKeys = Object.keys(data.virtual_organizations[vo].groups);
+ return groupKeys.length > 0 ? groupKeys[0] : "";
+ }
+ };
+
+ // Set vo
+ const handleVOChange = (
+ event: React.SyntheticEvent,
+ newValue: string | null,
+ ) => {
+ if (newValue) {
+ setSelectedVO(newValue);
+ setSelectedGroup(getDefaultGroup(data, newValue));
+ }
+ };
+
+ // Set group
+ const handleGroupChange = (event: SelectChangeEvent) => {
+ const value = event.target.value;
+ setSelectedGroup(value);
+ };
+
+ // Update OIDC configuration
+ const handleConfigurationChanges = () => {
+ if (selectedVO && selectedGroup && configuration) {
+ const newScope = `vo:${selectedVO} group:${selectedGroup}`;
+ setConfiguration({
+ ...configuration,
+ scope: newScope,
+ });
+ setConfigurationName(newScope);
+
+ sessionStorage.setItem("oidcConfigName", JSON.stringify(newScope));
+ login();
+ }
+ };
+ // Redirect to dashboard if already authenticated
+ if (isAuthenticated) {
+ router.push("/dashboard");
+ return null;
+ }
+
+ if (isLoading) {
+ return Loading...
;
+ }
+ if (error) {
+ return An error occurred while fetching metadata.
;
+ }
+ if (!data) {
+ return No metadata found.
;
+ }
+
+ // Is there only one VO?
+ const singleVO = data && Object.keys(data.virtual_organizations).length === 1;
+ if (singleVO && !selectedVO) {
+ setSelectedVO(Object.keys(data.virtual_organizations)[0]);
+ setSelectedGroup(
+ getDefaultGroup(data, Object.keys(data.virtual_organizations)[0]),
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {singleVO ? (
+
+ {selectedVO}
+
+ ) : (
+ option}
+ renderInput={(params) => (
+
+ )}
+ value={selectedVO}
+ onChange={handleVOChange}
+ sx={{
+ "& .MuiAutocomplete-root": {
+ // Style changes when an option is selected
+ opacity: selectedVO ? 0.5 : 1,
+ },
+ }}
+ />
+ )}
+ {selectedVO && (
+
+
+ Select a Group
+
+
+
+
+
+
+
+ Need help?{" "}
+ {data.virtual_organizations[selectedVO].support.message}
+
+
+ )}
+
+
+
+ );
+}
diff --git a/src/components/applications/Showcase.tsx b/src/components/applications/Showcase.tsx
index c6090e82..91c48732 100644
--- a/src/components/applications/Showcase.tsx
+++ b/src/components/applications/Showcase.tsx
@@ -7,7 +7,7 @@ import Grid from "@mui/material/Grid";
import Paper from "@mui/material/Paper";
import { styled } from "@mui/material/styles";
import { DiracLogo } from "../ui/DiracLogo";
-import { LoginButton } from "../ui/LoginButton";
+import { ProfileButton } from "../ui/ProfileButton";
import { Box, Stack } from "@mui/material";
import { DashboardButton } from "../ui/DashboardButton";
import Image from "next/image";
@@ -47,7 +47,7 @@ export default function Showcase() {
-
+
diff --git a/src/components/applications/UserDashboard.tsx b/src/components/applications/UserDashboard.tsx
index 5d10a658..a3fd9c99 100644
--- a/src/components/applications/UserDashboard.tsx
+++ b/src/components/applications/UserDashboard.tsx
@@ -5,6 +5,7 @@ import { Box } from "@mui/material";
import { useMUITheme } from "@/hooks/theme";
import { ThemeProvider as MUIThemeProvider } from "@mui/material/styles";
import { useOidcAccessToken } from "@axa-fr/react-oidc";
+import { useOIDCContext } from "@/hooks/oidcConfiguration";
/**
* Build the User Dashboard page
@@ -12,7 +13,8 @@ import { useOidcAccessToken } from "@axa-fr/react-oidc";
*/
export default function UserDashboard() {
const theme = useMUITheme();
- const { accessTokenPayload } = useOidcAccessToken();
+ const { configurationName } = useOIDCContext();
+ const { accessTokenPayload } = useOidcAccessToken(configurationName);
return (
diff --git a/src/components/auth/OIDCUtils.tsx b/src/components/auth/OIDCUtils.tsx
deleted file mode 100644
index 0f371c27..00000000
--- a/src/components/auth/OIDCUtils.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-"use client";
-import { OidcConfiguration, OidcProvider, useOidc } from "@axa-fr/react-oidc";
-import React, { useState, useEffect } from "react";
-import { useDiracxUrl } from "@/hooks/utils";
-import { useRouter } from "next/navigation";
-
-interface OIDCProviderProps {
- children: React.ReactNode;
-}
-
-interface OIDCProps {
- children: React.ReactNode;
-}
-
-/**
- * Wrapper around the react-oidc OidcProvider component
- * Needed because OidcProvider cannot be directly called from a server file
- * @param props - configuration of the OIDC provider
- * @returns the wrapper around OidcProvider
- */
-export function OIDCProvider(props: OIDCProviderProps) {
- const withCustomHistory = () => {
- return {
- replaceState: (url: string) => {
- // Cannot use router.replace() because the code is not adapted to NextJS 13 yet
- window.location.href = url;
- },
- };
- };
- const diracxUrl = useDiracxUrl();
- const [configuration, setConfiguration] = useState(
- null,
- );
-
- useEffect(() => {
- if (diracxUrl !== null) {
- setConfiguration((prevConfig) => ({
- authority: diracxUrl,
- // TODO: Figure out how to get this. Hardcode? Get from a /.well-known/diracx-configuration endpoint?
- client_id: "myDIRACClientID",
- // TODO: Get this from the /.well-known/openid-configuration endpoint
- scope: "vo:diracAdmin",
- redirect_uri: `${diracxUrl}/#authentication-callback`,
- }));
- }
- }, [diracxUrl]);
-
- if (configuration === null) {
- return <>>;
- }
-
- return (
- <>
-
- {props.children}
-
- >
- );
-}
-
-/**
- * Check whether the user is authenticated, and redirect to the login page if not
- * @param props - configuration of the OIDC provider
- * @returns The children if the user is authenticated, null otherwise
- */
-export function OIDCSecure({ children }: OIDCProps) {
- const { isAuthenticated } = useOidc();
- const router = useRouter();
-
- // Redirect to login page if not authenticated
- if (!isAuthenticated) {
- router.push("/auth");
- return null;
- }
-
- return <>{children}>;
-}
diff --git a/src/components/layout/Dashboard.tsx b/src/components/layout/Dashboard.tsx
index 76e8efe3..3f85fd65 100644
--- a/src/components/layout/Dashboard.tsx
+++ b/src/components/layout/Dashboard.tsx
@@ -7,7 +7,7 @@ import IconButton from "@mui/material/IconButton";
import MenuIcon from "@mui/icons-material/Menu";
import Toolbar from "@mui/material/Toolbar";
import Stack from "@mui/material/Stack";
-import { LoginButton } from "../ui/LoginButton";
+import { ProfileButton } from "../ui/ProfileButton";
import DashboardDrawer from "../ui/DashboardDrawer";
import { useMUITheme } from "@/hooks/theme";
import { ThemeProvider as MUIThemeProvider } from "@mui/material/styles";
@@ -68,7 +68,7 @@ export default function Dashboard(props: DashboardProps) {
-
+
diff --git a/src/components/layout/OIDCProvider.tsx b/src/components/layout/OIDCProvider.tsx
new file mode 100644
index 00000000..e0b3959a
--- /dev/null
+++ b/src/components/layout/OIDCProvider.tsx
@@ -0,0 +1,71 @@
+"use client";
+import { OidcProvider } from "@axa-fr/react-oidc";
+import React, { useEffect } from "react";
+import { useOIDCContext } from "@/hooks/oidcConfiguration";
+import { useDiracxUrl } from "@/hooks/utils";
+
+interface OIDCProviderProps {
+ children: React.ReactNode;
+}
+
+/**
+ * Wrapper around the react-oidc OidcProvider component
+ * Needed because OidcProvider cannot be directly called from a server file
+ * @param props - configuration of the OIDC provider
+ * @returns the wrapper around OidcProvider
+ */
+export function OIDCProvider(props: OIDCProviderProps) {
+ const {
+ configuration,
+ setConfiguration,
+ configurationName,
+ setConfigurationName,
+ } = useOIDCContext();
+ const diracxUrl = useDiracxUrl();
+
+ useEffect(() => {
+ if (!configuration && !configurationName && diracxUrl) {
+ // Get the OIDC configuration name from the session storage if it exists
+ let scope = sessionStorage.getItem("oidcConfigName") || ``;
+
+ if (scope) {
+ scope = scope.replace(/^"|"$/g, "");
+ setConfigurationName(scope);
+ }
+
+ // Set the OIDC configuration
+ setConfiguration({
+ authority: diracxUrl,
+ client_id: "myDIRACClientID",
+ scope: scope,
+ redirect_uri: `${diracxUrl}/#authentication-callback`,
+ });
+ }
+ }, [diracxUrl, configuration, setConfiguration]);
+
+ const withCustomHistory = () => {
+ return {
+ replaceState: (url: string) => {
+ // Cannot use router.replace() because the code is not adapted to NextJS 13 yet
+ window.location.href = url;
+ },
+ };
+ };
+
+ if (!configuration) {
+ // Handle the case where configuration is still being determined
+ return Loading OIDC Configuration...;
+ }
+ // Configure the OidcProvider
+ return (
+ <>
+
+ {props.children}
+
+ >
+ );
+}
diff --git a/src/components/layout/OIDCSecure.tsx b/src/components/layout/OIDCSecure.tsx
new file mode 100644
index 00000000..5ef6c167
--- /dev/null
+++ b/src/components/layout/OIDCSecure.tsx
@@ -0,0 +1,28 @@
+"use client";
+import React from "react";
+import { useRouter } from "next/navigation";
+import { useOidc } from "@axa-fr/react-oidc";
+import { useOIDCContext } from "@/hooks/oidcConfiguration";
+
+interface OIDCProps {
+ children: React.ReactNode;
+}
+
+/**
+ * Check whether the user is authenticated, and redirect to the login page if not
+ * @param props - configuration of the OIDC provider
+ * @returns The children if the user is authenticated, null otherwise
+ */
+export function OIDCSecure({ children }: OIDCProps) {
+ const { configurationName } = useOIDCContext();
+ const { isAuthenticated } = useOidc(configurationName);
+ const router = useRouter();
+
+ // Redirect to login page if not authenticated
+ if (!isAuthenticated) {
+ router.push("/auth");
+ return null;
+ }
+
+ return <>{children}>;
+}
diff --git a/src/components/ui/DashboardButton.tsx b/src/components/ui/DashboardButton.tsx
index a226b418..9cc05b2c 100644
--- a/src/components/ui/DashboardButton.tsx
+++ b/src/components/ui/DashboardButton.tsx
@@ -1,4 +1,5 @@
-import { useOidcAccessToken } from "@axa-fr/react-oidc";
+import { useOIDCContext } from "@/hooks/oidcConfiguration";
+import { useOidc } from "@axa-fr/react-oidc";
import { Button } from "@mui/material";
import { deepOrange, lightGreen } from "@mui/material/colors";
import Link from "next/link";
@@ -8,9 +9,11 @@ import Link from "next/link";
* @returns a Button
*/
export function DashboardButton() {
- const { accessToken } = useOidcAccessToken();
+ const { configurationName } = useOIDCContext();
+ const { isAuthenticated } = useOidc(configurationName);
- if (!accessToken) {
+ // Render null if the OIDC configuration is not ready or no access token is available
+ if (!isAuthenticated) {
return null;
}
diff --git a/src/components/ui/LoginForm.tsx b/src/components/ui/LoginForm.tsx
deleted file mode 100644
index 329eb3c6..00000000
--- a/src/components/ui/LoginForm.tsx
+++ /dev/null
@@ -1,209 +0,0 @@
-"use client";
-
-import { useState, useEffect } from "react";
-import Box from "@mui/material/Box";
-import Typography from "@mui/material/Typography";
-import FormControl from "@mui/material/FormControl";
-import InputLabel from "@mui/material/InputLabel";
-import Select, { SelectChangeEvent } from "@mui/material/Select";
-import MenuItem from "@mui/material/MenuItem";
-import Button from "@mui/material/Button";
-import Autocomplete from "@mui/material/Autocomplete";
-import TextField from "@mui/material/TextField";
-import { useMetadata } from "@/hooks/metadata";
-import { deepOrange, lightGreen } from "@mui/material/colors";
-import NextLink from "next/link";
-import Image from "next/image";
-import { CssBaseline, Stack } from "@mui/material";
-import { OidcConfiguration, useOidc } from "@axa-fr/react-oidc";
-import { useMUITheme } from "@/hooks/theme";
-import { ThemeProvider as MUIThemeProvider } from "@mui/material/styles";
-import { useRouter } from "next/navigation";
-import { useDiracxUrl } from "@/hooks/utils";
-
-/**
- * Login form
- * @returns a form
- */
-export function LoginForm() {
- const { login, isAuthenticated } = useOidc();
- const theme = useMUITheme();
- const router = useRouter();
- const { data, error, isLoading } = useMetadata();
- const [selectedVO, setSelectedVO] = useState(null);
- const [selectedGroup, setSelectedGroup] = useState(null);
- const [configuration, setConfiguration] = useState(
- null,
- );
- const diracxUrl = useDiracxUrl();
-
- // Set OIDC configuration
- useEffect(() => {
- if (diracxUrl !== null && selectedVO !== null && selectedGroup !== null) {
- setConfiguration(() => ({
- authority: diracxUrl,
- // TODO: Figure out how to get this. Hardcode? Get from a /.well-known/diracx-configuration endpoint?
- client_id: "myDIRACClientID",
- scope: `vo:${selectedVO} group:${selectedGroup}`,
- redirect_uri: `${diracxUrl}/#authentication-callback`,
- }));
- }
- }, [diracxUrl]);
-
- // Set default VO if only one is available
- useEffect(() => {
- if (data) {
- const vos = Object.keys(data.virtual_organizations);
- if (vos.length === 1) {
- setSelectedVO(vos[0]);
- }
- }
- }, [data]);
-
- // Redirect to dashboard if already authenticated
- if (isAuthenticated) {
- router.push("/dashboard");
- return null;
- }
-
- // Set group
- const handleGroupChange = (event: SelectChangeEvent) => {
- const value = event.target.value;
- setSelectedGroup(value);
- };
-
- // Login
- const handleLogin = () => {
- login();
- };
-
- if (isLoading) {
- return Loading...
;
- }
- if (error) {
- return An error occurred while fetching metadata.
;
- }
- if (!data) {
- return No metadata found.
;
- }
-
- // Is there only one VO?
- const singleVO = data && Object.keys(data.virtual_organizations).length === 1;
-
- return (
-
-
-
-
-
-
-
-
-
- {singleVO ? (
-
- {selectedVO}
-
- ) : (
- option}
- renderInput={(params) => (
-
- )}
- value={selectedVO}
- onChange={(event: any, newValue: string | null) => {
- setSelectedVO(newValue);
- }}
- sx={{
- "& .MuiAutocomplete-root": {
- // Style changes when an option is selected
- opacity: selectedVO ? 0.5 : 1,
- },
- }}
- />
- )}
- {selectedVO && (
-
-
- Select a Group
-
-
-
-
-
-
-
- Need help?{" "}
- {data.virtual_organizations[selectedVO].support.message}
-
-
- )}
-
-
- );
-}
diff --git a/src/components/ui/LoginButton.tsx b/src/components/ui/ProfileButton.tsx
similarity index 84%
rename from src/components/ui/LoginButton.tsx
rename to src/components/ui/ProfileButton.tsx
index 4714354e..1fb5a3fb 100644
--- a/src/components/ui/LoginButton.tsx
+++ b/src/components/ui/ProfileButton.tsx
@@ -1,4 +1,5 @@
"use client";
+import { useOIDCContext } from "@/hooks/oidcConfiguration";
import { useOidc, useOidcAccessToken } from "@axa-fr/react-oidc";
import { Logout } from "@mui/icons-material";
import {
@@ -16,12 +17,13 @@ import { deepOrange, lightGreen } from "@mui/material/colors";
import React from "react";
/**
- * Login/Logout button, expected to vary whether the user is connected
+ * Profile button, expected to vary whether the user is connected
* @returns a Button
*/
-export function LoginButton() {
- const { accessTokenPayload } = useOidcAccessToken();
- const { logout, isAuthenticated } = useOidc();
+export function ProfileButton() {
+ const { configurationName, setConfigurationName } = useOIDCContext();
+ const { accessTokenPayload } = useOidcAccessToken(configurationName);
+ const { logout, isAuthenticated } = useOidc(configurationName);
const [anchorEl, setAnchorEl] = React.useState(null);
const open = Boolean(anchorEl);
@@ -33,6 +35,9 @@ export function LoginButton() {
setAnchorEl(null);
};
const handleLogout = () => {
+ // Remove the OIDC configuration name from the session storage
+ setConfigurationName(undefined);
+ sessionStorage.removeItem("oidcConfigName");
logout();
};
diff --git a/src/contexts/OIDCConfigurationProvider.tsx b/src/contexts/OIDCConfigurationProvider.tsx
new file mode 100644
index 00000000..de3701eb
--- /dev/null
+++ b/src/contexts/OIDCConfigurationProvider.tsx
@@ -0,0 +1,54 @@
+"use client";
+
+import React, { useState, createContext } from "react";
+import { OidcConfiguration } from "@axa-fr/react-oidc";
+import { OIDCProvider } from "@/components/layout/OIDCProvider";
+
+/**
+ * OIDC configuration context
+ * @property configuration - the OIDC configuration
+ * @property setConfiguration - function to set the OIDC configuration
+ * @returns the OIDC configuration context
+ */
+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: () => {},
+});
+
+/**
+ * OIDC configuration provider
+ * @param children - the children of the provider
+ * @returns the OIDC configuration provider
+ */
+export const OIDCConfigurationProvider = ({
+ children,
+}: {
+ children: React.ReactNode;
+}) => {
+ 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 c8377d0c..07bcddca 100644
--- a/src/hooks/jobs.tsx
+++ b/src/hooks/jobs.tsx
@@ -1,14 +1,16 @@
import { useOidcAccessToken } from "@axa-fr/react-oidc";
import useSWR from "swr";
import { useDiracxUrl, fetcher } from "./utils";
+import { useOIDCContext } from "./oidcConfiguration";
/**
* Fetches the jobs from the /api/jobs/search endpoint
* @returns the jobs
*/
export function useJobs() {
+ const { configurationName } = useOIDCContext();
const diracxUrl = useDiracxUrl();
- const { accessToken } = useOidcAccessToken();
+ const { accessToken } = useOidcAccessToken(configurationName);
const url = `${diracxUrl}/api/jobs/search?page=0&per_page=100`;
const { data, error } = useSWR([url, accessToken, "POST"], fetcher);
diff --git a/src/hooks/metadata.tsx b/src/hooks/metadata.tsx
index 50c757b1..8728ad34 100644
--- a/src/hooks/metadata.tsx
+++ b/src/hooks/metadata.tsx
@@ -1,10 +1,10 @@
-import useSWR, { SWRResponse } from "swr";
+import useSWRImmutable, { SWRResponse } from "swr";
import { useDiracxUrl, fetcher } from "./utils";
/**
* Metadata returned by the /.well-known/dirac-metadata endpoint
*/
-interface Metadata {
+export interface Metadata {
virtual_organizations: {
[key: string]: {
groups: {
@@ -30,7 +30,10 @@ export function useMetadata() {
const diracxUrl = useDiracxUrl();
const url = `${diracxUrl}/.well-known/dirac-metadata`;
- const { data, error }: SWRResponse = useSWR([url], fetcher);
+ const { data, error }: SWRResponse = useSWRImmutable(
+ [url],
+ fetcher,
+ );
return {
data,
diff --git a/src/hooks/oidcConfiguration.tsx b/src/hooks/oidcConfiguration.tsx
new file mode 100644
index 00000000..41044bc5
--- /dev/null
+++ b/src/hooks/oidcConfiguration.tsx
@@ -0,0 +1,16 @@
+import { useContext } from "react";
+import { OIDCConfigurationContext } from "@/contexts/OIDCConfigurationProvider";
+
+/**
+ * Hook to use the OIDC configuration context
+ * @returns the OIDC configuration context
+ */
+export const useOIDCContext = () => {
+ const context = useContext(OIDCConfigurationContext);
+ if (!context) {
+ throw new Error(
+ "useOIDCConfigurationContext must be used within an OIDCConfigurationProvider",
+ );
+ }
+ return context;
+};
diff --git a/test/unit-tests/DashboardButton.test.tsx b/test/unit-tests/DashboardButton.test.tsx
index e16f40a5..0d371696 100644
--- a/test/unit-tests/DashboardButton.test.tsx
+++ b/test/unit-tests/DashboardButton.test.tsx
@@ -1,11 +1,11 @@
import React from "react";
import { render } from "@testing-library/react";
import { DashboardButton } from "@/components/ui/DashboardButton";
-import { useOidcAccessToken } from "@axa-fr/react-oidc";
+import { useOidc } from "@axa-fr/react-oidc";
// Mocking the useOidcAccessToken hook
jest.mock("@axa-fr/react-oidc", () => ({
- useOidcAccessToken: jest.fn(),
+ useOidc: jest.fn(),
}));
describe("", () => {
@@ -13,10 +13,10 @@ describe("", () => {
jest.clearAllMocks();
});
- it("renders the button when user is connected (has accessToken)", () => {
- // Mocking the return value of useOidcAccessToken to simulate a user with an accessToken
- (useOidcAccessToken as jest.Mock).mockReturnValue({
- accessToken: "mocked_token",
+ it("renders the button when user is connected (isAuthenticated = true)", () => {
+ // Mocking the return value of useOidcAccessToken to simulate a non-connected user
+ (useOidc as jest.Mock).mockReturnValue({
+ isAuthenticated: true,
});
const { getByText } = render();
@@ -26,8 +26,8 @@ describe("", () => {
});
it("does not render the button when user is not connected (no accessToken)", () => {
- // Mocking the return value of useOidcAccessToken to simulate a user without an accessToken
- (useOidcAccessToken as jest.Mock).mockReturnValue({ accessToken: null });
+ // Mocking the return value of useOidc to simulate a connected user
+ (useOidc as jest.Mock).mockReturnValue({ isAuthenticated: false });
const { queryByText } = render();
const button = queryByText("Dashboard");
diff --git a/test/unit-tests/LoginButton.test.tsx b/test/unit-tests/LoginButton.test.tsx
index 0efcdf38..dd8f2015 100644
--- a/test/unit-tests/LoginButton.test.tsx
+++ b/test/unit-tests/LoginButton.test.tsx
@@ -1,6 +1,6 @@
import React from "react";
import { render, fireEvent } from "@testing-library/react";
-import { LoginButton } from "@/components/ui/LoginButton";
+import { ProfileButton } from "@/components/ui/ProfileButton";
import { useOidcAccessToken, useOidc } from "@axa-fr/react-oidc";
// Mocking the hooks
@@ -10,12 +10,12 @@ beforeEach(() => {
jest.resetAllMocks();
});
-describe("", () => {
+describe("", () => {
it('displays the "Login" button when not authenticated', () => {
(useOidc as jest.Mock).mockReturnValue({ isAuthenticated: false });
(useOidcAccessToken as jest.Mock).mockReturnValue({});
- const { getByText } = render();
+ const { getByText } = render();
expect(getByText("Login")).toBeInTheDocument();
});
@@ -26,7 +26,7 @@ describe("", () => {
accessTokenPayload: { preferred_username: "John" },
});
- const { getByText } = render();
+ const { getByText } = render();
expect(getByText("J")).toBeInTheDocument(); // Assuming 'John' is the preferred username and 'J' is the first letter.
});
@@ -37,7 +37,7 @@ describe("", () => {
accessTokenPayload: { preferred_username: "John" },
});
- const { getByText, queryByText } = render();
+ const { getByText, queryByText } = render();
fireEvent.click(getByText("J"));
expect(queryByText("Profile")).toBeInTheDocument();
expect(queryByText("Logout")).toBeInTheDocument();
@@ -55,7 +55,7 @@ describe("", () => {
accessTokenPayload: { preferred_username: "John" },
});
- const { getByText } = render();
+ const { getByText } = render();
// Open the menu by clicking the avatar
fireEvent.click(getByText("J"));
diff --git a/test/unit-tests/LoginForm.test.tsx b/test/unit-tests/LoginForm.test.tsx
index 41c5ba01..26a69ff0 100644
--- a/test/unit-tests/LoginForm.test.tsx
+++ b/test/unit-tests/LoginForm.test.tsx
@@ -1,5 +1,5 @@
import { render, fireEvent, screen, within } from "@testing-library/react";
-import { LoginForm } from "@/components/ui/LoginForm";
+import { LoginForm } from "@/components/applications/LoginForm";
import { ThemeProvider } from "@/contexts/ThemeProvider";
import { useMetadata } from "@/hooks/metadata";