diff --git a/dashboard/package.json b/dashboard/package.json index 887e399cc5..4e78c1b249 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -9,6 +9,7 @@ "@patternfly/patternfly": "^4.183.1", "@patternfly/react-core": "^4.198.19", "@patternfly/react-table": "^4.75.2", + "@react-keycloak/web": "^3.4.0", "@testing-library/jest-dom": "^5.16.2", "@testing-library/react": "^12.1.4", "@testing-library/user-event": "^13.5.0", @@ -21,6 +22,7 @@ "gulp": "^4.0.2", "jest": "^27.5.1", "js-cookie": "^3.0.1", + "keycloak-js": "^21.0.1", "less-watch-compiler": "^1.16.3", "patternfly": "^3.9.0", "react": "^17.0.2", diff --git a/dashboard/src/App.js b/dashboard/src/App.js index d55e79b3d6..b4755fade3 100644 --- a/dashboard/src/App.js +++ b/dashboard/src/App.js @@ -16,21 +16,19 @@ import { AuthForm } from "modules/components/AuthComponent/common-components"; import AuthLayout from "modules/containers/AuthLayout"; import ComingSoonPage from "modules/components/EmptyPageComponent/ComingSoon"; import Cookies from "js-cookie"; -import LoginForm from "modules/components/AuthComponent/LoginForm"; import MainLayout from "modules/containers/MainLayout"; import NoMatchingPage from "modules/components/EmptyPageComponent/NoMatchingPage"; import OverviewComponent from "modules/components/OverviewComponent"; import ProfileComponent from "modules/components/ProfileComponent"; -import SignupForm from "modules/components/AuthComponent/SignupForm"; import TableOfContent from "modules/components/TableOfContent"; import TableWithFavorite from "modules/components/TableComponent"; import favicon from "./assets/logo/favicon.ico"; import { fetchEndpoints } from "./actions/endpointAction"; -import { getUserDetails } from "actions/authActions"; import { showToast } from "actions/toastActions"; -import { useDispatch } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; +import { ReactKeycloakProvider } from "@react-keycloak/web"; -const ProtectedRoute = ({ redirectPath = APP_ROUTES.AUTH_LOGIN, children }) => { +const ProtectedRoute = ({ redirectPath = APP_ROUTES.AUTH, children }) => { const loggedIn = Cookies.get("isLoggedIn"); const dispatch = useDispatch(); @@ -47,56 +45,68 @@ const HomeRoute = ({ redirectPath = APP_ROUTES.HOME }) => { const App = () => { const dispatch = useDispatch(); + const { keycloak } = useSelector((state) => state.apiEndpoint); useEffect(() => { const faviconLogo = document.getElementById("favicon"); faviconLogo?.setAttribute("href", favicon); dispatch(fetchEndpoints); - dispatch(getUserDetails()); }, [dispatch]); return (
- - - }> - - }> - } /> - } /> - } /> - - }> - } /> - }> - } - /> - } - /> - } - /> - } - /> - } - /> + {keycloak && ( + + + + }> + + }> + } /> + + }> + } /> + }> + } + /> + } + /> + } + /> + } + /> + } + /> + + } + /> + + } /> - } /> - - } /> - - - + + + + )}
); }; diff --git a/dashboard/src/actions/authActions.js b/dashboard/src/actions/authActions.js index 94138224ae..458bfcddf3 100644 --- a/dashboard/src/actions/authActions.js +++ b/dashboard/src/actions/authActions.js @@ -2,203 +2,70 @@ import * as APP_ROUTES from "utils/routeConstants"; import * as CONSTANTS from "../assets/constants/authConstants"; import * as TYPES from "./types"; -import API from "../utils/axiosInstance"; import Cookies from "js-cookie"; import { SUCCESS } from "assets/constants/overviewConstants"; -import { showToast } from "actions/toastActions"; -import { uid } from "../utils/helper"; +import { showToast, clearToast } from "actions/toastActions"; - -// Create an Authentication Request -export const authenticationRequest = () => async (dispatch, getState) => { - try { - const endpoints = getState().apiEndpoint.endpoints; - const oidcServer = endpoints.openid.server; - const oidcRealm = endpoints.openid.realm; - const oidcClient = endpoints.openid.client; - // URI parameters ref: https://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint - // Refer Step 3 of pbench/docs/user_authentication/third_party_token_management.md - const uri = `${oidcServer}/realms/${oidcRealm}/protocol/openid-connect/auth`; - const queryParams = [ - 'client_id=' + oidcClient, - 'response_type=code', - 'redirect_uri=' + window.location.href.split('?')[0], - 'scope=profile', - 'prompt=login', - 'max_age=120' - ]; - window.location.href = uri + '?' + queryParams.join('&'); - } catch (error) { - const alerts = getState().userAuth.alerts; - dispatch(error?.response - ? toggleLoginBtn(true) - : { type: TYPES.OPENID_ERROR } - ); - const alert = { - title: error?.response ? error.response.data?.message : error?.message, - key: uid(), - }; - dispatch({ - type: TYPES.USER_NOTION_ALERTS, - payload: [...alerts, alert], - }); - dispatch({ type: TYPES.COMPLETED }); +/** + * Wait for the Pbench Server endpoints to be loaded. + * @param {getState} getState object. + * @return {promise} promise object + */ +export function waitForEndpoints(getState) { + const waitStart = Date.now(); + /** + * Settle the wait-for-endpoints promise. + * @param {resolve} resolve object. + * @param {reject} reject object + */ + function check(resolve, reject) { + if (Object.keys(getState().apiEndpoint.endpoints).length !== 0) { + resolve("Endpoints loaded"); + } else if (Date.now() - waitStart > CONSTANTS.MAX_ENDPOINTS_WAIT_MS) { + reject(new Error("Timed out waiting for endpoints request")); + } else { + setTimeout(check, 250, resolve, reject); } -}; - -export const makeLoginRequest = - (details, navigate) => async (dispatch, getState) => { - try { - dispatch({ type: TYPES.LOADING }); - // empty the alerts - dispatch({ - type: TYPES.USER_NOTION_ALERTS, - payload: [], - }); - const endpoints = getState().apiEndpoint.endpoints; - const response = await API.post(endpoints?.api?.login, { - ...details, - }); - if (response.status === 200 && Object.keys(response.data).length > 0) { - const keepUser = getState().userAuth.keepLoggedIn; - const expiryTime = keepUser - ? CONSTANTS.EXPIRY_KEEPUSER_DAYS - : CONSTANTS.EXPIRY_DEFAULT_DAYS; - Cookies.set("isLoggedIn", true, { expires: expiryTime }); - Cookies.set("token", response.data?.auth_token, { - expires: expiryTime, - }); - Cookies.set("username", response.data?.username, { - expires: expiryTime, - }); - const loginDetails = { - isLoggedIn: true, - token: response.data?.auth_token, - username: response.data?.username, - }; - await dispatch({ - type: TYPES.SET_LOGIN_DETAILS, - payload: loginDetails, - }); - - navigate(APP_ROUTES.OVERVIEW); + } + return new Promise((resolve, reject) => check(resolve, reject)); +} - dispatch(showToast(SUCCESS, "Logged in successfully!")); - } - dispatch({ type: TYPES.COMPLETED }); - } catch (error) { - const alerts = getState().userAuth.alerts; - let alert = {}; - if (error?.response) { - alert = { - title: error?.response?.data?.message, - key: uid(), - }; - dispatch(toggleLoginBtn(true)); - } else { - alert = { - title: error?.message, - key: uid(), - }; - dispatch({ type: TYPES.NETWORK_ERROR }); - } - alerts.push(alert); - dispatch({ - type: TYPES.USER_NOTION_ALERTS, - payload: alerts, - }); - dispatch({ type: TYPES.COMPLETED }); - } - }; +// Perform some house keeping when the user logs in +export const authCookies = async (dispatch, getState) => { + await waitForEndpoints(getState); + const keycloak = getState().apiEndpoint.keycloak; + if (keycloak.authenticated) { + // Set the isLoggedIn cookie with an expiry of OIDC refresh token. + // We have to convert the UNIX epoch seconds returned by the refresh token + // expiry to milliseconds before we can use it for creating a Date object. + Cookies.set("isLoggedIn", true, { + expires: new Date(keycloak.refreshTokenParsed.exp * 1000), + }); + dispatch(showToast(SUCCESS, "Logged in successfully!")); + } +}; export const movePage = (toPage, navigate) => async (dispatch) => { - // empty the alerts - dispatch({ - type: TYPES.USER_NOTION_ALERTS, - payload: [], - }); + // clear all the toasts before navigating to another page + dispatch(clearToast()); navigate(toPage); }; -export const setUserLoggedInState = (value) => async (dispatch) => { - dispatch({ - type: TYPES.KEEP_USER_LOGGED_IN, - payload: value, - }); -}; - -export const registerUser = - (details, navigate) => async (dispatch, getState) => { - try { - dispatch({ type: TYPES.LOADING }); - // empty the alerts - dispatch({ - type: TYPES.USER_NOTION_ALERTS, - payload: [], - }); - const endpoints = getState().apiEndpoint.endpoints; - const response = await API.post(endpoints?.api?.register, { - ...details, - }); - if (response.status === 201) { - dispatch(showToast(SUCCESS, "Account created!", "Login to continue")); - navigate(APP_ROUTES.AUTH_LOGIN); - } - dispatch({ type: TYPES.COMPLETED }); - } catch (error) { - const alerts = getState().userAuth.alerts; - let amsg = {}; - document.querySelector(".signup-card").scrollTo(0, 0); - if (error?.response) { - amsg = error?.response?.data?.message; - dispatch(toggleSignupBtn(true)); - } else { - amsg = error?.message; - dispatch({ type: TYPES.NETWORK_ERROR }); - } - const alert = { title: amsg, key: uid() }; - alerts.push(alert); - dispatch({ - type: TYPES.USER_NOTION_ALERTS, - payload: alerts, - }); - dispatch({ type: TYPES.COMPLETED }); - } - }; - -export const toggleSignupBtn = (isDisabled) => async (dispatch) => { - dispatch({ - type: TYPES.SET_SIGNUP_BUTTON, - payload: isDisabled, - }); -}; - -export const toggleLoginBtn = (isDisabled) => async (dispatch) => { - dispatch({ - type: TYPES.SET_LOGIN_BUTTON, - payload: isDisabled, - }); -}; - -export const getUserDetails = () => async (dispatch) => { - const loginDetails = { - isLoggedIn: Cookies.get("isLoggedIn"), - token: Cookies.get("token"), - username: Cookies.get("username"), - }; - dispatch({ - type: TYPES.SET_LOGIN_DETAILS, - payload: loginDetails, - }); -}; -export const logout = () => async (dispatch) => { +/** + * Clear the local cookies and re-direct to the auth page. + * @param {dispatch} dispatch object. + */ +export function clearCachedSession(dispatch) { dispatch({ type: TYPES.LOADING }); - const keys = ["username", "token", "isLoggedIn"]; - for (const key of keys) { - Cookies.remove(key); - } + Cookies.remove("isLoggedIn"); dispatch({ type: TYPES.COMPLETED }); setTimeout(() => { window.location.href = APP_ROUTES.AUTH; }, CONSTANTS.LOGOUT_DELAY_MS); +} + +export const sessionLogout = () => async (dispatch, getState) => { + const keycloak = getState().apiEndpoint.keycloak; + keycloak.logout(); + clearCachedSession(dispatch); }; diff --git a/dashboard/src/actions/endpointAction.js b/dashboard/src/actions/endpointAction.js index c7020d1995..4b107049d7 100644 --- a/dashboard/src/actions/endpointAction.js +++ b/dashboard/src/actions/endpointAction.js @@ -1,4 +1,5 @@ import * as TYPES from "./types"; +import Keycloak from "keycloak-js"; export const fetchEndpoints = async (dispatch) => { try { @@ -12,6 +13,15 @@ export const fetchEndpoints = async (dispatch) => { type: TYPES.SET_ENDPOINTS, payload: data, }); + const keycloak = new Keycloak({ + url: data.openid.server, + realm: data.openid.realm, + clientId: data.openid.client, + }); + dispatch({ + type: TYPES.SET_KEYCLOAK, + payload: keycloak, + }); } catch (error) { dispatch({ type: TYPES.SHOW_TOAST, diff --git a/dashboard/src/actions/overviewActions.js b/dashboard/src/actions/overviewActions.js index 48cd0e0e30..e91bf89335 100644 --- a/dashboard/src/actions/overviewActions.js +++ b/dashboard/src/actions/overviewActions.js @@ -7,6 +7,7 @@ import API from "../utils/axiosInstance"; import { expandUriTemplate } from "../utils/helper"; import { findNoOfDays } from "utils/dateFunctions"; import { showToast } from "./toastActions"; +import { clearCachedSession } from "./authActions"; export const getDatasets = () => async (dispatch, getState) => { const alreadyRendered = getState().overview.loadingDone; @@ -42,8 +43,17 @@ export const getDatasets = () => async (dispatch, getState) => { } } } catch (error) { - dispatch(showToast(DANGER, error?.response?.data?.message ?? ERROR_MSG)); - dispatch({ type: TYPES.NETWORK_ERROR }); + if (!error?.response) { + dispatch(showToast(DANGER, "Not Authenticated")); + dispatch({ type: TYPES.OPENID_ERROR }); + clearCachedSession(dispatch); + } else { + const msg = error.response?.data?.message; + dispatch( + showToast(DANGER, msg ? msg : `Error response: ERROR_MSG`) + ); + dispatch({ type: TYPES.NETWORK_ERROR }); + } } if (alreadyRendered) { dispatch({ type: TYPES.COMPLETED }); diff --git a/dashboard/src/actions/profileActions.js b/dashboard/src/actions/profileActions.js deleted file mode 100644 index 14ae1a6c13..0000000000 --- a/dashboard/src/actions/profileActions.js +++ /dev/null @@ -1,85 +0,0 @@ -import * as TYPES from "./types"; - -import { showFailureToast, showToast } from "./toastActions"; - -import API from "../utils/axiosInstance"; - -export const getProfileDetails = () => async (dispatch, getState) => { - try { - dispatch({ type: TYPES.LOADING }); - - const username = getState().userAuth.loginDetails.username; - const endpoints = getState().apiEndpoint.endpoints; - - const response = await API.get(`${endpoints?.api?.user}/${username}`); - - if (response.status === 200 && Object.keys(response.data).length > 0) { - dispatch({ - type: TYPES.GET_USER_DETAILS, - payload: response?.data, - }); - } else { - dispatch(showFailureToast()); - } - dispatch({ type: TYPES.COMPLETED }); - } catch (error) { - dispatch(showToast("danger", error?.response?.data?.message)); - dispatch({ type: TYPES.NETWORK_ERROR }); - dispatch({ type: TYPES.COMPLETED }); - } -}; - -export const updateUserDetails = - (value, fieldName) => async (dispatch, getState) => { - const userDetails = { ...getState().userProfile.userDetails }; - const updatedUserDetails = { ...getState().userProfile.updatedUserDetails }; - - userDetails[fieldName] = value; - updatedUserDetails[fieldName] = value; - const payload = { - userDetails, - updatedUserDetails, - }; - dispatch({ - type: TYPES.UPDATE_USER_DETAILS, - payload, - }); - }; - -export const sendForUpdate = () => async (dispatch, getState) => { - try { - dispatch({ type: TYPES.LOADING }); - - const username = getState().userAuth.loginDetails.username; - const endpoints = getState().apiEndpoint.endpoints; - - if (username) { - const response = await API.put(`${endpoints?.api?.user}/${username}`, { - ...getState().userProfile.updatedUserDetails, - }); - if (response.status === 200) { - dispatch(showToast("success", "Updated!")); - dispatch({ - type: TYPES.GET_USER_DETAILS, - payload: response?.data, - }); - dispatch({ type: TYPES.RESET_DATA }); - } else { - dispatch(showFailureToast()); - } - dispatch({ type: TYPES.COMPLETED }); - } - } catch (error) { - dispatch(showToast("danger", error?.response?.data?.message)); - dispatch({ type: TYPES.NETWORK_ERROR }); - dispatch({ type: TYPES.COMPLETED }); - } -}; - -export const resetUserDetails = () => async (dispatch, getState) => { - dispatch({ - type: TYPES.SET_USER_DETAILS, - payload: getState().userProfile.userDetails_copy, - }); - dispatch({ type: TYPES.RESET_DATA }); -}; diff --git a/dashboard/src/actions/toastActions.js b/dashboard/src/actions/toastActions.js index 6e22673280..93eb4c4fef 100644 --- a/dashboard/src/actions/toastActions.js +++ b/dashboard/src/actions/toastActions.js @@ -1,7 +1,7 @@ import * as TYPES from "./types"; -import { logout } from "./authActions"; import { uid } from "utils/helper"; +import { clearCachedSession } from "./authActions"; export const showSessionExpired = () => async (dispatch) => { const toast = { @@ -10,7 +10,7 @@ export const showSessionExpired = () => async (dispatch) => { message: "Please login to continue", }; dispatch(showToast(toast.variant, toast.title, toast.message)); - dispatch(logout()); + clearCachedSession(dispatch); }; export const showFailureToast = () => async (dispatch) => { diff --git a/dashboard/src/actions/types.js b/dashboard/src/actions/types.js index 625af5b4fe..ee559da137 100644 --- a/dashboard/src/actions/types.js +++ b/dashboard/src/actions/types.js @@ -1,5 +1,6 @@ /* ENDPOINTS */ export const SET_ENDPOINTS = "SET_ENDPOINTS"; +export const SET_KEYCLOAK = "SET_KEYCLOAK"; /* TOAST */ export const SHOW_TOAST = "SHOW_TOAST"; @@ -14,10 +15,7 @@ export const OPENID_ERROR = "OPENID_ERROR"; export const DASHBOARD_LOADING = "DASHBOARD_LOADING"; /* USER AUTHENTICATION */ -export const KEEP_USER_LOGGED_IN = "KEEP_USER_LOGGED_IN"; export const USER_NOTION_ALERTS = "USER_NOTION_ALERTS"; -export const SET_LOGIN_BUTTON = "SET_LOGIN_BUTTON"; -export const SET_SIGNUP_BUTTON = "SET_SIGNUP_BUTTON"; /* NAVBAR OPEN/CLOSE */ export const NAVBAR_OPEN = "NAVBAR_OPEN"; @@ -27,13 +25,6 @@ export const NAVBAR_CLOSE = "NAVBAR_CLOSE"; export const GET_PUBLIC_DATASETS = "GET_PUBLIC_DATASETS"; export const FAVORITED_DATASETS = "GET_FAVORITE_DATASETS"; export const UPDATE_PUBLIC_DATASETS = "UPDATE_PUBLIC_DATASETS"; -export const SET_LOGIN_DETAILS = "SET_LOGIN_DETAILS"; - -/* USER DETAILS */ -export const GET_USER_DETAILS = "GET_USER_DETAILS"; -export const UPDATE_USER_DETAILS = "UPDATE_USER_DETAILS"; -export const RESET_DATA = "RESET_DATA"; -export const SET_USER_DETAILS = "SET_USER_DETAILS"; /* DASHBOARD OVERVIEW */ export const USER_RUNS = "USER_RUNS"; diff --git a/dashboard/src/assets/constants/authConstants.js b/dashboard/src/assets/constants/authConstants.js index dd06176980..569d6ffac1 100644 --- a/dashboard/src/assets/constants/authConstants.js +++ b/dashboard/src/assets/constants/authConstants.js @@ -1,3 +1,2 @@ export const LOGOUT_DELAY_MS = 2000; -export const EXPIRY_KEEPUSER_DAYS = 7; -export const EXPIRY_DEFAULT_DAYS = 0.5; +export const MAX_ENDPOINTS_WAIT_MS = 10000; diff --git a/dashboard/src/modules/components/AuthComponent/LoginForm.jsx b/dashboard/src/modules/components/AuthComponent/LoginForm.jsx deleted file mode 100644 index 587778a89e..0000000000 --- a/dashboard/src/modules/components/AuthComponent/LoginForm.jsx +++ /dev/null @@ -1,170 +0,0 @@ -import * as APP_ROUTES from "utils/routeConstants"; - -import { - Alert, - AlertGroup, - Button, - Card, - CardBody, - CardFooter, - CardTitle, - Checkbox, - Form, - FormGroup, - TextInput, -} from "@patternfly/react-core"; -import { - Back, - LoginHeader, - NoLoginComponent, - PasswordTextInput, -} from "./common-components"; -import { EyeIcon, EyeSlashIcon } from "@patternfly/react-icons"; -import React, { useCallback, useEffect, useState } from "react"; -import { makeLoginRequest, setUserLoggedInState } from "actions/authActions"; -import { useDispatch, useSelector } from "react-redux"; - -import { useOutletContext } from "react-router-dom"; - -const LoginForm = () => { - const dispatch = useDispatch(); - const navigate = useOutletContext(); - const alerts = useSelector((state) => state.userAuth.alerts); - const [details, setDetails] = useState({ - password: "", - username: "", - }); - const [btnDisabled, setBtnDisabled] = useState(true); - const [showPassword, setShowPassword] = useState(false); - - const { endpoints } = useSelector((state) => state.apiEndpoint); - const isLoading = useSelector((state) => state.loading.isLoading); - - const primaryLoadingProps = {}; - if (isLoading) { - primaryLoadingProps.spinnerAriaValueText = "Loading"; - primaryLoadingProps.spinnerAriaLabelledBy = "primary-loading-button"; - primaryLoadingProps.isLoading = true; - } - const handleUsernameChange = (value) => { - setDetails({ - ...details, - username: value, - }); - }; - const handlePasswordChange = (value) => { - setDetails({ - ...details, - password: value, - }); - }; - const sendLoginDetails = () => { - dispatch(makeLoginRequest(details, navigate)); - }; - const checkOkButton = useCallback(() => { - if ( - details.username?.length > 0 && - details.password?.length > 0 && - Object.keys(endpoints).length > 0 - ) { - setBtnDisabled(false); - } else { - setBtnDisabled(true); - } - }, [details, endpoints]); - - const keepUser = useSelector((state) => state.userAuth.keepLoggedIn); - const checkBoxChangeHander = (value) => { - dispatch(setUserLoggedInState(value)); - }; - const onShowPassword = () => { - setShowPassword(!showPassword); - }; - useEffect(() => { - checkOkButton(); - }, [checkOkButton, details]); - - const handleKeypress = (e) => { - // it triggers by pressing the enter key - if (e.charCode === 13) { - sendLoginDetails(); - } - }; - return ( - - - - - - {alerts.map(({ title, key }) => ( - - ))} - - - -
- - - - -
- - -
-
- - -
- -
- -
- -
-or-
-
- -
-
-
- ); -}; - -export default LoginForm; diff --git a/dashboard/src/modules/components/AuthComponent/SignupForm.jsx b/dashboard/src/modules/components/AuthComponent/SignupForm.jsx deleted file mode 100644 index 4cbf0333d1..0000000000 --- a/dashboard/src/modules/components/AuthComponent/SignupForm.jsx +++ /dev/null @@ -1,223 +0,0 @@ -import * as APP_ROUTES from "utils/routeConstants"; - -import { - Alert, - AlertGroup, - Button, - Card, - CardBody, - CardFooter, - CardTitle, - Form, - FormGroup, - TextInput, -} from "@patternfly/react-core"; -import { - Back, - LoginHeader, - NoLoginComponent, - PasswordConstraints, - PasswordTextInput, -} from "./common-components"; -import { EyeIcon, EyeSlashIcon } from "@patternfly/react-icons"; -import React, { useCallback, useEffect, useState } from "react"; -import { registerUser, toggleSignupBtn } from "actions/authActions"; -import { useDispatch, useSelector } from "react-redux"; -import { validateEmail, validatePassword } from "utils/helper.js"; - -import { signupFormData } from "./signupFormData"; -import { useOutletContext } from "react-router-dom"; - -const SignupForm = () => { - const dispatch = useDispatch(); - const navigate = useOutletContext(); - const { endpoints } = useSelector((state) => state.apiEndpoint); - const { alerts, isSignupBtnDisabled, passwordLength } = useSelector( - (state) => state.userAuth - ); - const [showPassword, setShowPassword] = useState(false); - const [userDetails, setUserDetails] = useState({ - firstName: "", - lastName: "", - userName: "", - email: "", - password: "", - passwordConfirm: "", - }); - const [errors, setErrors] = useState({ - firstName: "", - lastName: "", - userName: "", - email: "", - passwordConstraints: "", - passwordConfirm: "", - }); - const [constraints, setConstraints] = useState({ - passwordLength: "indeterminate", - passwordSpecialChars: "indeterminate", - passwordContainsNumber: "indeterminate", - passwordBlockLetter: "indeterminate", - }); - - const validateForm = useCallback(() => { - if ( - userDetails.firstName?.trim() === "" || - userDetails.lastName?.trim() === "" || - userDetails.userName?.trim() === "" || - userDetails.email?.trim() === "" || - userDetails.password?.trim() === "" || - userDetails.passwordConfirm?.trim() === "" - ) { - return false; - } - // check if no errors. - for (const dep of Object.entries(errors)) { - if (dep[1].length > 0) { - return false; - } - } - // check if all constraints are met. - for (const ct of Object.entries(constraints)) { - if (ct[1] !== "success") { - return false; - } - } - // if we reach here, it means - // we have covered all of the edge cases. - return true; - }, [constraints, errors, userDetails]); - - const onShowPassword = () => { - setShowPassword(!showPassword); - }; - - useEffect(() => { - if (validateForm() && Object.keys(endpoints).length > 0) { - dispatch(toggleSignupBtn(false)); - } else { - dispatch(toggleSignupBtn(true)); - } - }, [validateForm, userDetails, dispatch, endpoints]); - - const checkPasswordError = (password, cnfPassword) => { - if (cnfPassword.length > 1 && password !== cnfPassword) { - setErrors({ - ...errors, - passwordConfirm: "The above passwords do not match!", - }); - } else { - setErrors({ ...errors, passwordConfirm: "" }); - } - }; - const changeHandler = (value, fieldName) => { - setUserDetails({ - ...userDetails, - [fieldName]: value, - }); - if (fieldName === "email") { - const isEmailValid = validateEmail(value); - setErrors({ - ...errors, - ...isEmailValid, - }); - } else if (fieldName === "password") { - const validPassword = validatePassword(value, passwordLength); - setConstraints({ - ...constraints, - ...validPassword, - }); - // edge case where user deliberately - // edits the password field, even when - // confirm password is not empty. - checkPasswordError(value, userDetails.confirmPassword); - } else if (fieldName === "passwordConfirm") { - checkPasswordError(userDetails.password, value); - } - }; - const sendForRegistration = () => { - const details = { - email: userDetails.email, - password: userDetails.password, - first_name: userDetails.firstName, - last_name: userDetails.lastName, - username: userDetails.userName, - }; - dispatch(registerUser({ ...details }, navigate)); - }; - return ( - - - - - - {alerts.map(({ title, key }) => ( - - ))} - - - -
- {signupFormData.map((formItem) => { - return ( - -
- {["password", "passwordConfirm"].includes(formItem.name) ? ( - - changeHandler(val, formItem.name) - } - /> - ) : ( - changeHandler(val, formItem.name)} - /> - )} - {["password", "passwordConfirm"].includes(formItem.name) && ( - - )} -
-

{errors[formItem.name]}

-
- ); - })} -
-
- - -
- -
- -
-
- ); -}; - -export default SignupForm; diff --git a/dashboard/src/modules/components/AuthComponent/common-components.jsx b/dashboard/src/modules/components/AuthComponent/common-components.jsx index 3520a00935..a7698d50ac 100644 --- a/dashboard/src/modules/components/AuthComponent/common-components.jsx +++ b/dashboard/src/modules/components/AuthComponent/common-components.jsx @@ -5,26 +5,21 @@ import * as APP_ROUTES from "utils/routeConstants"; import { Button, Card, - CardBody, CardFooter, CardTitle, Flex, FlexItem, - HelperText, - HelperTextItem, - TextInput, Title, } from "@patternfly/react-core"; -import { CheckIcon, CloseIcon, TimesIcon } from "@patternfly/react-icons"; -import { useDispatch, useSelector } from "react-redux"; +import { useDispatch } from "react-redux"; import { useNavigate, useOutletContext } from "react-router-dom"; -import { authenticationRequest } from "actions/authActions"; +import { authCookies } from "actions/authActions"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import PBenchLogo from "assets/logo/pbench_logo.svg"; -import React from "react"; +import React, { useEffect } from "react"; import { faAngleLeft } from "@fortawesome/free-solid-svg-icons"; import { movePage } from "actions/authActions"; -import { passwordConstraintsText } from "./signupFormData"; +import { useKeycloak } from "@react-keycloak/web"; export const LoginHeader = (props) => { return {props?.title}; @@ -80,46 +75,27 @@ export const LoginRightComponent = () => { export const AuthForm = () => { const navigate = useOutletContext(); + const { keycloak } = useKeycloak(); const dispatch = useDispatch(); const navigatePage = (toPage) => { dispatch(movePage(toPage, navigate)); }; + useEffect(() => { + dispatch(authCookies); + }); return ( - -
- -
-
-
- Need an account? - -
-
- -
-
-
-
Or log in with...
@@ -130,38 +106,6 @@ export const AuthForm = () => { ); }; -export const PasswordConstraints = (props) => { - const { checkConstraints } = props; - const iconList = { - indeterminate: , - success: , - error: , - }; - const passwordLength = useSelector((state) => state.userAuth.passwordLength); - return ( - <> -

Passwords must contain at least:

-
- {passwordConstraintsText.map((constraint, index) => { - const variant = checkConstraints[constraint.name]; - return ( - - - {constraint.name === "passwordLength" && passwordLength}{" "} - {constraint.label} - - - ); - })} -
- - ); -}; - export const NoLoginComponent = () => { const navigate = useNavigate(); const dispatch = useDispatch(); @@ -181,19 +125,3 @@ export const NoLoginComponent = () => {
); }; - -export const PasswordTextInput = (props) => { - const { isRequired, id, name, onChangeMethod, value, isShowPassword } = props; - return ( - onChangeMethod(val, name)} - onKeyPress={props.onKeyPress} - /> - ); -}; diff --git a/dashboard/src/modules/components/AuthComponent/signupFormData.js b/dashboard/src/modules/components/AuthComponent/signupFormData.js deleted file mode 100644 index 665dc701f7..0000000000 --- a/dashboard/src/modules/components/AuthComponent/signupFormData.js +++ /dev/null @@ -1,69 +0,0 @@ -export const signupFormData = [ - { - key: 1, - label: "First name", - id: "horizontal-form-first-name", - name: "firstName", - isRequired: true, - type: "text", - }, - { - key: 2, - label: "Last name", - id: "horizontal-form-last-name", - name: "lastName", - isRequired: true, - type: "text", - }, - { - key: 3, - label: "User name", - id: "horizontal-form-user-name", - name: "userName", - isRequired: true, - type: "text", - }, - { - key: 4, - label: "Email address", - id: "horizontal-form-email-address", - name: "email", - isRequired: true, - type: "text", - }, - { - key: 5, - label: "Password", - id: "horizontal-form-password", - name: "password", - isRequired: true, - type: "password", - }, - { - key: 6, - label: "Confirm password", - id: "horizontal-form-confirm-password", - name: "passwordConfirm", - isRequired: true, - type: "password", - }, -]; - -export const passwordConstraintsText = [ - { - label: "characters", - name: "passwordLength", - }, - { - label: "1 special character", - name: "passwordSpecialChars", - }, - { - label: "1 number", - name: "passwordContainsNumber", - }, - { - label: "1 uppercase letter", - name: "passwordBlockLetter", - }, -]; diff --git a/dashboard/src/modules/components/HeaderComponent/index.jsx b/dashboard/src/modules/components/HeaderComponent/index.jsx index ea34080021..841477f94d 100644 --- a/dashboard/src/modules/components/HeaderComponent/index.jsx +++ b/dashboard/src/modules/components/HeaderComponent/index.jsx @@ -31,23 +31,26 @@ import React, { useState } from "react"; import { useDispatch, useSelector } from "react-redux"; import { useLocation, useNavigate } from "react-router-dom"; -import Cookies from "js-cookie"; -import { logout } from "actions/authActions"; +import { sessionLogout } from "actions/authActions"; import pbenchLogo from "assets/logo/pbench_logo.svg"; +import { useKeycloak } from "@react-keycloak/web"; +import { movePage } from "actions/authActions"; const HeaderToolbar = () => { const dispatch = useDispatch(); - const loginDetails = useSelector((state) => state.userAuth.loginDetails); + const { keycloak } = useKeycloak(); const [isDropdownOpen, setIsDropdownOpen] = useState(false); const navigate = useNavigate(); const { pathname } = useLocation(); - const isLoggedIn = Cookies.get("isLoggedIn"); + const navigatePage = (toPage) => { + dispatch(movePage(toPage, navigate)); + }; const onDropdownSelect = (event) => { const type = event.target.name; const menuOptions = { profile: () => navigate(APP_ROUTES.USER_PROFILE), - logout: () => dispatch(logout()), + logout: () => dispatch(sessionLogout()), }; const action = menuOptions[type]; if (action) { @@ -91,14 +94,14 @@ const HeaderToolbar = () => { - {isLoggedIn ? ( + {keycloak.authenticated ? ( - {loginDetails?.username} + {keycloak.tokenParsed.preferred_username} } dropdownItems={userDropdownItems} @@ -108,7 +111,7 @@ const HeaderToolbar = () => { aria-label="Login" className="header-login-button" variant={ButtonVariant.plain} - onClick={() => navigate(APP_ROUTES.AUTH_LOGIN)} + onClick={() => navigatePage(APP_ROUTES.AUTH)} > Login diff --git a/dashboard/src/modules/components/OverviewComponent/index.jsx b/dashboard/src/modules/components/OverviewComponent/index.jsx index 079e552c3c..613a4b3153 100644 --- a/dashboard/src/modules/components/OverviewComponent/index.jsx +++ b/dashboard/src/modules/components/OverviewComponent/index.jsx @@ -28,7 +28,6 @@ import { getDatasets } from "actions/overviewActions"; const OverviewComponent = () => { const dispatch = useDispatch(); const { endpoints } = useSelector((state) => state.apiEndpoint); - const { loginDetails } = useSelector((state) => state.userAuth); const { expiringRuns, savedRuns, newRuns, loadingDone } = useSelector( (state) => state.overview ); @@ -39,7 +38,7 @@ const OverviewComponent = () => { if (Object.keys(endpoints).length > 0) { dispatch(getDatasets()); } - }, [dispatch, endpoints, loginDetails]); + }, [dispatch, endpoints]); const onToggle = (id) => { if (expanded.has(id)) { diff --git a/dashboard/src/modules/components/ProfileComponent/index.jsx b/dashboard/src/modules/components/ProfileComponent/index.jsx index 6f1fc6bf2e..076c97b669 100644 --- a/dashboard/src/modules/components/ProfileComponent/index.jsx +++ b/dashboard/src/modules/components/ProfileComponent/index.jsx @@ -1,7 +1,5 @@ -import React, { useEffect, useState } from "react"; -import { useDispatch, useSelector } from "react-redux"; +import React from "react"; import { - Button, Card, CardBody, Grid, @@ -10,47 +8,16 @@ import { LevelItem, Text, TextContent, - TextInput, TextVariants, isValidDate, } from "@patternfly/react-core"; -import { KeyIcon, PencilAltIcon, UserAltIcon } from "@patternfly/react-icons"; +import { KeyIcon, UserAltIcon } from "@patternfly/react-icons"; import "./index.less"; import avatar from "assets/images/avatar.jpg"; -import { - getProfileDetails, - resetUserDetails, - sendForUpdate, - updateUserDetails, -} from "actions/profileActions"; +import { useKeycloak } from "@react-keycloak/web"; const ProfileComponent = () => { - const [editView, setEditView] = useState(false); - const dispatch = useDispatch(); - - const user = useSelector((state) => state.userProfile.userDetails); - const loginDetails = useSelector((state) => state.userAuth.loginDetails); - const { endpoints } = useSelector((state) => state.apiEndpoint); - - const isUserDetailsUpdated = useSelector( - (state) => state.userProfile.isUserDetailsUpdated - ); - - const edit = () => { - if (editView) { - dispatch(resetUserDetails()); - } - setEditView(!editView); - }; - const saveEdit = () => { - dispatch(sendForUpdate()); - setEditView(false); - }; - useEffect(() => { - if (loginDetails?.username && Object.keys(endpoints).length > 0) { - dispatch(getProfileDetails()); - } - }, [dispatch, loginDetails?.username, endpoints]); + const { keycloak } = useKeycloak(); const formatDate = (date) => { const registerDate = new Date(date); @@ -58,9 +25,6 @@ const ProfileComponent = () => { ? registerDate.toLocaleDateString() : "----"; }; - const handleInputChange = (value, name) => { - dispatch(updateUserDetails(value, name)); - }; return (
@@ -78,14 +42,6 @@ const ProfileComponent = () => {
-
Profile Picture
@@ -95,110 +51,56 @@ const ProfileComponent = () => {
First Name
- {editView ? ( - - handleInputChange(val, "first_name") - } - /> - ) : ( + { - {user?.first_name} + {keycloak.tokenParsed?.given_name + ? keycloak.tokenParsed.given_name + : ""} - )} + }
Last Name
- {editView ? ( - - handleInputChange(val, "last_name") - } - /> - ) : ( + { - {user?.last_name} + {keycloak.tokenParsed?.family_name + ? keycloak.tokenParsed.family_name + : ""} - )} + }
User Name
- {editView ? ( - - ) : ( + { - {user?.username} + {keycloak.tokenParsed?.preferred_username + ? keycloak.tokenParsed.preferred_username + : ""} - )} + }
Email
- {editView ? ( - handleInputChange(val, "email")} - /> - ) : ( + { - {user?.email} + + {keycloak.tokenParsed?.email + ? keycloak.tokenParsed.email + : ""} + - )} + }
- {editView ? ( - - - {" "} - - - - ) : ( - <> - )} + {<>}
@@ -213,9 +115,10 @@ const ProfileComponent = () => { + {/* TODO: How to handle account creation date */} Account creation Date - {formatDate(user?.registered_on)} + {formatDate("MM/DD/YYYY")} diff --git a/dashboard/src/modules/components/SidebarComponent/index.jsx b/dashboard/src/modules/components/SidebarComponent/index.jsx index 7f1a86e904..cebeabe86e 100644 --- a/dashboard/src/modules/components/SidebarComponent/index.jsx +++ b/dashboard/src/modules/components/SidebarComponent/index.jsx @@ -9,8 +9,7 @@ import React, { useEffect } from "react"; import { menuOptions, menuOptionsNonLoggedIn } from "./sideMenuOptions"; import { useDispatch, useSelector } from "react-redux"; import { useLocation, useNavigate } from "react-router-dom"; - -import Cookies from "js-cookie"; +import { useKeycloak } from "@react-keycloak/web"; import { setActiveItem } from "actions/sideBarActions"; const MenuItem = ({ data, activeItem }) => { @@ -34,7 +33,7 @@ const Menu = () => { const { pathname } = useLocation(); const activeItem = useSelector((state) => state.sidebar.activeMenuItem); - const isLoggedIn = Cookies.get("isLoggedIn"); + const { keycloak } = useKeycloak(); const onSelect = (result) => { dispatch(setActiveItem(result.itemId)); }; @@ -49,7 +48,7 @@ const Menu = () => { return (