diff --git a/forms-flow-web/src/apiManager/endpoints/index.js b/forms-flow-web/src/apiManager/endpoints/index.js index 583cda16e0..f6d707dc48 100644 --- a/forms-flow-web/src/apiManager/endpoints/index.js +++ b/forms-flow-web/src/apiManager/endpoints/index.js @@ -78,6 +78,7 @@ const API = { HANDLE_AUTHORIZATION_FOR_DESIGNER: `${WEB_BASE_URL}/authorizations/resource/`, GET_FILTERS : `${WEB_BASE_URL}/filter`, GET_BPM_TASK_FILTERS : `${BPM_BASE_URL_EXT}/v1/task-filters`, + VALIDATE_TENANT: `${MT_ADMIN_BASE_URL}/${MT_ADMIN_BASE_URL_VERSION}/tenants//validate`, }; export default API; diff --git a/forms-flow-web/src/apiManager/services/tenantServices.js b/forms-flow-web/src/apiManager/services/tenantServices.js index adbacd8dd4..55d5bb651b 100644 --- a/forms-flow-web/src/apiManager/services/tenantServices.js +++ b/forms-flow-web/src/apiManager/services/tenantServices.js @@ -1,9 +1,12 @@ import { tenantDetail } from "../../constants/tenantConstant"; +import { RequestService } from "@formsflow/service"; import { setTenantDetails, setTenantID, } from "../../actions/tenantActions"; import { Keycloak_Tenant_Client } from "../../constants/constants"; +import { replaceUrl } from "../../helper/helper"; +import API from "../endpoints"; @@ -25,3 +28,12 @@ export const setTenantFromId = (tenantKey, ...rest) => { done(null, tenantDetail); }; }; + +export const validateTenant = (tenantId) => { + const validateTenantUrl = replaceUrl( + API.VALIDATE_TENANT, + "", + tenantId + ); + return RequestService.httpGETRequest(validateTenantUrl); +}; \ No newline at end of file diff --git a/forms-flow-web/src/components/BaseRouting.jsx b/forms-flow-web/src/components/BaseRouting.jsx index c5de082e5b..12be48a183 100644 --- a/forms-flow-web/src/components/BaseRouting.jsx +++ b/forms-flow-web/src/components/BaseRouting.jsx @@ -1,10 +1,10 @@ import React from "react"; -import { Route, Switch, Redirect, useLocation } from "react-router-dom"; +import { Route, Switch, useLocation } from "react-router-dom"; import { useSelector } from "react-redux"; import PublicRoute from "./PublicRoute"; import PrivateRoute from "./PrivateRoute"; -import { BASE_ROUTE } from "../constants/constants"; +import { BASE_ROUTE, MULTITENANCY_ENABLED } from "../constants/constants"; import Footer from "../components/Footer"; import { ToastContainer } from "react-toastify"; @@ -15,6 +15,7 @@ import i18n from "../resourceBundles/i18n"; import { setLanguage } from "../actions/languageSetAction"; import { initPubSub } from "../actions/pubSubActions"; import { push } from "connected-react-router"; +import LandingPage from "./MultiTenant"; const BaseRouting = React.memo( ({ store, publish, subscribe, getKcInstance }) => { @@ -49,10 +50,11 @@ const BaseRouting = React.memo( React.useEffect(() => { publish("ES_ROUTE", location); }, [location]); - - return ( - + if (MULTITENANCY_ENABLED && !location.pathname.startsWith("/tenant/")) { + return ; + } + return (
@@ -74,8 +76,7 @@ const BaseRouting = React.memo( /> - - +
{isAuth ?
: null}
diff --git a/forms-flow-web/src/components/MultiTenant/LandingPage.js b/forms-flow-web/src/components/MultiTenant/LandingPage.js new file mode 100644 index 0000000000..7ecf45e2a9 --- /dev/null +++ b/forms-flow-web/src/components/MultiTenant/LandingPage.js @@ -0,0 +1,81 @@ +import React, { useState } from "react"; +import { useHistory } from "react-router-dom"; +import "./landingPage.css"; +import { validateTenant } from "../../apiManager/services/tenantServices"; +import { useTranslation } from "react-i18next"; + +const LandingPage = () => { + const [username, setUsername] = useState(""); + const [error, setError] = useState(null); + const history = useHistory(); + const { t } = useTranslation(); + + const handleSubmit = (event) => { + event.preventDefault(); + validateTenant(username) + .then((res) => { + if (res.data.status === "INVALID") { + setError(t("Tenant not found")); + } else { + setError(null); // Clear the error if validation is successful + history.push(`/tenant/${username}`); + } + }) + .catch((err) => { + console.error("error", err); + }); + }; + + return ( +
+
+ Login Image +
+
+
+ formsflow Logo +

{t("Enter your Tenant Name")}

+
+
+ + setUsername(e.target.value)} + className={`form-control ${error ? "is-invalid" : ""}`} + placeholder="Eg: johndoe" + required + /> + {error &&
{error}
} +
+ +
+ {/*
+
{t("Contact formsflow.ai support")}
+ */} +
+
+
+ ); +}; + +export default LandingPage; diff --git a/forms-flow-web/src/components/MultiTenant/index.js b/forms-flow-web/src/components/MultiTenant/index.js new file mode 100644 index 0000000000..986610f4c7 --- /dev/null +++ b/forms-flow-web/src/components/MultiTenant/index.js @@ -0,0 +1,6 @@ +import React from "react"; +import LandingPage from "./LandingPage"; + +export default React.memo(() => { + return ; +}); diff --git a/forms-flow-web/src/components/MultiTenant/landingPage.css b/forms-flow-web/src/components/MultiTenant/landingPage.css new file mode 100644 index 0000000000..f1a1d0710f --- /dev/null +++ b/forms-flow-web/src/components/MultiTenant/landingPage.css @@ -0,0 +1,117 @@ +.landing { + display: flex; + flex-direction: column; + height: 100vh; + align-items: center; +} + +.imageContainer { + width: 100%; + display: flex; + justify-content: center; + align-items: center; + background-position: left top; + padding: 1rem; +} + +.image { + width: 100%; + max-width: 700px; + object-fit: contain; +} + +.formContainer { + width: 100%; + max-width: 500px; + padding: 5rem; + background-color: #f9f9f9; + box-shadow: 0 0.5rem 1rem 0 rgba(3, 3, 3, 0.16), + 0 0 0.375rem 0 rgba(3, 3, 3, 0.08); + margin: 1rem auto; + /* border-radius: 8px; */ +} + +.innerContainer { + display: flex; + flex-direction: column; + align-items: center; +} + +.logo { + max-width: 150px; +} + +.heading { + margin-bottom: 20px; + font-size: 1.5rem; + text-align: center; +} + +.formTenant { + width: 100%; +} + +.formGroupTenant { + width: 100%; + margin-bottom: 20px; + text-align: left; +} + +.formLabelTenant { + display: block; + margin-bottom: 10px; + font-size: 1rem; + line-height: 1.66666667; +} + +.btnTenant { + display: block; + width: 100%; + margin-bottom: 20px; + padding: 6px 10px; + font-size: 0.875rem; + line-height: 1.3333333; + border-radius: 1px; +} + +.lineTenant { + width: 100%; + height: 1px; + background-color: #ccc; + margin-bottom: 20px; +} + +.supportText { + color: black; + text-align: center; +} + +.supportLink { + text-align: center; +} + +.supportLink a { + color: #007bff; + text-decoration: none; +} + +.supportLink a:hover { + text-decoration: underline; +} + +@media (min-width: 768px) { + .landing { + flex-direction: row; + justify-content: space-between; + } + + .imageContainer { + flex: 1; + max-width: 50%; + } + + .formContainer { + flex: 1; + margin: auto; + } +} diff --git a/forms-flow-web/src/components/PrivateRoute.jsx b/forms-flow-web/src/components/PrivateRoute.jsx index 6a71b83c66..9baf9edb86 100644 --- a/forms-flow-web/src/components/PrivateRoute.jsx +++ b/forms-flow-web/src/components/PrivateRoute.jsx @@ -1,7 +1,7 @@ /* eslint-disable no-unused-vars */ -import React, { useEffect, Suspense, lazy, useMemo } from "react"; +import React, { useEffect, Suspense, lazy, useMemo,useCallback } from "react"; import { Route, Switch, Redirect, useParams } from "react-router-dom"; -import { useDispatch, useSelector } from "react-redux"; +import { useDispatch, useSelector} from "react-redux"; import { BASE_ROUTE, DRAFT_ENABLED, @@ -31,7 +31,10 @@ import { import Loading from "../containers/Loading"; import NotFound from "./NotFound"; -import { setTenantFromId } from "../apiManager/services/tenantServices"; +import { + setTenantFromId, + validateTenant, +} from "../apiManager/services/tenantServices"; // Lazy imports is having issues with micro-front-end build @@ -80,6 +83,7 @@ const PrivateRoute = React.memo((props) => { const tenant = useSelector((state) => state.tenants); const [authError, setAuthError] = React.useState(false); const [kcInstance, setKcInstance] = React.useState(getKcInstance()); + const [tenantValid, setTenantValid] = React.useState(true); // State to track tenant validity const authenticate = (instance, store) => { setKcInstance(instance); @@ -88,71 +92,82 @@ const PrivateRoute = React.memo((props) => { ); dispatch(setUserAuth(instance.isAuthenticated())); store.dispatch(setUserToken(instance.getToken())); - //Set Cammunda/Formio Base URL + // Set Cammunda/Formio Base URL setApiBaseUrlToLocalStorage(); - // get formio roles + // Get formio roles store.dispatch( getFormioRoleIds((err) => { if (err) { console.error(err); - // doLogout(); } else { store.dispatch( setUserDetails( JSON.parse(StorageService.get(StorageService.User.USER_DETAILS)) ) ); - - // onAuthenticatedCallback(); } }) ); }; - useEffect(() => { + const keycloakInitialize = useCallback(() => { let instance = tenantId ? kcServiceInstance(tenantId) : kcServiceInstance(); - if (tenantId && props.store) { - let currentTenant = sessionStorage.getItem("tenantKey"); - if (currentTenant && currentTenant !== tenantId) { - sessionStorage.clear(); - localStorage.clear(); - } - sessionStorage.setItem("tenantKey", tenantId); - dispatch(setTenantFromId(tenantId)); - } if (props.store) { if (kcInstance) { authenticate(kcInstance, props.store); } else { instance.initKeycloak((authenticated) => { - if(!authenticated) - { + if (!authenticated) { setAuthError(true); - } - else{ + } else { authenticate(instance, props.store); publish("FF_AUTH", instance); } }); } } - }, [props.store, tenantId, dispatch]); + }, [props.store, kcInstance, tenantId]); - /** - * Retrieves the user's locale from the Keycloak instance or the tenant data, and dispatches an action to set the language in the application state. - * This effect is triggered whenever the Keycloak instance or the tenant data changes. - */ useEffect(() => { - if(kcInstance){ - const lang = kcInstance?.userData?.locale || - tenant?.tenantData?.details?.locale || - selectedLanguage ; + if (tenantId && MULTITENANCY_ENABLED) { + validateTenant(tenantId) + .then((res) => { + if (res.data.status === "INVALID") { + setTenantValid(false); + } else { + setTenantValid(true); + + if (tenantId && props.store) { + let currentTenant = sessionStorage.getItem("tenantKey"); + if (currentTenant && currentTenant !== tenantId) { + sessionStorage.clear(); + localStorage.clear(); + } + sessionStorage.setItem("tenantKey", tenantId); + dispatch(setTenantFromId(tenantId)); + keycloakInitialize(); + } + } + }) + .catch((err) => { + console.error("Error validating tenant", err); + setTenantValid(false); + }); + } else { + keycloakInitialize(); + } + }, [tenantId, props.store, dispatch]); + + useEffect(() => { + if (kcInstance) { + const lang = + kcInstance?.userData?.locale || + tenant?.tenantData?.details?.locale || + selectedLanguage; dispatch(setLanguage(lang)); } }, [kcInstance, tenant?.tenantData]); - // useMemo prevents unneccessary rerendering caused by the route update. - const DesignerRoute = useMemo( () => ({ component: Component, ...rest }) => @@ -224,6 +239,11 @@ const PrivateRoute = React.memo((props) => { ), [userRoles] ); + + if (!tenantValid) { + return ; + } + return ( <> {authError ? ( @@ -246,14 +266,12 @@ const PrivateRoute = React.memo((props) => { component={Application} /> )} - {ENABLE_PROCESSES_MODULE && ( )} - {ENABLE_DASHBOARDS_MODULE && ( { )} - {userRoles.length && } + {userRoles.length && ( + + )}