diff --git a/src/App.tsx b/src/App.tsx index bd35162cc..51fd4bc76 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,138 +2,25 @@ import React from "react"; // PatternFly import "@patternfly/react-core/dist/styles/base.css"; // Layouts -import { AppLayout } from "./AppLayout"; import DataSpinner from "./components/layouts/DataSpinner"; // Navigation import { AppRoutes } from "./navigation/AppRoutes"; // RPC client -import { Command, useBatchCommandQuery } from "./services/rpc"; -// Redux -import { useAppDispatch, useAppSelector } from "src/store/hooks"; -import { - updateIpaServerConfiguration, - updateLoggedUserInfo, - updateEnvironment, - updateDnsIsEnabled, - updateTrustConfiguration, - updateDomainLevel, - updateCaIsEnabled, - updateVaultConfiguration, -} from "src/store/Global/global-slice"; -import { setIsLogin, setIsLogout } from "./store/Global/auth-slice"; +import { useUserMetadataQuery } from "./services/rpcAuth"; // Alerts import ManagedAlerts from "./components/ManagedAlerts"; const App: React.FunctionComponent = () => { - const dispatch = useAppDispatch(); + const { isFetching } = useUserMetadataQuery(); - // Default: no user logged in & no loaded information about it - const [loggedInUser, setLoggedInUser] = React.useState(null); - const [hasUser, setHasUser] = React.useState(false); - const [isDataLoaded, setIsDataLoaded] = React.useState(false); - - const userLoggedIn = useAppSelector((state) => state.auth.isUserLoggedIn); - - // [API Call] Get initial data - const payloadDataBatch: Command[] = []; - const methods = [ - "config_show", - "whoami", - "env", - "dns_is_enabled", - "trustconfig_show", - "domainlevel_get", - "ca_is_enabled", - "vaultconfig_show", - ]; - - methods.map((method) => { - const payloadItem = { - method: method, - params: [[], {}], - }; - payloadDataBatch.push(payloadItem); - }); - - const { - data: initialBatchResponse, - isLoading: isInitialBatchLoading, - // TODO: Manage error handling correctly - } = useBatchCommandQuery(payloadDataBatch); - - // Store data in global slice (Redux) - React.useEffect(() => { - if (!isInitialBatchLoading && initialBatchResponse === undefined) { - // Assume that the user is not loaded - setLoggedInUser(null); - setIsDataLoaded(true); - setHasUser(false); - dispatch(setIsLogout()); - } - - if (!isInitialBatchLoading && initialBatchResponse !== undefined) { - setIsDataLoaded(true); - // 0: IPA server configuration ("config_show") - const configShowResponse = initialBatchResponse.result.results[0].result; - dispatch(updateIpaServerConfiguration(configShowResponse)); - // 1: Logged user information ("whoami") - const whoamiResponse = initialBatchResponse.result.results[1]; - const user = whoamiResponse.arguments.toString(); - dispatch(updateLoggedUserInfo(user)); - // 2: Environment ("env") - const envResponse = initialBatchResponse.result.results[2].result; - dispatch(updateEnvironment(envResponse)); - // 3: DNS is enabled ("dns_is_enabled") - const dnsEnabledResponse: boolean = - initialBatchResponse.result.results[3].result; - dispatch(updateDnsIsEnabled(dnsEnabledResponse)); - // 4: Trust configuration ("trustconfig_show") - const trustConfigResponse = initialBatchResponse.result.results[4].result; - dispatch(updateTrustConfiguration(trustConfigResponse)); - // 5: Domain level ("domainlevel_get") - const domainLevelResponse = initialBatchResponse.result.results[5].result; - dispatch(updateDomainLevel(domainLevelResponse)); - // 6: CA is enabled ("ca_is_enabled") - const caEnabledResponse = initialBatchResponse.result.results[6].result; - dispatch(updateCaIsEnabled(caEnabledResponse)); - // 7: Vault configuration ("vaultconfig_show") - const vaultConfig = initialBatchResponse.result.results[7].result; - dispatch(updateVaultConfiguration(vaultConfig)); - - // Set the login status if user found in the whoami response - if (user) { - setLoggedInUser(user); - setHasUser(true); - // [Redux] Update the login status - const loginPayload = { - loggedInUser: loggedInUser as string, - error: null, - }; - dispatch(setIsLogin(loginPayload)); - } else { - setLoggedInUser(null); - setHasUser(false); - dispatch(setIsLogout()); - } - } - }, [isInitialBatchLoading]); - - if (isInitialBatchLoading && !initialBatchResponse) { + if (isFetching) { return ; } + return ( <> - {hasUser && userLoggedIn && ( - - - - )} - {!hasUser && !userLoggedIn && ( - <> - - - )} + ); }; diff --git a/src/AppLayout.tsx b/src/AppLayout.tsx index e4cb8ec36..4d2a14011 100644 --- a/src/AppLayout.tsx +++ b/src/AppLayout.tsx @@ -36,51 +36,42 @@ import ContextualHelpPanel from "./components/ContextualHelpPanel/ContextualHelp import headerLogo from "/assets/images/header-logo.png"; import avatarImg from "/assets/images/avatarImg.svg"; // Redux -import { useAppDispatch } from "./store/hooks"; -import { setIsLogout } from "./store/Global/auth-slice"; +import { useAppDispatch, useAppSelector } from "./store/hooks"; // RPC import { useLogoutMutation } from "./services/rpcAuth"; -import { useGetUserDetailsByUidMutation } from "./services/rpcUsers"; +import { useGetUserByUidQuery } from "./services/rpcUsers"; +import { Outlet } from "react-router"; +import { logoutUser } from "./store/Global/global-slice"; -interface PropsToAppLayout { - loggedInUser: string | null; - children: React.ReactNode; -} - -const AppLayout = (props: PropsToAppLayout) => { +const AppLayout = () => { const dispatch = useAppDispatch(); + const loggedInUser = useAppSelector((state) => state.global.loggedInUser); // RPC const [logout] = useLogoutMutation(); - const [getUserDetails] = useGetUserDetailsByUidMutation(); + const { data: userDetails, isFetching } = useGetUserByUidQuery( + loggedInUser!, + { + skip: !loggedInUser, + } + ); // Retrieve and assign user full name - const [fullName, setFullName] = React.useState(""); - - React.useEffect(() => { - if (props.loggedInUser) { - getUserDetails(props.loggedInUser).then((response) => { - if ("data" in response) { - const first = response.data?.result.result.givenname; - const last = response.data?.result.result.sn; - // Some users (e.g., admin) don't have first name - if (!first) { - setFullName(last as string); - } else { - setFullName(first + " " + last); - } - } - }); + const fullName = React.useMemo(() => { + if (!userDetails) return ""; + if (userDetails?.givenname !== "") { + return userDetails?.givenname + " " + userDetails?.sn; } - }, [props.loggedInUser]); + + return userDetails?.sn; + }, [userDetails]); // On logout handler const onLogout = () => { logout().then((response) => { if ("data" in response && !response.data?.error) { - dispatch(setIsLogout()); - // Forcing full page to reload and redirect to login page - window.location.reload(); + sessionStorage.setItem("isKerberosDisabled", "true"); + dispatch(logoutUser()); } }); }; @@ -147,7 +138,7 @@ const AppLayout = (props: PropsToAppLayout) => { className="pf-v6-u-mr-md" icon={} > - {fullName} + {isFetching ? "" : fullName} )} isOpen={isDropdownOpen} @@ -230,7 +221,9 @@ const AppLayout = (props: PropsToAppLayout) => { className="--pf-t--global--text--color--regular" isContentFilled > - {props.children} + + + ); }; diff --git a/src/components/layouts/DualListLayout/DualListLayout.test.tsx b/src/components/layouts/DualListLayout/DualListLayout.test.tsx index 62ed980e7..c17c90d65 100644 --- a/src/components/layouts/DualListLayout/DualListLayout.test.tsx +++ b/src/components/layouts/DualListLayout/DualListLayout.test.tsx @@ -8,7 +8,7 @@ import { import { afterEach, describe, expect, it, Mock, vi } from "vitest"; // Component import DualListLayout, { DualListProps } from "./DualListLayout"; -import { renderWithRouter } from "src/utils/testUtils"; +import { renderWithRouter } from "src/utils/testRouterUtils"; interface MockReturn { data: { list: string[] } | { error: { message: string } }; @@ -28,10 +28,6 @@ const retrieveIDs: Mock<() => Promise> = vi.fn(async () => { }); vi.mock("src/services/rpc", () => ({ - api: { - reducer: () => ({}), - middleware: () => (next) => next, - }, useGetIDListMutation: () => [retrieveIDs], })); diff --git a/src/components/layouts/PaginationLayout.test.tsx b/src/components/layouts/PaginationLayout.test.tsx index 66111cf4e..e953c1cca 100644 --- a/src/components/layouts/PaginationLayout.test.tsx +++ b/src/components/layouts/PaginationLayout.test.tsx @@ -3,7 +3,7 @@ import { cleanup, fireEvent, screen } from "@testing-library/react"; import { afterEach, describe, expect, it, vi } from "vitest"; // Component import PaginationLayout from "./PaginationLayout"; -import { renderWithRouter } from "src/utils/testUtils"; +import { renderWithRouter } from "src/utils/testRouterUtils"; const list = Array.from({ length: 50 }, (_, i) => `item-${i + 1}`); diff --git a/src/components/modals/UserModals/ResetPassword.tsx b/src/components/modals/UserModals/ResetPassword.tsx index 169d9dc2f..7e42eb6a7 100644 --- a/src/components/modals/UserModals/ResetPassword.tsx +++ b/src/components/modals/UserModals/ResetPassword.tsx @@ -27,9 +27,7 @@ const ResetPassword = (props: PropsToResetPassword) => { const dispatch = useAppDispatch(); // Get current logged-in user info - const loggedInUser = useAppSelector( - (state) => state.global.loggedUserInfo.arguments - ); + const loggedInUser = useAppSelector((state) => state.global.loggedInUser); // RPC hooks const [resetPassword] = useChangePasswordMutation(); diff --git a/src/login/LoginMainPage.tsx b/src/login/LoginMainPage.tsx index 291c7a27d..ea90ead40 100644 --- a/src/login/LoginMainPage.tsx +++ b/src/login/LoginMainPage.tsx @@ -4,7 +4,6 @@ import { LoginFooterItem, LoginForm, LoginMainFooterLinksItem, - LoginPage, ListItem, ListVariant, Content, @@ -14,6 +13,7 @@ import { ModalBody, ModalHeader, ModalFooter, + List, } from "@patternfly/react-core"; // Hooks import { addAlert } from "src/store/Global/alerts-slice"; @@ -24,6 +24,7 @@ import BrandImg from "/assets/images/product-name.png"; import BackgroundImg from "/assets/images/login-screen-background.jpg"; // RPC import { + authApi, MetaResponse, useKrbLoginMutation, useUserPasswordLoginMutation, @@ -31,10 +32,10 @@ import { } from "src/services/rpcAuth"; // Redux import { useAppDispatch } from "src/store/hooks"; -import { setIsLogin } from "src/store/Global/auth-slice"; // Navigation import { useLocation, useNavigate } from "react-router"; import { Link } from "react-router"; +import { LoginPage } from "./LoginPage"; interface StateFromSyncOtpPage { alertMessage: string; @@ -71,10 +72,6 @@ const LoginMainPage = () => { const [isValidPassword, setIsValidPassword] = React.useState(true); const [authenticating, setAuthenticating] = React.useState(false); - // Authentication method (assumes user + password by default) - // - This will help to get the user credentials if the user is logged in via Kerberos - let isUserPwdAuthentication = true; - const handleUsernameChange = ( _event: React.FormEvent, value: string @@ -94,7 +91,8 @@ const LoginMainPage = () => { * 1.- Check if Kerberos is enabled * 2.- Based on the result, authenticate using one method (Kerberos) or the other (via user + password) */ - const isKerberosEnabled = true; + const isKerberosDisabled = + sessionStorage.getItem("isKerberosDisabled") === "true"; // API calls const [onUserPwdLogin] = useUserPasswordLoginMutation(); @@ -103,7 +101,7 @@ const LoginMainPage = () => { // Kerberos login when loading the component React.useEffect(() => { - if (!username && isKerberosEnabled) { + if (!username && !isKerberosDisabled) { onKrbLogin().then((response) => { if ("error" in response) { const receivedError = response.error as MetaResponse; @@ -116,15 +114,11 @@ const LoginMainPage = () => { wwwAuthenticateHeader?.startsWith("Negotiate") ) { // Success on Kerberos login - isUserPwdAuthentication = false; onSuccessLogin(); } else { // Set error without showing the modal setErrorMessage("Authentication with Kerberos failed"); } - } else { - isUserPwdAuthentication = false; - onSuccessLogin(); } }); } @@ -168,16 +162,13 @@ const LoginMainPage = () => { // Action on login success const onSuccessLogin = () => { - // Sore data on Redux - if (isUserPwdAuthentication) { - dispatch(setIsLogin({ loggedInUser: username, error: null })); - } else { - // TODO: Extract the username from the Kerberos ticket and store in Redux - } - - // Forcing full page to reload and access the protected pages (Default: active users) - window.location.reload(); - // TODO: Improve this mechanism and redirect to the last page visited + dispatch( + authApi.endpoints.userMetadata.initiate(undefined, { + forceRefetch: true, + subscribe: false, + }) + ); + sessionStorage.removeItem("isKerberosDisabled"); }; // Analyze the error reason @@ -225,58 +216,71 @@ const LoginMainPage = () => { event: React.MouseEvent ) => { event.preventDefault(); - setIsValidUsername(!!username); - setIsValidPassword(!!password); - setShowHelperText(!username || !password); setAuthenticating(true); - if (!username && isKerberosEnabled) { - onKrbLogin().then((response) => { - if ("error" in response) { - const receivedError = response.error as MetaResponse; + if (!username) { + onKrbLogin() + .then((response) => { + if ("error" in response) { + const receivedError = response.error as MetaResponse; - const status = receivedError.response?.status; - const wwwAuthenticateHeader = - receivedError.response?.headers.get("www-authenticate"); - if ( - status === 200 && - wwwAuthenticateHeader?.startsWith("Negotiate") - ) { + const status = receivedError.response?.status; + const wwwAuthenticateHeader = + receivedError.response?.headers.get("www-authenticate"); + if ( + status === 200 && + wwwAuthenticateHeader?.startsWith("Negotiate") + ) { + // Success on Kerberos login + onSuccessLogin(); + } else { + // Set error without showing the modal + setErrorMessage("Authentication with Kerberos failed"); + setIsValidUsername(!!username); + setIsValidPassword(!!password); + setShowHelperText(!username || !password); + } + } else { // Success on Kerberos login - isUserPwdAuthentication = false; onSuccessLogin(); - } else { - // Set error without showing the modal - setErrorMessage("Authentication with Kerberos failed"); } - } else { - // Success on Kerberos login - isUserPwdAuthentication = false; - onSuccessLogin(); - } - }); + }) + .catch(() => { + setIsValidUsername(!!username); + setIsValidPassword(!!password); + setShowHelperText(!username || !password); + }) + .finally(() => { + setAuthenticating(false); + }); } else { - onUserPwdLogin({ username, password }).then((response) => { - if ("error" in response) { - const receivedError = response.error as MetaResponse; + setIsValidUsername(!!username); + setIsValidPassword(!!password); + setShowHelperText(!username || !password); + onUserPwdLogin({ username, password }) + .then((response) => { + if ("error" in response) { + const receivedError = response.error as MetaResponse; - // Get the reason of the error - const reason = receivedError.response?.headers.get( - "x-ipa-rejection-reason" - ); + // Get the reason of the error + const reason = receivedError.response?.headers.get( + "x-ipa-rejection-reason" + ); - const msg = analyzeErrorReason(reason); + const msg = analyzeErrorReason(reason); - if (msg) { - navigate("/reset-password/" + username, { - state: { username, msg }, - }); + if (msg) { + navigate("/reset-password/" + username, { + state: { username, msg }, + }); + } + } else { + onSuccessLogin(); } - } else { - onSuccessLogin(); - } - setAuthenticating(false); - }); + }) + .finally(() => { + setAuthenticating(false); + }); } }; @@ -284,24 +288,27 @@ const LoginMainPage = () => { const onLoginWithCertClick = (_event) => { _event.preventDefault(); setAuthenticating(true); - onCertLogin(username).then((response) => { - if ("error" in response) { - const receivedError = response.error as MetaResponse; - const status = receivedError.response?.status; - const statusText = "Authentication with personal certificate failed"; + onCertLogin(username) + .then((response) => { + if ("error" in response) { + const receivedError = response.error as MetaResponse; + const status = receivedError.response?.status; + const statusText = "Authentication with personal certificate failed"; - if (status === 200) { - onSuccessLogin(); + if (status === 200) { + onSuccessLogin(); + } else { + // Set error without showing the modal + setErrorMessage(statusText); + setShowHelperText(true); + } } else { - // Set error without showing the modal - setErrorMessage(statusText); - setShowHelperText(true); + onSuccessLogin(); } - } else { - onSuccessLogin(); - } - setAuthenticating(false); - }); + }) + .finally(() => { + setAuthenticating(false); + }); }; const socialMediaLoginContent = ( @@ -369,30 +376,44 @@ const LoginMainPage = () => { /> ); - const placeHolderText = - "· To log in with username and password, enter them in the corresponding fields, then click 'Log in'. \n\n" + - "· To log in with Kerberos, please make sure you have valid tickets (obtainable via kinit) and configured the browser correctly, then click 'Log in'. \n\n" + - "· To log in with certificate, please make sure you have valid personal certificate."; + const placeHolderText = ( + + + Take me back to the old WebUI + + + To log in with username and password, enter them in the corresponding + fields, then click 'Log in'. + + + To log in with Kerberos, please make sure you have valid tickets + (obtainable via kinit) and configured the browser correctly, then click + 'Log in'. + + + To log in with certificate, please make sure you have valid personal + certificate. + + + ); return ( - <> - - {loginForm} - {showErrorModal && errorModal(errorMessage)} - - + + {loginForm} + {showErrorModal && errorModal(errorMessage)} + ); }; diff --git a/src/login/LoginPage.tsx b/src/login/LoginPage.tsx new file mode 100644 index 000000000..769076159 --- /dev/null +++ b/src/login/LoginPage.tsx @@ -0,0 +1,106 @@ +import React from "react"; +import { css } from "@patternfly/react-styles"; + +import { + BackgroundImage, + Brand, + List, + ListVariant, + Login, + LoginHeader, + LoginFooter, + LoginMainHeader, + LoginMainBody, + LoginMainFooter, +} from "@patternfly/react-core"; + +interface LoginPageProps extends React.HTMLProps { + /** Anything that can be rendered inside of the login page (e.g. ) */ + children?: React.ReactNode; + /** Additional classes added to the login page */ + className?: string; + /** Attribute that specifies the URL of the brand image for the login page */ + brandImgSrc?: string; + /** Attribute that specifies the alt text of the brand image for the login page */ + brandImgAlt?: string; + /** Attribute that specifies the URL of the background image for the login page */ + backgroundImgSrc?: string; + /** Content rendered inside of the text component of the login page */ + loginPageContent?: React.ReactNode; + /** Items rendered inside of the footer list component of the login page */ + footerListItems?: React.ReactNode; + /** Adds list variant styles for the footer list component of the login page. The only current value is'inline' */ + footerListVariants?: ListVariant.inline; + /** Title for the login main body header of the login page */ + loginTitle: string; + /** Subtitle for the login main body header of the login page */ + loginSubtitle?: string; + /** Header utilities for the login main body header of the login page */ + headerUtilities?: React.ReactNode; + /** Content rendered inside of login main footer band to display a sign up for account message */ + signUpForAccountMessage?: React.ReactNode; + /** Content rendered inside of login main footer band to display a forgot credentials link. */ + forgotCredentials?: React.ReactNode; + /** Content rendered inside of social media login footer section */ + socialMediaLoginContent?: React.ReactNode; + /** Adds an accessible name to the social media login list. */ + socialMediaLoginAriaLabel?: string; +} + +export const LoginPage: React.FunctionComponent = ({ + children = null, + className = "", + brandImgSrc = "", + brandImgAlt = "", + backgroundImgSrc = "", + footerListItems = null, + loginPageContent = null, + footerListVariants, + loginTitle, + loginSubtitle, + headerUtilities, + signUpForAccountMessage = null, + forgotCredentials = null, + socialMediaLoginContent = null, + socialMediaLoginAriaLabel, + ...props +}: LoginPageProps) => { + const HeaderBrand = ; + const Header = ; + const Footer = ( + + {loginPageContent} + {footerListItems} + + ); + + return ( + <> + {backgroundImgSrc && } + + + {children} + {(socialMediaLoginContent || + forgotCredentials || + signUpForAccountMessage) && ( + + )} + + + ); +}; +LoginPage.displayName = "LoginPage"; diff --git a/src/main.css b/src/main.css index 146349841..ec8e564be 100644 --- a/src/main.css +++ b/src/main.css @@ -44,6 +44,10 @@ code { padding: 0.2em; } +.login-page-list { + list-style-type: "· "; +} + .topology-ca-blue-edge { &.pf-topology__edge, .pf-topology-connector-arrow { diff --git a/src/navigation/AppRoutes.tsx b/src/navigation/AppRoutes.tsx index 69c609acb..71dc82924 100644 --- a/src/navigation/AppRoutes.tsx +++ b/src/navigation/AppRoutes.tsx @@ -1,10 +1,7 @@ -/* eslint-disable react/prop-types */ import * as React from "react"; // React router dom import { Navigate, Route, Routes } from "react-router"; import { NotFound } from "src/components/errors/PageErrors"; -// Layouts -import DataSpinner from "src/components/layouts/DataSpinner"; // Redux import { useAppSelector } from "src/store/hooks"; @@ -83,563 +80,537 @@ import Roles from "src/pages/Roles/Roles"; import RolesTabs from "src/pages/Roles/RolesTabs"; import Privileges from "src/pages/Privileges/Privileges"; import PrivilegesTabs from "src/pages/Privileges/PrivilegesTabs"; +import { AppLayout } from "src/AppLayout"; // Renders routes (React) -export const AppRoutes = ({ isInitialDataLoaded }): React.ReactElement => { +export const AppRoutes = (): React.ReactElement => { // Redux: Get if user is logged in - const userLoggedIn = useAppSelector((state) => state.auth.isUserLoggedIn); + const loggedInUser = useAppSelector((state) => state.global.loggedInUser); const configurationSettings = useConfigurationSettings(); const dnsIsEnabled = configurationSettings.dnsIsEnabled; return ( - <> - {!isInitialDataLoaded ? ( - - ) : ( - - {userLoggedIn ? ( - <> - - } /> - - } /> - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - - - - } /> - - } /> - - - - } /> - - } /> - - - - } /> - - } /> - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - - - - } /> - - } - /> - } - /> - } - /> - - - - } /> - - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - - - - } /> - - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - - - - } /> - - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - - - - } /> - - } /> - } - /> - } - /> - } - /> - + + {loggedInUser !== "" ? ( + <> + }> + + } /> + + } /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> - - } /> - - - } - /> - + + + } /> + + } /> + + + + } /> + + } /> + + + + } /> + + } /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> - - } /> - - - } - /> - + + + } /> + + } /> + } + /> + } + /> - - } /> - - } - /> - + + + } /> + + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> - - } /> - - } /> - + + + } /> + + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> - - } /> + + + } /> + + } /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> - - } /> - - } - /> - - } - /> - + + + } /> + + } /> + } + /> + } + /> + } + /> - - } /> - - } - /> - } - /> - + + + } /> + + + } + /> - - } /> + + + } /> + + + } + /> - - } /> + + + } /> + + } /> + + + + } /> + + } /> + + + + } /> + + + } /> + + } + /> } + path="memberof_hbacsvcgroup" + element={} /> - - } /> - - } - /> - } - /> - + + + } /> + + } + /> + } + /> - - } /> - - } - /> - } - /> - + + + } /> + + + } /> + } + /> + + + } /> + + } /> + } + /> - - } /> + + + } /> + + } + /> + } + /> - - } /> - - } - /> - + + + } /> + + + } /> + + } + /> - - } /> + + + } /> + + + } /> + + } /> + } + /> - - } /> - - } - /> - } - /> - + + + } /> + + } + /> - - } /> - - } - /> - + + + } /> + + } + /> - - } /> - + + + } /> + + + } /> + + {dnsIsEnabled && ( + + } /> + } + element={} /> - - - - } /> - - - } /> - - {dnsIsEnabled && ( - - } /> - + } + element={} /> - - } - /> - } - /> - - - - )} - - } /> - - } - /> - - - - } /> - - } - /> - - - - } /> - - - } /> - - - } /> - - } - /> - - - - } /> - - } /> - } + path=":recordName" + element={} /> - - } /> - - - } /> + )} + + } /> + + } + /> - - } /> - - } /> - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - + + + } /> + + } + /> - - } /> - + + + } /> + + + } /> + + + } /> + + } /> + + + + } /> + + } /> + } + element={} /> - } /> - {/* Redirect to Active users page if user is logged in and navigates to the root page */} - } /> - } - /> - {/* 404 page */} - } /> - - ) : ( - <> - } /> - - } /> + + + } /> + + + } /> + + + } /> + + } /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> - } /> - - )} - {/* Browser configuration page */} - } /> - {/* Sync OTP token page */} - } /> - + + + } /> + + } + /> + + + } /> + {/* Redirect to Active users page if user is logged in and navigates to the root page */} + } /> + } /> + {/* 404 page */} + + } /> + + ) : ( + <> + } /> + + } /> + + } /> + )} - + {/* Browser configuration page */} + } /> + {/* Sync OTP token page */} + } /> + ); }; diff --git a/src/services/rpcAuth.ts b/src/services/rpcAuth.ts index 63d151d1c..047da1f85 100644 --- a/src/services/rpcAuth.ts +++ b/src/services/rpcAuth.ts @@ -1,9 +1,17 @@ -import { api, FindRPCResponse, getCommandNoVersion } from "./rpc"; +import { + api, + BatchRPCResponse, + Command, + FindRPCResponse, + getBatchCommand, + getCommandNoVersion, +} from "./rpc"; import { URL_PREFIX } from "src/navigation/NavRoutes"; import { FetchBaseQueryError, FetchBaseQueryMeta, } from "@reduxjs/toolkit/query"; +import { API_VERSION_BACKUP } from "src/utils/utils"; /** * Endpoints: userPasswordLogin, logout @@ -12,6 +20,37 @@ import { * - session_logout: https://freeipa.readthedocs.io/en/latest/api/session_logout.html */ +const BATCH_COMMANDS_AUTH = [ + "config_show", + "whoami", + "env", + "dns_is_enabled", + "trustconfig_show", + "domainlevel_get", + "ca_is_enabled", + "vaultconfig_show", +]; + +const BATCH_COMMANDS_AUTH_PAYLOAD: Command[] = BATCH_COMMANDS_AUTH.map( + (method) => { + return { + method: method, + params: [[], {}], + }; + } +); + +export interface UserMetadata { + ipaServerConfiguration: Record; + loggedInUser: string; + environment: Record; + dnsIsEnabled: boolean; + trustConfiguration: Record; + domainLevel: number; + caIsEnabled: boolean; + vaultConfiguration: Record; +} + interface UserPasswordPayload { username: string; password: string; @@ -207,9 +246,29 @@ const extendedApi = api.injectEndpoints({ return meta as unknown as MetaResponse; }, }), + userMetadata: build.query({ + query: () => + getBatchCommand(BATCH_COMMANDS_AUTH_PAYLOAD, API_VERSION_BACKUP), + transformResponse: (response: BatchRPCResponse): UserMetadata => { + const results = response.result.results; + const whoamiResponse = results[1] as Record; + return { + ipaServerConfiguration: results[0].result, + loggedInUser: whoamiResponse.arguments?.toString() ?? "", + environment: results[2].result, + dnsIsEnabled: results[3].result as boolean, + trustConfiguration: results[4].result, + domainLevel: results[5].result, + caIsEnabled: results[6].result, + vaultConfiguration: results[7].result, + }; + }, + }), }), }); +export const authApi = extendedApi; + export const { useUserPasswordLoginMutation, useLogoutMutation, @@ -217,4 +276,5 @@ export const { useX509LoginMutation, useResetPasswordMutation, useSyncOtpMutation, -} = extendedApi; + useUserMetadataQuery, +} = authApi; diff --git a/src/services/rpcUsers.ts b/src/services/rpcUsers.ts index 269def256..85e00e7d0 100644 --- a/src/services/rpcUsers.ts +++ b/src/services/rpcUsers.ts @@ -546,14 +546,6 @@ const extendedApi = api.injectEndpoints({ return userList; }, }), - getUserDetailsByUid: build.mutation({ - query: (uid) => { - return getCommand({ - method: "user_show", - params: [[uid], { version: API_VERSION_BACKUP }], - }); - }, - }), userFind: build.query({ query: (payload) => { // Add noMembers option if it exists @@ -675,7 +667,6 @@ export const { useGetRadiusProxyQuery, useGetIdpServerQuery, useGetUsersInfoByUidQuery, - useGetUserDetailsByUidMutation, useUserFindQuery, useAddUserMutation, } = extendedApi; diff --git a/src/store/Global/auth-slice.ts b/src/store/Global/auth-slice.ts deleted file mode 100644 index 22daeaeee..000000000 --- a/src/store/Global/auth-slice.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { createSlice, PayloadAction } from "@reduxjs/toolkit"; - -interface onLoginPayload { - loggedInUser: string; - error: string | null; -} - -interface AuthState { - isUserLoggedIn: boolean; - user: string | null; - error: string | null; -} - -const initialState: AuthState = { - isUserLoggedIn: false, - user: null, - error: null, -}; - -const authSlice = createSlice({ - name: "auth", - initialState, - reducers: { - setIsLogin: (state, action: PayloadAction) => { - state.isUserLoggedIn = true; - state.user = action.payload.loggedInUser; - state.error = action.payload.error; - }, - setIsLogout: (state) => { - state.isUserLoggedIn = false; - state.user = null; - state.error = null; - }, - }, -}); - -export const { setIsLogin, setIsLogout } = authSlice.actions; -export default authSlice.reducer; diff --git a/src/store/Global/global-slice.ts b/src/store/Global/global-slice.ts index 5e9cbefcb..0d01a70bc 100644 --- a/src/store/Global/global-slice.ts +++ b/src/store/Global/global-slice.ts @@ -1,37 +1,14 @@ -import { createSlice, PayloadAction } from "@reduxjs/toolkit"; +import { createSlice } from "@reduxjs/toolkit"; +import { authApi, UserMetadata } from "src/services/rpcAuth"; -interface GlobalState { - // TODO: Specify data types - ipaServerConfiguration: Record; - loggedUserInfo: LoggedUserInfo; - environment: Record; - dnsIsEnabled: boolean; - trustConfiguration: Record; - domainLevel: Record; - caIsEnabled: Record; - vaultConfiguration: Record; -} - -interface LoggedUserInfo { - arguments: string | Record; - command: string; - error: Record; - object: string; -} - -const initialState: GlobalState = { +const initialState: UserMetadata = { ipaServerConfiguration: {}, - loggedUserInfo: { - arguments: "", - command: "", - error: {}, - object: "", - }, + loggedInUser: "", environment: {}, dnsIsEnabled: false, trustConfiguration: {}, - domainLevel: {}, - caIsEnabled: {}, + domainLevel: 0, + caIsEnabled: false, vaultConfiguration: {}, }; @@ -39,73 +16,24 @@ const globalSlice = createSlice({ name: "global", initialState, reducers: { - updateIpaServerConfiguration: ( - state, - action: PayloadAction> - ) => { - const newIpaServerConfig = action.payload; - state.ipaServerConfiguration = newIpaServerConfig; - }, - updateLoggedUserInfo: ( - state, - action: PayloadAction> - ) => { - const newLoggedUserInfo = action.payload; - state.loggedUserInfo = { - ...state.loggedUserInfo, - arguments: newLoggedUserInfo, - }; - }, - updateEnvironment: ( - state, - action: PayloadAction> - ) => { - const newEnv = action.payload; - state.environment = newEnv; - }, - updateDnsIsEnabled: (state, action: PayloadAction) => { - const newDnsIsEnabled = action.payload; - state.dnsIsEnabled = newDnsIsEnabled; - }, - updateTrustConfiguration: ( - state, - action: PayloadAction> - ) => { - const newTrustConfig = action.payload; - state.trustConfiguration = newTrustConfig; - }, - updateDomainLevel: ( - state, - action: PayloadAction> - ) => { - const newDomainLevel = action.payload; - state.domainLevel = newDomainLevel; - }, - updateCaIsEnabled: ( - state, - action: PayloadAction> - ) => { - const newCaIsEnabled = action.payload; - state.caIsEnabled = newCaIsEnabled; - }, - updateVaultConfiguration: ( - state, - action: PayloadAction> - ) => { - const newVaultConfig = action.payload; - state.vaultConfiguration = newVaultConfig; + // We need logout reducer, whoami together with Kerberos will always report a user thus being unable to logout + logoutUser: (state) => { + state.loggedInUser = ""; }, }, + extraReducers: (builder) => { + builder + .addMatcher( + authApi.endpoints.userMetadata.matchFulfilled, + (state, action) => { + Object.assign(state, action.payload); + } + ) + .addMatcher(authApi.endpoints.userMetadata.matchRejected, (state) => { + state.loggedInUser = ""; + }); + }, }); -export const { - updateIpaServerConfiguration, - updateLoggedUserInfo, - updateEnvironment, - updateDnsIsEnabled, - updateTrustConfiguration, - updateDomainLevel, - updateCaIsEnabled, - updateVaultConfiguration, -} = globalSlice.actions; +export const { logoutUser } = globalSlice.actions; export default globalSlice.reducer; diff --git a/src/store/store.ts b/src/store/store.ts index cb583823f..4ee8c4155 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -3,7 +3,6 @@ import { setupListeners } from "@reduxjs/toolkit/query"; import globalReducer from "./Global/global-slice"; import { api } from "../services/rpc"; import routesReducer from "./Global/routes-slice"; -import authReducer from "./Global/auth-slice"; import alertsReducer from "./Global/alerts-slice"; import contextualHelpReducer from "./Global/contextual-help-slice"; @@ -13,7 +12,6 @@ export const setupStore = () => { api: api.reducer, global: globalReducer, routes: routesReducer, - auth: authReducer, alerts: alertsReducer, contextualHelp: contextualHelpReducer, }, diff --git a/src/utils/testRouterUtils.tsx b/src/utils/testRouterUtils.tsx new file mode 100644 index 000000000..470ab88c1 --- /dev/null +++ b/src/utils/testRouterUtils.tsx @@ -0,0 +1,34 @@ +import React from "react"; +import { render } from "@testing-library/react"; +import { useSearchParams, MemoryRouter } from "react-router"; + +const SearchParamsProbe = ({ + onParams, +}: { + onParams: (params: URLSearchParams) => void; +}) => { + const [params] = useSearchParams(); + onParams(params); + return null; +}; + +export const renderWithRouter = ( + ui: React.ReactElement, + initialEntry = "/" +) => { + let latestParams = new URLSearchParams(); + const result = render( + + {ui} + { + latestParams = params; + }} + /> + + ); + return { + ...result, + getParams: () => latestParams, + }; +}; diff --git a/src/utils/testUtils.tsx b/src/utils/testUtils.tsx index da0091ea7..07ea3962e 100644 --- a/src/utils/testUtils.tsx +++ b/src/utils/testUtils.tsx @@ -4,8 +4,6 @@ import type { RenderOptions } from "@testing-library/react"; import { Provider } from "react-redux"; import ManagedAlerts from "src/components/ManagedAlerts"; import { setupStore } from "src/store/store"; -import { useSearchParams } from "react-router"; -import { MemoryRouter } from "react-router"; export function renderWithAlerts( ui: React.ReactElement, @@ -26,34 +24,3 @@ export function renderWithAlerts( ...render(ui, { wrapper: Wrapper, ...renderOptions }), }; } - -const SearchParamsProbe = ({ - onParams, -}: { - onParams: (params: URLSearchParams) => void; -}) => { - const [params] = useSearchParams(); - onParams(params); - return null; -}; - -export const renderWithRouter = ( - ui: React.ReactElement, - initialEntry = "/" -) => { - let latestParams = new URLSearchParams(); - const result = render( - - {ui} - { - latestParams = params; - }} - /> - - ); - return { - ...result, - getParams: () => latestParams, - }; -};