diff --git a/apps/OpenSign/src/App.js b/apps/OpenSign/src/App.js index 40bebb3de..eb6f3dbda 100644 --- a/apps/OpenSign/src/App.js +++ b/apps/OpenSign/src/App.js @@ -16,6 +16,7 @@ import PlaceHolderSign from "./pages/PlaceHolderSign"; import PdfRequestFiles from "./pages/PdfRequestFiles"; import LazyPage from "./primitives/LazyPage"; import { isEnableSubscription } from "./constant/const"; +import SSOVerify from "./pages/SSOVerify"; const DebugPdf = lazy(() => import("./pages/DebugPdf")); const ForgetPassword = lazy(() => import("./pages/ForgetPassword")); const GuestLogin = lazy(() => import("./pages/GuestLogin")); @@ -181,6 +182,7 @@ function App() { element={} /> + } /> } /> diff --git a/apps/OpenSign/src/components/Header.js b/apps/OpenSign/src/components/Header.js index 2b1a477cb..90563aac5 100644 --- a/apps/OpenSign/src/components/Header.js +++ b/apps/OpenSign/src/components/Header.js @@ -44,7 +44,9 @@ const Header = ({ showSidebar }) => { const closeDropdown = () => { setIsOpen(false); - Parse.User.logOut(); + if (Parse?.User?.current()) { + Parse.User.logOut(); + } let appdata = localStorage.getItem("userSettings"); let applogo = localStorage.getItem("appLogo"); let appName = localStorage.getItem("appName"); diff --git a/apps/OpenSign/src/components/sidebar/Menu.js b/apps/OpenSign/src/components/sidebar/Menu.js index 584b6f410..0e42c542f 100644 --- a/apps/OpenSign/src/components/sidebar/Menu.js +++ b/apps/OpenSign/src/components/sidebar/Menu.js @@ -10,7 +10,11 @@ const Menu = ({ item, isOpen, closeSidebar }) => { ? `/${item.pageType}/${item.objectId}` : `/${item.objectId}` } - className="mx-auto flex items-center hover:bg-[#eef1f5] p-3 lg:p-4 cursor-pointer focus:text-[#0056b3] focus:bg-[#eef1f5]" + className={({ isActive }) => + `${ + isActive ? "text-[#0056b3] bg-[#eef1f5]" : "" + } mx-auto flex items-center hover:bg-[#eef1f5] p-3 lg:p-4 cursor-pointer` + } onClick={closeSidebar} tabIndex={isOpen ? 0 : -1} role="menuitem" diff --git a/apps/OpenSign/src/constant/Utils.js b/apps/OpenSign/src/constant/Utils.js index 147d360d2..3a24c4398 100644 --- a/apps/OpenSign/src/constant/Utils.js +++ b/apps/OpenSign/src/constant/Utils.js @@ -12,8 +12,12 @@ export const isMobile = window.innerWidth < 767; export const textInputWidget = "text input"; export const textWidget = "text"; export const radioButtonWidget = "radio button"; -export const openInNewTab = (url) => { - window.open(url, "_blank", "noopener,noreferrer"); +export const openInNewTab = (url, target) => { + if (target) { + window.open(url, target, "noopener,noreferrer"); + } else { + window.open(url, "_blank", "noopener,noreferrer"); + } }; export async function fetchSubscription( diff --git a/apps/OpenSign/src/pages/Login.js b/apps/OpenSign/src/pages/Login.js index 24ef4f7b8..c49b1e77e 100644 --- a/apps/OpenSign/src/pages/Login.js +++ b/apps/OpenSign/src/pages/Login.js @@ -19,7 +19,7 @@ import Alert from "../primitives/Alert"; import { appInfo } from "../constant/appinfo"; import { fetchAppInfo } from "../redux/reducers/infoReducer"; import { showTenant } from "../redux/reducers/ShowTenant"; -import { fetchSubscription, getAppLogo } from "../constant/Utils"; +import { fetchSubscription, getAppLogo, openInNewTab } from "../constant/Utils"; function Login() { const navigate = useNavigate(); const location = useLocation(); @@ -45,6 +45,7 @@ function Login() { }); const [isModal, setIsModal] = useState(false); const [image, setImage] = useState(); + const [isLoginSSO, setIsLoginSSO] = useState(false); useEffect(() => { if (localStorage.getItem("accesstoken")) { @@ -1016,8 +1017,9 @@ function Login() { const handleCloseModal = () => { setIsModal(false); - Parse.User.logOut(); - + if (Parse?.User?.current()) { + Parse.User.logOut(); + } let appdata = localStorage.getItem("userSettings"); let applogo = localStorage.getItem("appLogo"); let appName = localStorage.getItem("appName"); @@ -1040,6 +1042,24 @@ function Login() { localStorage.setItem("baseUrl", baseUrl); localStorage.setItem("parseAppId", appid); }; + + // `handleSignInWithSSO` is trigger when user click sign in with sso and open sso authorize endpoint + const handleSignInWithSSO = () => { + if (state?.email) { + setIsLoginSSO(true); + const encodedEmail = encodeURIComponent(state.email); + const clientUrl = window.location.origin; + const domain = state.email.split("@")?.pop(); + const ssoApiUrl = + process.env.SSO_API_URL || "https://sso.opensignlabs.com/api"; + openInNewTab( + `${ssoApiUrl}/oauth/authorize?response_type=code&provider=saml&tenant=${domain}&product=OpenSign&redirect_uri=${clientUrl}/sso&state=${encodedEmail}`, + "_self" + ); + } else { + alert("Please provide email."); + } + }; return (
@@ -1087,136 +1107,131 @@ function Login() { </div> <div className="grid grid-cols-1 md:grid-cols-2 gap-x-2"> <div> - <div> - <form onSubmit={handleSubmit} aria-label="Login Form"> - <h1 className="text-[30px] mt-6">Welcome Back!</h1> - <fieldset> - <legend className="text-[12px] text-[#878787]"> - Login to your account - </legend> - <div className="px-6 py-4 outline outline-1 outline-slate-300/50 my-2 rounded shadow-md"> - <label className="block text-xs" htmlFor="email"> - Username - </label> - <input - id="email" - type="text" - className="px-3 py-2 w-full border-[1px] border-gray-300 rounded text-xs" - name="email" - value={state.email} - onChange={handleChange} - required - /> - <hr className="my-2 border-none" /> - <label className="block text-xs" htmlFor="password"> - Password - </label> - <div className="relative"> - <input - id="password" - type={state.passwordVisible ? "text" : "password"} - className="px-3 py-2 w-full border-[1px] border-gray-300 rounded text-xs" - name="password" - value={state.password} - onChange={handleChange} - required - /> - <span - className={`absolute top-[50%] right-[10px] -translate-y-[50%] cursor-pointer ${ - state.passwordVisible - ? "text-[#007bff]" - : "text-black" - }`} - onClick={togglePasswordVisibility} - > - {state.passwordVisible ? ( - <i className="fa fa-eye-slash text-xs pb-1" /> // Close eye icon - ) : ( - <i className="fa fa-eye text-xs pb-1 " /> // Open eye icon - )} - </span> - </div> - <div className="relative mt-1"> - <NavLink - to="/forgetpassword" - className="text-[13px] text-[#002864] hover:underline underline-offset-1 focus:outline-none cursor-pointer ml-1" - > - Forgot Password? - </NavLink> - </div> + <form onSubmit={handleSubmit} aria-label="Login Form"> + <h1 className="text-[30px] mt-6">Welcome Back!</h1> + <fieldset> + <legend className="text-[12px] text-[#878787]"> + Login to your account + </legend> + <div className="px-6 py-4 outline outline-1 outline-slate-300/50 my-2 rounded shadow-md"> + <label className="block text-xs" htmlFor="email"> + Email + </label> + <input + id="email" + type="text" + className="px-3 py-2 w-full border-[1px] border-gray-300 rounded text-xs" + name="email" + value={state.email} + onChange={handleChange} + required + /> + <hr className="my-2 border-none" /> + {isLoginSSO && ( + <> + <label className="block text-xs" htmlFor="password"> + Password + </label> + <div className="relative"> + <input + id="password" + type={ + state.passwordVisible ? "text" : "password" + } + className="px-3 py-2 w-full border-[1px] border-gray-300 rounded text-xs" + name="password" + value={state.password} + onChange={handleChange} + required + /> + <span + className={`absolute top-[50%] right-[10px] -translate-y-[50%] cursor-pointer ${ + state.passwordVisible + ? "text-[#007bff]" + : "text-black" + }`} + onClick={togglePasswordVisibility} + > + {state.passwordVisible ? ( + <i className="fa fa-eye-slash text-xs pb-1" /> // Close eye icon + ) : ( + <i className="fa fa-eye text-xs pb-1 " /> // Open eye icon + )} + </span> + </div> + </> + )} + <div className="relative mt-1"> + <NavLink + to="/forgetpassword" + className="text-[13px] text-[#002864] hover:underline underline-offset-1 focus:outline-none cursor-pointer ml-1" + > + Forgot Password? + </NavLink> </div> - </fieldset> - <div className="flex flex-col md:flex-row justify-between items-stretch gap-8 text-center text-xs font-bold mt-2"> - <button - type="submit" - className="rounded-sm bg-[#3ac9d6] text-white w-full py-3 shadow outline-none uppercase focus:ring-2 focus:ring-blue-600" - disabled={state.loading} - > - {state.loading ? "Loading..." : "Login"} - </button> - <NavLink - className="rounded-sm cursor-pointer bg-white border-[1px] border-[#15b4e9] text-[#15b4e9] w-full py-3 shadow uppercase" - to={ - location.search - ? "/signup" + location.search - : "/signup" - } - style={width < 768 ? { textAlign: "center" } : {}} - > - Create Account - </NavLink> </div> - </form> - <br /> - {(appInfo.fbAppId || appInfo.googleClietId) && ( - <div className="text-sm flex justify-center items-center"> - <hr className="border-[1px] border-gray-300 w-full" /> - <span className="px-2 text-gray-500 cursor-default"> - OR - </span> - <hr className="border-[1px] border-gray-300 w-full" /> - </div> - )} - <br /> - <div - style={{ - textAlign: "center", - display: "flex", - alignItems: "center", - justifyContent: "center" - }} - > - {/* {appInfo.fbAppId && appInfo.fbAppId !== "" ? ( + </fieldset> + <div className="flex flex-col md:flex-row justify-between items-stretch gap-8 text-center text-xs font-bold mt-2"> + <button + type="submit" + className="rounded-sm bg-[#3ac9d6] text-white w-full py-3 shadow outline-none uppercase focus:ring-2 focus:ring-blue-600" + disabled={state.loading} + > + {state.loading ? "Loading..." : "Login"} + </button> + <NavLink + className="rounded-sm cursor-pointer bg-white border-[1px] border-[#15b4e9] text-[#15b4e9] w-full py-3 shadow uppercase" + to={ + location.search + ? "/signup" + location.search + : "/signup" + } + style={width < 768 ? { textAlign: "center" } : {}} + > + Create Account + </NavLink> + </div> + </form> + <br /> + {appInfo.googleClietId && ( + <div className="text-sm flex justify-center items-center"> + <hr className="border-[1px] border-gray-300 w-full" /> + <span className="px-2 text-gray-500 cursor-default"> + OR + </span> + <hr className="border-[1px] border-gray-300 w-full" /> + </div> + )} + <br /> + <div className="flex flex-col justify-center items-center gap-y-3"> + {/* {appInfo?.fbAppId && ( <LoginFacebook FBCred={appInfo.fbAppId} thirdpartyLoginfn={thirdpartyLoginfn} thirdpartyLoader={state.thirdpartyLoader} setThirdpartyLoader={setThirdpartyLoader} /> - ) : null} */} - </div> - <div style={{ margin: "10px 0" }}></div> - <div - style={{ - textAlign: "center", - display: "flex", - alignItems: "center", - justifyContent: "center" - }} - > - {appInfo.googleClietId && appInfo.googleClietId !== "" ? ( - <GoogleSignInBtn - GoogleCred={appInfo.googleClietId} - thirdpartyLoginfn={thirdpartyLoginfn} - thirdpartyLoader={state.thirdpartyLoader} - setThirdpartyLoader={setThirdpartyLoader} - /> - ) : null} - </div> + )} */} + {appInfo?.googleClietId && ( + <GoogleSignInBtn + GoogleCred={appInfo.googleClietId} + thirdpartyLoginfn={thirdpartyLoginfn} + thirdpartyLoader={state.thirdpartyLoader} + setThirdpartyLoader={setThirdpartyLoader} + /> + )} + {isEnableSubscription && ( + <div + className="cursor-pointer border-[1px] border-gray-300 rounded px-[40px] py-2 font-semibold text-sm hover:border-[#d2e3fc] hover:bg-[#ecf3feb7]" + onClick={() => handleSignInWithSSO()} + > + Sign in with SSO + </div> + )} </div> </div> {width >= 768 && ( - <div className="self-center"> + <div className="place-self-center"> <div className="mx-auto md:w-[300px] lg:w-[400px] xl:w-[500px]"> <img src={login_img} diff --git a/apps/OpenSign/src/pages/SSOVerify.js b/apps/OpenSign/src/pages/SSOVerify.js new file mode 100644 index 000000000..85dbb70bd --- /dev/null +++ b/apps/OpenSign/src/pages/SSOVerify.js @@ -0,0 +1,413 @@ +import React, { useEffect, useState } from "react"; +import { useLocation, useNavigate } from "react-router-dom"; +import Parse from "parse"; +import axios from "axios"; +import { appInfo } from "../constant/appinfo"; +import { + isEnableSubscription, + modalCancelBtnColor, + modalSubmitBtnColor +} from "../constant/const"; +import { fetchSubscription } from "../constant/Utils"; +import { useDispatch } from "react-redux"; +import { showTenant } from "../redux/reducers/ShowTenant"; +import ModalUi from "../primitives/ModalUi"; +import loader from "../assets/images/loader2.gif"; + +const SSOVerify = () => { + const location = useLocation(); + const navigate = useNavigate(); + const dispatch = useDispatch(); + const [isModal, setIsModal] = useState(false); + const [message, setMessage] = useState("Verifying SSO..."); + const [isLoader, setIsLoader] = useState(false); + const [userDetails, setUserDetails] = useState({ + name: "", + email: "", + phone: "", + Destination: "", + Company: "" + }); + + useEffect(() => { + linkUserWithSSO(); + // eslint-disable-next-line + }, []); + + // `linkUserWithSSO` is used to sign in or sign up a user using an SSO code and check if the user is present in the extended class + const linkUserWithSSO = async () => { + const param = new URLSearchParams(location.search); + const code = param?.get("code"); + const state = param?.get("state"); + try { + // The `ssosign` cloud function is used to sign in or sign up a user + const ssosign = await Parse.Cloud.run("ssosign", { + code: code, + email: state + }); + localStorage.setItem("accesstoken", ssosign.sessiontoken); + // `checkExtUser` checks if the user is present in the extended class `contracts_Users` and if not, initiates the new user flow + await checkExtUser(ssosign); + } catch (err) { + setMessage("Error: " + err.message); + console.log("err", err.message); + } + }; + const checkExtUser = async (ssosign) => { + const params = { email: ssosign?.email }; + try { + // `isextenduser` checks if the current user is present in the extended class + const extRes = await Parse.Cloud.run("isextenduser", params); + if (extRes?.isUserExist) { + // `getUserDetails` retrieves the current user's details from the extended class + const extRes = await Parse.Cloud.run("getUserDetails", params); + if (ssosign && ssosign.sessiontoken) { + const LocalUserDetails = { + name: extRes.Name, + email: extRes.email, + phone: extRes?.get("Phone") || "", + company: extRes.get("Company") + }; + localStorage.setItem("userDetails", JSON.stringify(LocalUserDetails)); + thirdpartyLoginfn(ssosign.sessiontoken); + } + } else { + setIsModal(true); + setUserDetails((prev) => ({ + ...prev, + name: ssosign.name, + email: ssosign.email, + phone: ssosign?.phone || "" + })); + } + } catch (err) { + console.log("Err in isextenduser or getuserdetails cloud function", err); + setMessage("Error: " + err.message); + } + }; + // `handleSubmitbtn` is used to create a user in the extended class + const handleSubmitbtn = async (e) => { + e.preventDefault(); + setIsLoader(true); + let phone; + if (userDetails?.phone) { + phone = validateInput(userDetails?.phone); + } else { + phone = true; + } + if (userDetails.Destination && userDetails.Company && phone) { + const payload = { sessionToken: localStorage.getItem("accesstoken") }; + if (payload && payload.sessionToken) { + const params = { + userDetails: { + name: userDetails.name, + email: userDetails.email, + phone: userDetails?.phone || "", + role: "contracts_User", + company: userDetails.Company, + jobTitle: userDetails.Destination + } + }; + try { + const userSignUp = await Parse.Cloud.run("usersignup", params); + if (userSignUp && userSignUp.sessionToken) { + const LocalUserDetails = params.userDetails; + localStorage.setItem( + "userDetails", + JSON.stringify(LocalUserDetails) + ); + await thirdpartyLoginfn(userSignUp.sessionToken); + setIsLoader(false); + } else { + alert(userSignUp.message); + setIsLoader(false); + } + } catch (err) { + console.log("error in usersignup", err); + localStorage.removeItem("accesstoken"); + alert("Something went wrong."); + setIsLoader(false); + } + } else { + localStorage.removeItem("accesstoken"); + alert("Internal server error."); + setIsLoader(false); + } + } else { + alert("Please fill required details correctly."); + setIsLoader(false); + } + }; + // `thirdpartyLoginfn` is used to save necessary parameters locally for the logged-in user + const thirdpartyLoginfn = async (sessionToken) => { + const baseUrl = localStorage.getItem("baseUrl"); + const parseAppId = localStorage.getItem("parseAppId"); + try { + const validUser = await Parse.User.become(sessionToken); + if (validUser) { + localStorage.setItem("accesstoken", sessionToken); + const _user = JSON.parse(JSON.stringify(validUser)); + localStorage.setItem("UserInformation", JSON.stringify(_user)); + if (_user.ProfilePic) { + localStorage.setItem("profileImg", _user.ProfilePic); + } else { + localStorage.setItem("profileImg", ""); + } + // Check extended class user role and tenentId + try { + let userRoles = []; + const userSettings = appInfo.settings; + //Get Current user roles + const url = `${baseUrl}functions/UserGroups`; + const headers = { + "Content-Type": "application/json", + "X-Parse-Application-Id": parseAppId, + sessionToken: _user.sessionToken + }; + const body = { appname: appInfo.appname }; + const UserGroupsRes = await axios.post(url, JSON.stringify(body), { + headers: headers + }); + userRoles = (UserGroupsRes.data && UserGroupsRes.data.result) || []; + if (userRoles) { + let _currentRole = ""; + const excludeRoles = ["contracts_Guest", `contracts_appeditor`]; + const filteredRole = userRoles.filter( + (x) => !excludeRoles.includes(x) + ); + if (filteredRole?.length > 1) { + console.log("user has two roles"); + // _currentRole = filteredRole.filter((x) => x === "contracts_User"); + } else { + _currentRole = filteredRole?.[0] || ""; + } + if (_currentRole) { + const roleSetting = userSettings?.find( + (setting) => setting.role === _currentRole + ); + const redirectUrl = + location?.state?.from || + `/${roleSetting.pageType}/${roleSetting.pageId}`; + const _role = _currentRole.replace(`${appInfo.appname}_`, ""); + localStorage.setItem("_user_role", _role); + // Get TenentID from Extendend Class + localStorage.setItem( + "extended_class", + roleSetting.extended_class + ); + try { + const extUser = await Parse.Cloud.run("getUserDetails", { + email: _user.email + }); + if (extUser) { + const _extUser = JSON.parse(JSON.stringify(extUser)); + localStorage.setItem("userEmail", _extUser.Email); + localStorage.setItem("username", _extUser.Name); + localStorage.setItem("scriptId", true); + let tenentInfo = []; + const results = [extUser]; + if (results) { + let extendedInfo_stringify = JSON.stringify(results); + localStorage.setItem( + "Extand_Class", + extendedInfo_stringify + ); + if (_extUser.TenantId) { + const obj = { + tenentId: _extUser.TenantId.objectId, + tenentName: _extUser.TenantId.TenantName || "" + }; + localStorage.setItem("TenantId", obj.tenentId); + tenentInfo.push(obj); + dispatch(showTenant(obj.tenentName || "")); + localStorage.setItem("TenantName", obj.tenentName || ""); + } + localStorage.setItem("PageLanding", roleSetting.pageId); + localStorage.setItem("defaultmenuid", roleSetting.menuId); + localStorage.setItem("pageType", roleSetting.pageType); + if (isEnableSubscription) { + const res = await fetchSubscription(); + const plan = res.plan; + const billingDate = res.billingDate; + if (plan === "freeplan") { + navigate(redirectUrl); + } else if (billingDate) { + if (new Date(billingDate) > new Date()) { + localStorage.removeItem("userDetails"); + navigate(redirectUrl); + } else { + navigate(`/subscription`, { + replace: true + }); + } + } else { + navigate(`/subscription`, { + replace: true + }); + } + } else { + navigate(redirectUrl); + } + } + } + } catch (err) { + alert("user not exist."); + setMessage("Error: User not exist."); + console.log("err in get extUser", err); + } + } else { + alert("Role does not exists."); + setMessage("Error: Role does not exists."); + } + } else { + alert("Role does not exists."); + setMessage("Error: Role does not exists."); + } + } catch (err) { + console.log("err in usergroups", err); + } + } + } catch (err) { + console.log("Err in become method", err); + } + }; + // `handleCloseModal` is triggered when the user wants to close the new user flow modal + const handleCloseModal = () => { + setIsModal(false); + if (Parse?.User?.current()) { + Parse.User.logOut(); + } + }; + + // `validateInput` is used to verify the phone pattern + function validateInput(input) { + if (input) { + const pattern = /^(?!.*\+.*\+)[\d+-]*$/; + return pattern.test(input); + } else { + return true; + } + } + return ( + <div> + <div className="w-full h-screen flex flex-col justify-center items-center text-sm md:text-xl "> + {message === "Verifying SSO..." && ( + <img alt="loader" src={loader} className="w-[80px] h-[80px]" /> + )} + <div className="text-[gray]">{message}</div> + </div> + <ModalUi isOpen={isModal} title="Additional Info" showClose={false}> + <div className="relative"> + {isLoader && ( + <div className="absolute w-full h-full bg-black bg-opacity-25 flex justify-center items-center"> + <div + style={{ + fontSize: "45px", + color: "#3dd3e0" + }} + className="loader-37" + ></div> + </div> + )} + <form className="px-4 py-3" onSubmit={handleSubmitbtn}> + <div className="mb-3"> + <label + htmlFor="Company" + style={{ display: "flex" }} + className="block text-xs text-gray-700 font-semibold" + > + Phone <span style={{ fontSize: 13, color: "red" }}>*</span> + </label> + <input + type="tel" + className="px-3 py-2 w-full border-[1px] border-gray-300 rounded focus:outline-none text-xs" + id="Phone" + value={userDetails.phone} + onChange={(e) => + setUserDetails({ + ...userDetails, + phone: e.target.value + }) + } + disabled={isLoader} + required + /> + <p className="text-[10px] text-[red] ml-2"> + {!validateInput(userDetails.phone) && "Invalid phone value"} + </p> + </div> + <div className="mb-3"> + <label + htmlFor="Company" + style={{ display: "flex" }} + className="block text-xs text-gray-700 font-semibold" + > + Company <span style={{ fontSize: 13, color: "red" }}>*</span> + </label> + <input + type="text" + className="px-3 py-2 w-full border-[1px] border-gray-300 rounded focus:outline-none text-xs" + id="Company" + value={userDetails.Company} + onChange={(e) => + setUserDetails({ + ...userDetails, + Company: e.target.value + }) + } + disabled={isLoader} + required + /> + </div> + <div className="mb-3"> + <label + htmlFor="JobTitle" + style={{ display: "flex" }} + className="block text-xs text-gray-700 font-semibold" + > + Job Title + <span style={{ fontSize: 13, color: "red" }}>*</span> + </label> + <input + type="text" + className="px-3 py-2 w-full border-[1px] border-gray-300 rounded focus:outline-none text-xs" + id="JobTitle" + value={userDetails.Destination} + onChange={(e) => + setUserDetails({ + ...userDetails, + Destination: e.target.value + }) + } + disabled={isLoader} + required + /> + </div> + <div className="mt-4"> + <button + type="submit" + className="px-3 py-1.5 text-white rounded shadow-md text-center focus:outline-none " + style={{ + marginRight: 10, + backgroundColor: modalSubmitBtnColor + }} + > + Login + </button> + <button + type="button" + className="py-1.5 text-black border-[1px] border-[#ccc] shadow-md rounded focus:outline-none" + onClick={handleCloseModal} + style={{ width: 75, backgroundColor: modalCancelBtnColor }} + > + Cancel + </button> + </div> + </form> + </div> + </ModalUi> + </div> + ); +}; + +export default SSOVerify; diff --git a/apps/OpenSignServer/auth/authadapter.js b/apps/OpenSignServer/auth/authadapter.js new file mode 100644 index 000000000..1e12c7836 --- /dev/null +++ b/apps/OpenSignServer/auth/authadapter.js @@ -0,0 +1,28 @@ +import axios from 'axios'; +import dotenv from 'dotenv'; +dotenv.config(); +const ssoApiUrl = process.env.SSO_API_URL || 'https://sso.opensignlabs.com/api'; //'https://osl-jacksonv2.vercel.app/api'; +export const SSOAuth = { + // Returns a promise that fulfills if this user mail is valid. + validateAuthData: async authData => { + try { + const response = await axios.get(ssoApiUrl + '/oauth/userinfo', { + headers: { + Authorization: `Bearer ${authData.access_token}`, + }, + }); + if (response.data && response.data.id && response.data.email === authData.id) { + return; + } + throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'SSO auth is invalid for this user.'); + } catch (error) { + console.log('error in sso adapter', error?.response); + throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'SSO auth is invalid for this user.'); + } + }, + + // Returns a promise that fulfills if this app id is valid. + validateAppId: () => { + return Promise.resolve(); + }, +}; diff --git a/apps/OpenSignServer/cloud/main.js b/apps/OpenSignServer/cloud/main.js index 6de2c181a..59a903b1b 100644 --- a/apps/OpenSignServer/cloud/main.js +++ b/apps/OpenSignServer/cloud/main.js @@ -42,6 +42,8 @@ import linkContactToDoc from './parsefunction/linkContactToDoc.js'; import CreatePublicTemplate from './parsefunction/CreatePublicTemplate.js'; import GetPublicUserName from './parsefunction/GetPublicUserName.js'; import GetPublicTemplate from './parsefunction/GetPublicTemplate.js'; +import ssoSignin from './parsefunction/ssoSignin.js'; +import isextenduser from './parsefunction/isextenduser.js'; // This afterSave function triggers after an object is added or updated in the specified class, allowing for post-processing logic. Parse.Cloud.afterSave('contracts_Document', DocumentAftersave); @@ -94,3 +96,5 @@ Parse.Cloud.define('linkcontacttodoc', linkContactToDoc); Parse.Cloud.define('createpublictemplate', CreatePublicTemplate); Parse.Cloud.define('getpublicusername', GetPublicUserName); Parse.Cloud.define('getpublictemplate', GetPublicTemplate); +Parse.Cloud.define('ssosign', ssoSignin); +Parse.Cloud.define('isextenduser', isextenduser); diff --git a/apps/OpenSignServer/cloud/parsefunction/SendMailOTPv1.js b/apps/OpenSignServer/cloud/parsefunction/SendMailOTPv1.js index 290ff0c3a..52054688b 100644 --- a/apps/OpenSignServer/cloud/parsefunction/SendMailOTPv1.js +++ b/apps/OpenSignServer/cloud/parsefunction/SendMailOTPv1.js @@ -39,7 +39,7 @@ async function sendMailOTPv1(request) { code + '</p></div> </div> </div></body></html>', }); - console.log('OTP sent'); + console.log('OTP sent', code); if (request.params?.docId) { const extUserId = await getDocument(request.params?.docId); if (extUserId) { diff --git a/apps/OpenSignServer/cloud/parsefunction/UserGroups.js b/apps/OpenSignServer/cloud/parsefunction/UserGroups.js index 3e3dc0cb6..5ab2f4724 100644 --- a/apps/OpenSignServer/cloud/parsefunction/UserGroups.js +++ b/apps/OpenSignServer/cloud/parsefunction/UserGroups.js @@ -2,41 +2,41 @@ //-- Export Modules import dotenv from 'dotenv'; -dotenv.config() -import axios from "axios"; +dotenv.config(); +import axios from 'axios'; export async function getUserGroups(request) { try { var appname = request.params.appname; - if (appname == "") { - return Promise.reject("Error:please provide appname"); + if (appname == '') { + return Promise.reject('Error:please provide appname'); } var response = {}; var rolelist = {}; - appname = appname + "_"; + appname = appname + '_'; //--function to get the userid from session token function getuserid(request) { return new Promise(function (resolve, reject) { const options = { - url: process.env.SERVER_URL + "/users/me", - method: "get", + url: process.env.SERVER_URL + '/users/me', + method: 'get', headers: { - "X-Parse-Application-Id": process.env.APP_ID, - "X-Parse-Session-Token": request.headers["sessiontoken"], + 'X-Parse-Application-Id': process.env.APP_ID, + 'X-Parse-Session-Token': request.headers['sessiontoken'], }, }; axios(options) - .then((x) => { + .then(x => { const body = x.data; - var error = body == "" ? true : false; + var error = body == '' ? true : false; if (error) { - reject("result not found!"); + reject('result not found!'); } else { resolve(body); } }) - .catch((err) => { + .catch(err => { if (err) { console.error(err); return; @@ -46,7 +46,7 @@ export async function getUserGroups(request) { } var userData = await getuserid(request); var userid = userData.objectId; - console.log("userid " + userid); + // console.log("userid " + userid); var url = process.env.SERVER_URL + '/roles?where={"users":{"__type":"Pointer","className":"_User","objectId":"' + @@ -60,29 +60,29 @@ export async function getUserGroups(request) { return new Promise(function (resolve, reject) { const options = { url: url, - method: "get", + method: 'get', headers: { - "X-Parse-Application-Id": process.env.APP_ID, + 'X-Parse-Application-Id': process.env.APP_ID, }, }; axios(options) - .then((x) => { + .then(x => { const body = x.data; var roleres = []; - for (var i = 0; i < body["results"].length; i++) { - var rolename = body["results"][i]["name"]; + for (var i = 0; i < body['results'].length; i++) { + var rolename = body['results'][i]['name']; //var roleprefix = rolename.split("_")[0]; roleres.push(rolename); } - var error = roleres == "" ? true : false; + var error = roleres == '' ? true : false; if (error) { - reject("result not found!"); + reject('result not found!'); } else { resolve(roleres); } }) - .catch((err) => { + .catch(err => { if (err) { console.error(err); return; @@ -92,7 +92,7 @@ export async function getUserGroups(request) { } rolelist = await getRoleList(request); - console.log(rolelist); + // console.log(rolelist); //--check user roles according to appId var rolesInapp = []; for (let i = 0; i < rolelist.length; i++) { @@ -102,11 +102,11 @@ export async function getUserGroups(request) { rolesInapp.push(rolelist[i]); } } - console.log(rolesInapp); + // console.log(rolesInapp); return rolesInapp; } catch (err) { - console.log("err in usergroup"); + console.log('err in usergroup'); console.log(err); - return Promise.reject("Error:Result not found"); + return Promise.reject('Error:Result not found'); } } diff --git a/apps/OpenSignServer/cloud/parsefunction/isextenduser.js b/apps/OpenSignServer/cloud/parsefunction/isextenduser.js new file mode 100644 index 000000000..84111597e --- /dev/null +++ b/apps/OpenSignServer/cloud/parsefunction/isextenduser.js @@ -0,0 +1,30 @@ +/** + * Checks if a user exists in the extended class 'contracts_Users' based on the provided email. + * @param email - The request contains parameters, such as the user's email. + * @returns {Object} - Returns an object indicating whether the user exists. + */ +export default async function isextenduser(request) { + try { + // Query the 'contracts_Users' class in the database based on the provided email + const userQuery = new Parse.Query('contracts_Users'); + userQuery.equalTo('Email', request.params.email); + + // Execute the query + const res = await userQuery.first({ useMasterKey: true }); + + // Check if a user was found + if (res) { + // If user exists, return object with 'isUserExist' set to true + return { isUserExist: true }; + } else { + // If user does not exist, return object with 'isUserExist' set to false + return { isUserExist: false }; + } + } catch (err) { + // Handle errors + console.log('Error in userexist', err); + const code = err?.code || 400; + const message = err?.message || 'Something went wrong.'; + throw new Parse.Error(code, message); + } +} diff --git a/apps/OpenSignServer/cloud/parsefunction/ssoSignin.js b/apps/OpenSignServer/cloud/parsefunction/ssoSignin.js new file mode 100644 index 000000000..1067db69d --- /dev/null +++ b/apps/OpenSignServer/cloud/parsefunction/ssoSignin.js @@ -0,0 +1,132 @@ +import axios from 'axios'; + +const serverUrl = process.env.SERVER_URL; +const APPID = process.env.APP_ID; +const masterKEY = process.env.MASTER_KEY; +const clientUrl = process.env.PUBLIC_URL; +const ssoApiUrl = process.env.SSO_API_URL || 'https://sso.opensignlabs.com/api'; //'https://osl-jacksonv2.vercel.app/api'; +/** + * ssoSign is function which is used to sign up/sign in with SSO + * @param code It is code return by jackson using authorize endpoint + * @param email It is user's email with user sign in/sign up + * @returns if success {email, name, phone message, sessiontoken} else on reject error {code, message} + */ + +export default async function ssoSignin(request) { + const code = request.params.code; + const userEmail = request.params.email; + try { + const headers = { 'content-type': 'application/x-www-form-urlencoded' }; + const axiosRes = await axios.post( + ssoApiUrl + '/oauth/token', + { + grant_type: 'authorization_code', + client_id: 'dummy', + tenant: 'Okta-dev-nxglabs-in', + product: 'OpenSign', + client_secret: 'dummy', + redirect_uri: clientUrl + '/sso', + code: code, + }, + { headers: headers } + ); + const ssoAccessToken = axiosRes.data && axiosRes.data.access_token; + const authData = { sso: { id: userEmail, access_token: ssoAccessToken } }; + const userQuery = new Parse.Query(Parse.User); + userQuery.equalTo('username', userEmail); + const res = await userQuery.first({ useMasterKey: true }); + if (res) { + try { + const SignIn = await axios.put( + serverUrl + '/users/' + res.id, + { authData: authData }, + { + headers: { + 'X-Parse-Application-Id': APPID, + 'X-Parse-Master-key': masterKEY, + }, + } + ); + + if (SignIn.data) { + const response = await axios.get(ssoApiUrl + '/oauth/userinfo', { + headers: { + Authorization: `Bearer ${ssoAccessToken}`, + }, + }); + const sessiontoken = SignIn.data.sessionToken; + // console.log('sso sessiontoken', sessiontoken); + const payload = { + email: userEmail, + name: response.data?.firstName + ' ' + response.data?.lastName, + phone: response?.data?.phone || '', + message: 'User Sign In', + sessiontoken: sessiontoken, + }; + return payload; + } + } catch (err) { + const errCode = err?.response?.data?.code || err?.response?.status || err?.code || 400; + const message = + err?.response?.data?.error || + err?.response?.data || + err?.message || + 'Internal server error.'; + console.log('err in user sso sign in', errCode, message); + throw new Parse.Error(errCode, message); + } + } else { + // console.log("in sign up condition"); + const response = await axios.get('https://osl-jacksonv2.vercel.app/api/oauth/userinfo', { + headers: { + Authorization: `Bearer ${ssoAccessToken}`, + }, + }); + if (response.data && response.data.id) { + try { + const SignUp = await axios.post( + serverUrl + '/users', + { + authData: authData, + username: response.data.email, + email: response.data.email, + phone: response.data?.phone, + name: response.data?.firstName + ' ' + response.data?.lastName, + }, + { + headers: { + 'X-Parse-Application-Id': APPID, + 'X-Parse-Revocable-Session': '1', + }, + } + ); + if (SignUp.data) { + const sessiontoken = SignUp.data.sessionToken; + const payload = { + email: userEmail, + name: SignUp?.data?.name, + phone: SignUp?.data?.phone || '', + message: 'User Sign Up', + sessiontoken: sessiontoken, + }; + return payload; + } + } catch (err) { + const errCode = err?.response?.data?.code || err?.response?.status || err?.code || 400; + const message = + err?.response?.data?.error || + err?.response?.data || + err?.message || + 'Internal server error.'; + console.log('err in user sso sign up', errCode, message); + throw new Parse.Error(errCode, message); + } + } + } + } catch (err) { + const errCode = err?.response?.status || err?.code || 400; + const message = err?.response?.data || err?.message || 'Internal server error.'; + console.log('err in ssoSign', errCode, message); + throw new Parse.Error(errCode, message); + } +} diff --git a/apps/OpenSignServer/cloud/parsefunction/usersignup.js b/apps/OpenSignServer/cloud/parsefunction/usersignup.js index 84a7a101e..2d4338a5f 100644 --- a/apps/OpenSignServer/cloud/parsefunction/usersignup.js +++ b/apps/OpenSignServer/cloud/parsefunction/usersignup.js @@ -132,7 +132,9 @@ export default async function usersignup(request) { newObj.set('JobTitle', userDetails.jobTitle); } const extRes = await newObj.save(null, { useMasterKey: true }); - await saveSubscription(extRes.id, user.id, tenantRes.id, subscription); + if (subscription) { + await saveSubscription(extRes.id, user.id, tenantRes.id, subscription); + } return { message: 'User sign up', sessionToken: user.sessionToken }; } } catch (err) { diff --git a/apps/OpenSignServer/index.js b/apps/OpenSignServer/index.js index 9f7ab88ac..139f65bcf 100644 --- a/apps/OpenSignServer/index.js +++ b/apps/OpenSignServer/index.js @@ -18,7 +18,7 @@ import { createTransport } from 'nodemailer'; import { app as v1 } from './cloud/customRoute/v1/apiV1.js'; import { PostHog } from 'posthog-node'; import { useLocal } from './Utils.js'; - +import { SSOAuth } from './auth/authadapter.js'; let fsAdapter; if (useLocal !== 'true') { try { @@ -147,14 +147,7 @@ export const config = { google: { enabled: true, }, - ldap: { - enabled: true, - url: 'ldap://ldap.forumsys.com:389', - suffix: 'dc=example,dc=com', - // dn: 'ou=mathematicians, dc=example, dc=com', - groupCn: 'mathematicians', - groupFilter: '(&(uniqueMember=uid=,dc=example,dc=com)(objectClass=groupOfUniqueNames))', - }, + sso: SSOAuth, }, }; // Client-keys like the javascript key or the .NET key are not necessary with parse-server