From bc5c8cac33fa692d828f4edd071564705ebef276 Mon Sep 17 00:00:00 2001 From: prafull-opensignlabs Date: Fri, 31 May 2024 11:41:53 +0530 Subject: [PATCH 1/3] feat: existing user can sign in with sso --- apps/OpenSign/src/App.js | 2 + apps/OpenSign/src/components/Header.js | 4 +- apps/OpenSign/src/pages/Login.js | 225 +++++++++--------- apps/OpenSign/src/pages/SSOVerify.js | 208 ++++++++++++++++ apps/OpenSignServer/auth/authadapter.js | 27 +++ apps/OpenSignServer/cloud/main.js | 2 + .../cloud/parsefunction/SendMailOTPv1.js | 2 +- .../cloud/parsefunction/UserGroups.js | 52 ++-- .../cloud/parsefunction/ssoSignin.js | 115 +++++++++ apps/OpenSignServer/index.js | 11 +- 10 files changed, 497 insertions(+), 151 deletions(-) create mode 100644 apps/OpenSign/src/pages/SSOVerify.js create mode 100644 apps/OpenSignServer/auth/authadapter.js create mode 100644 apps/OpenSignServer/cloud/parsefunction/ssoSignin.js 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/pages/Login.js b/apps/OpenSign/src/pages/Login.js index 24ef4f7b8..c5251ed27 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(); @@ -1016,8 +1016,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 +1041,15 @@ function Login() { localStorage.setItem("baseUrl", baseUrl); localStorage.setItem("parseAppId", appid); }; + const handleSignInWithSSO = () => { + if (state?.email) { + openInNewTab( + `https://osl-jacksonv2.vercel.app/api/oauth/authorize?response_type=code&provider=saml&tenant=Okta-dev-nxglabs-in&product=OpenSign&redirect_uri=http://localhost:3000/sso&state=${state.email}` + ); + } else { + alert("Please provide email."); + } + }; return (
@@ -1087,136 +1097,123 @@ 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> + <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" /> + <label className="block text-xs" htmlFor="password"> + Password + </label> + <div className="relative"> <input - id="email" - type="text" + id="password" + type={state.passwordVisible ? "text" : "password"} className="px-3 py-2 w-full border-[1px] border-gray-300 rounded text-xs" - name="email" - value={state.email} + name="password" + value={state.password} 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> + <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> + )} */} + {appInfo?.googleClietId && ( + <GoogleSignInBtn + GoogleCred={appInfo.googleClietId} + thirdpartyLoginfn={thirdpartyLoginfn} + thirdpartyLoader={state.thirdpartyLoader} + setThirdpartyLoader={setThirdpartyLoader} + /> + )} <div - style={{ - textAlign: "center", - display: "flex", - alignItems: "center", - justifyContent: "center" - }} + 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()} > - {appInfo.googleClietId && appInfo.googleClietId !== "" ? ( - <GoogleSignInBtn - GoogleCred={appInfo.googleClietId} - thirdpartyLoginfn={thirdpartyLoginfn} - thirdpartyLoader={state.thirdpartyLoader} - setThirdpartyLoader={setThirdpartyLoader} - /> - ) : null} + 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..e811b2f08 --- /dev/null +++ b/apps/OpenSign/src/pages/SSOVerify.js @@ -0,0 +1,208 @@ +// import axios from "axios"; +import React, { useEffect } from "react"; +import { useLocation, useNavigate } from "react-router-dom"; +import Parse from "parse"; +import axios from "axios"; +import { appInfo } from "../constant/appinfo"; +import { isEnableSubscription } from "../constant/const"; +import { fetchSubscription } from "../constant/Utils"; +import { useDispatch } from "react-redux"; +import { showTenant } from "../redux/reducers/ShowTenant"; +const SSOVerify = () => { + const location = useLocation(); + const navigate = useNavigate(); + const dispatch = useDispatch(); + + useEffect(() => { + getAccesstoken(); + // eslint-disable-next-line + }, []); + const getAccesstoken = async () => { + const param = new URLSearchParams(location.search); + const code = param?.get("code"); + const state = param?.get("state"); + // console.log("code ", code); + try { + const ssosign = await Parse.Cloud.run("ssosign", { + code: code, + email: state + }); + await checkExtUser(ssosign); + // console.log("ssores ", ssosign); + } catch (err) { + console.log("err", err); + } + }; + const checkExtUser = async (ssosign) => { + // const extUser = new Parse.Query("contracts_Users"); + // extUser.equalTo("Email", details.Gmail); + // const extRes = await extUser.first(); + const params = { email: ssosign?.email }; + const extRes = await Parse.Cloud.run("getUserDetails", params); + // console.log("extRes ", extRes); + if (extRes) { + 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); + } + return { msg: "exist" }; + } else { + // setIsModal(true); + // setThirdpartyLoader(false); + console.log("not exist"); + return { msg: "notexist" }; + } + }; + 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)); + // console.log("_user ", _user); + 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 = []; + if (appInfo.settings) { + const userSettings = appInfo.settings; + + //Get Current user roles + let 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) { + // _currentRole = userRoles.filter((x) => x === "contracts_User"); + // } else { + // _currentRole = rolesfiltered[0]; + // } + _currentRole = userRoles?.find((x) => x === "contracts_User"); + 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); + // console.log("_extUser", _extUser); + 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) { + // const payload = { + // sessionToken: sessionToken + // }; + // setThirdpartyLoader(false); + // handleSubmitbtn(payload); + + alert("ext user not exist."); + console.log("err in get extUser", err); + } + } else { + alert("contracts_User role not exist"); + } + } else { + alert("contracts_User role not exist"); + } + } + } catch (err) { + console.log("err in usergroups", err); + } + } + } catch (err) { + console.log("Err in me call", err); + } + }; + return <div>Verifying SSO....</div>; +}; + +export default SSOVerify; diff --git a/apps/OpenSignServer/auth/authadapter.js b/apps/OpenSignServer/auth/authadapter.js new file mode 100644 index 000000000..a48b3cb76 --- /dev/null +++ b/apps/OpenSignServer/auth/authadapter.js @@ -0,0 +1,27 @@ +import axios from 'axios'; + +export const SSOAuth = { + // Returns a promise that fulfills if this user mail is valid. + validateAuthData: async authData => { + try { + const response = await axios.get('https://osl-jacksonv2.vercel.app/api/oauth/userinfo', { + headers: { + Authorization: `Bearer ${authData.access_token}`, + }, + }); + // console.log('response.data.id ', response.data.email); + // console.log('authData.id', authData.id); + 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) { + 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..1bbd9feea 100644 --- a/apps/OpenSignServer/cloud/main.js +++ b/apps/OpenSignServer/cloud/main.js @@ -42,6 +42,7 @@ 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'; // 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 +95,4 @@ 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); 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/ssoSignin.js b/apps/OpenSignServer/cloud/parsefunction/ssoSignin.js new file mode 100644 index 000000000..ca243cd4f --- /dev/null +++ b/apps/OpenSignServer/cloud/parsefunction/ssoSignin.js @@ -0,0 +1,115 @@ +import axios from 'axios'; + +const serverUrl = process.env.SERVER_URL; +const APPID = process.env.APP_ID; +const masterKEY = process.env.MASTER_KEY; + +/** + * 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, message, sessiontoken} else on reject {message} + */ + +export default async function ssoSignin(request) { + const code = request.params.code; + const userEmail = request.params.email; + + // console.log('code ', code); + try { + const headers = { 'content-type': 'application/x-www-form-urlencoded' }; + const axiosRes = await axios.post( + 'https://osl-jacksonv2.vercel.app/api/oauth/token', + { + grant_type: 'authorization_code', + client_id: 'dummy', + tenant: 'Okta-dev-nxglabs-in', + product: 'OpenSign', + client_secret: 'dummy', + redirect_uri: 'http://localhost:3000/sso', + code: code, + }, + { headers: headers } + ); + const ssoAccessToken = axiosRes.data && axiosRes.data.access_token; + // console.log('ssoAccessToken ', ssoAccessToken); + const authData = { sso: { id: userEmail, access_token: ssoAccessToken } }; + const userQuery = new Parse.Query(Parse.User); + userQuery.equalTo('email', 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 sessiontoken = SignIn.data.sessionToken; + // console.log('sso sessiontoken', sessiontoken); + return { + email: userEmail, + message: 'User Sign In', + sessiontoken: sessiontoken, + }; + } + } catch (err) { + console.log('err in user sso sign in', err); + throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'Internal server error.'); + } + } 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', + }, + } + ); + + // console.log("SignUp", SignUp); + + if (SignUp.data) { + const sessiontoken = SignUp.data.sessionToken; + const payload = { + email: userEmail, + message: 'User Sign Up', + sessiontoken: sessiontoken, + }; + return payload; + } + } catch (err) { + console.log('err in user sso sign up', err); + throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'Internal server error.'); + } + } + } + } 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/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 From afb4de9d0c3d78ce6dfe5d6fced2d2b2ed7849f5 Mon Sep 17 00:00:00 2001 From: prafull-opensignlabs <prafull.navkar@nxglabs.com> Date: Fri, 31 May 2024 18:22:16 +0530 Subject: [PATCH 2/3] Feat: create SSO user account on platform --- apps/OpenSign/src/components/sidebar/Menu.js | 6 +- apps/OpenSign/src/constant/Utils.js | 8 +- apps/OpenSign/src/pages/Login.js | 7 +- apps/OpenSign/src/pages/SSOVerify.js | 466 +++++++++++++----- apps/OpenSignServer/auth/authadapter.js | 9 +- apps/OpenSignServer/cloud/main.js | 2 + .../cloud/parsefunction/isextenduser.js | 30 ++ .../cloud/parsefunction/ssoSignin.js | 51 +- .../cloud/parsefunction/usersignup.js | 4 +- 9 files changed, 423 insertions(+), 160 deletions(-) create mode 100644 apps/OpenSignServer/cloud/parsefunction/isextenduser.js 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 c5251ed27..9a4d49e75 100644 --- a/apps/OpenSign/src/pages/Login.js +++ b/apps/OpenSign/src/pages/Login.js @@ -1041,10 +1041,15 @@ 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) { + const encodedEmail = encodeURIComponent(state.email); + const clientUrl = window.location.origin; openInNewTab( - `https://osl-jacksonv2.vercel.app/api/oauth/authorize?response_type=code&provider=saml&tenant=Okta-dev-nxglabs-in&product=OpenSign&redirect_uri=http://localhost:3000/sso&state=${state.email}` + `https://osl-jacksonv2.vercel.app/api/oauth/authorize?response_type=code&provider=saml&tenant=Okta-dev-nxglabs-in&product=OpenSign&redirect_uri=${clientUrl}/sso&state=${encodedEmail}`, + "_self" ); } else { alert("Please provide email."); diff --git a/apps/OpenSign/src/pages/SSOVerify.js b/apps/OpenSign/src/pages/SSOVerify.js index e811b2f08..988e1e5f8 100644 --- a/apps/OpenSign/src/pages/SSOVerify.js +++ b/apps/OpenSign/src/pages/SSOVerify.js @@ -1,64 +1,143 @@ -// import axios from "axios"; -import React, { useEffect } from "react"; +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 } from "../constant/const"; +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"; + const SSOVerify = () => { const location = useLocation(); const navigate = useNavigate(); const dispatch = useDispatch(); + const [isModal, setIsModal] = useState(false); + const [ssoErrMsg, setSsoErrMsg] = useState("Verifying SSO..."); + const [isLoader, setIsLoader] = useState(false); + const [userDetails, setUserDetails] = useState({ + name: "", + email: "", + phone: "", + Destination: "", + Company: "" + }); useEffect(() => { - getAccesstoken(); + linkUserWithSSO(); // eslint-disable-next-line }, []); - const getAccesstoken = async () => { + + // `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"); - // console.log("code ", code); 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); - // console.log("ssores ", ssosign); } catch (err) { - console.log("err", err); + setSsoErrMsg(err.message); + console.log("err", err.message); } }; const checkExtUser = async (ssosign) => { - // const extUser = new Parse.Query("contracts_Users"); - // extUser.equalTo("Email", details.Gmail); - // const extRes = await extUser.first(); const params = { email: ssosign?.email }; - const extRes = await Parse.Cloud.run("getUserDetails", params); - // console.log("extRes ", extRes); - if (extRes) { - 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); + 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 || "" + })); } - return { msg: "exist" }; - } else { - // setIsModal(true); - // setThirdpartyLoader(false); - console.log("not exist"); - return { msg: "notexist" }; + } catch (err) { + console.log("Err in isextenduser or getuserdetails cloud function", err); + alert(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, please try again later."); + 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"); @@ -67,7 +146,6 @@ const SSOVerify = () => { if (validUser) { localStorage.setItem("accesstoken", sessionToken); const _user = JSON.parse(JSON.stringify(validUser)); - // console.log("_user ", _user); localStorage.setItem("UserInformation", JSON.stringify(_user)); if (_user.ProfilePic) { localStorage.setItem("profileImg", _user.ProfilePic); @@ -77,132 +155,252 @@ const SSOVerify = () => { // Check extended class user role and tenentId try { let userRoles = []; - if (appInfo.settings) { - const userSettings = appInfo.settings; - - //Get Current user roles - let 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) { - // _currentRole = userRoles.filter((x) => x === "contracts_User"); - // } else { - // _currentRole = rolesfiltered[0]; - // } - _currentRole = userRoles?.find((x) => x === "contracts_User"); - 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); - // console.log("_extUser", _extUser); - 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") { + 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 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); + navigate(`/subscription`, { + replace: true + }); } + } else { + navigate(redirectUrl); } } - } catch (err) { - // const payload = { - // sessionToken: sessionToken - // }; - // setThirdpartyLoader(false); - // handleSubmitbtn(payload); - - alert("ext user not exist."); - console.log("err in get extUser", err); } - } else { - alert("contracts_User role not exist"); + } catch (err) { + alert("user not exist."); + console.log("err in get extUser", err); } } else { - alert("contracts_User role not exist"); + alert("Role does not exists."); } + } else { + alert("Role does not exists."); } } catch (err) { console.log("err in usergroups", err); } } } catch (err) { - console.log("Err in me call", err); + console.log("Err in become method", err); } }; - return <div>Verifying SSO....</div>; + // `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 justify-center items-center text-sm md:text-xl "> + {ssoErrMsg} + </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 index a48b3cb76..528ea8176 100644 --- a/apps/OpenSignServer/auth/authadapter.js +++ b/apps/OpenSignServer/auth/authadapter.js @@ -1,21 +1,22 @@ import axios from 'axios'; - +import dotenv from 'dotenv'; +dotenv.config(); +const ssoApiUrl = process.env.SSO_API_URL || '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('https://osl-jacksonv2.vercel.app/api/oauth/userinfo', { + const response = await axios.get(ssoApiUrl + '/oauth/userinfo', { headers: { Authorization: `Bearer ${authData.access_token}`, }, }); - // console.log('response.data.id ', response.data.email); - // console.log('authData.id', authData.id); 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.'); } }, diff --git a/apps/OpenSignServer/cloud/main.js b/apps/OpenSignServer/cloud/main.js index 1bbd9feea..59a903b1b 100644 --- a/apps/OpenSignServer/cloud/main.js +++ b/apps/OpenSignServer/cloud/main.js @@ -43,6 +43,7 @@ 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); @@ -96,3 +97,4 @@ 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/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 index ca243cd4f..6e50d0639 100644 --- a/apps/OpenSignServer/cloud/parsefunction/ssoSignin.js +++ b/apps/OpenSignServer/cloud/parsefunction/ssoSignin.js @@ -3,39 +3,37 @@ 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://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, message, sessiontoken} else on reject {message} + * @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; - - // console.log('code ', code); try { const headers = { 'content-type': 'application/x-www-form-urlencoded' }; const axiosRes = await axios.post( - 'https://osl-jacksonv2.vercel.app/api/oauth/token', + ssoApiUrl + '/oauth/token', { grant_type: 'authorization_code', client_id: 'dummy', tenant: 'Okta-dev-nxglabs-in', product: 'OpenSign', client_secret: 'dummy', - redirect_uri: 'http://localhost:3000/sso', + redirect_uri: clientUrl + '/sso', code: code, }, { headers: headers } ); const ssoAccessToken = axiosRes.data && axiosRes.data.access_token; - // console.log('ssoAccessToken ', ssoAccessToken); const authData = { sso: { id: userEmail, access_token: ssoAccessToken } }; const userQuery = new Parse.Query(Parse.User); - userQuery.equalTo('email', userEmail); + userQuery.equalTo('username', userEmail); const res = await userQuery.first({ useMasterKey: true }); if (res) { try { @@ -51,17 +49,31 @@ export default async function ssoSignin(request) { ); 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); - return { + 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) { - console.log('err in user sso sign in', err); - throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'Internal server error.'); + 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"); @@ -79,7 +91,7 @@ export default async function ssoSignin(request) { username: response.data.email, email: response.data.email, phone: response.data?.phone, - name: response.data?.firstName + response.data?.lastName, + name: response.data?.firstName + ' ' + response.data?.lastName, }, { headers: { @@ -88,21 +100,26 @@ export default async function ssoSignin(request) { }, } ); - - // console.log("SignUp", SignUp); - 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) { - console.log('err in user sso sign up', err); - throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'Internal server error.'); + 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); } } } 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) { From d071986aacdd68be542901522903c90dff9d5fcc Mon Sep 17 00:00:00 2001 From: prafull-opensignlabs <prafull.navkar@nxglabs.com> Date: Fri, 31 May 2024 19:00:29 +0530 Subject: [PATCH 3/3] feat: hide password input for sso and restict sso --- apps/OpenSign/src/pages/Login.js | 83 +++++++++++-------- apps/OpenSign/src/pages/SSOVerify.js | 21 +++-- apps/OpenSignServer/auth/authadapter.js | 2 +- .../cloud/parsefunction/ssoSignin.js | 2 +- 4 files changed, 64 insertions(+), 44 deletions(-) diff --git a/apps/OpenSign/src/pages/Login.js b/apps/OpenSign/src/pages/Login.js index 9a4d49e75..c49b1e77e 100644 --- a/apps/OpenSign/src/pages/Login.js +++ b/apps/OpenSign/src/pages/Login.js @@ -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")) { @@ -1045,10 +1046,14 @@ function Login() { // `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( - `https://osl-jacksonv2.vercel.app/api/oauth/authorize?response_type=code&provider=saml&tenant=Okta-dev-nxglabs-in&product=OpenSign&redirect_uri=${clientUrl}/sso&state=${encodedEmail}`, + `${ssoApiUrl}/oauth/authorize?response_type=code&provider=saml&tenant=${domain}&product=OpenSign&redirect_uri=${clientUrl}/sso&state=${encodedEmail}`, "_self" ); } else { @@ -1122,34 +1127,40 @@ function Login() { 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> + {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" @@ -1209,12 +1220,14 @@ function Login() { setThirdpartyLoader={setThirdpartyLoader} /> )} - <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> + {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 && ( diff --git a/apps/OpenSign/src/pages/SSOVerify.js b/apps/OpenSign/src/pages/SSOVerify.js index 988e1e5f8..85dbb70bd 100644 --- a/apps/OpenSign/src/pages/SSOVerify.js +++ b/apps/OpenSign/src/pages/SSOVerify.js @@ -12,13 +12,14 @@ 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 [ssoErrMsg, setSsoErrMsg] = useState("Verifying SSO..."); + const [message, setMessage] = useState("Verifying SSO..."); const [isLoader, setIsLoader] = useState(false); const [userDetails, setUserDetails] = useState({ name: "", @@ -48,7 +49,7 @@ const SSOVerify = () => { // `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) { - setSsoErrMsg(err.message); + setMessage("Error: " + err.message); console.log("err", err.message); } }; @@ -81,7 +82,7 @@ const SSOVerify = () => { } } catch (err) { console.log("Err in isextenduser or getuserdetails cloud function", err); - alert(err.message); + setMessage("Error: " + err.message); } }; // `handleSubmitbtn` is used to create a user in the extended class @@ -124,12 +125,12 @@ const SSOVerify = () => { } catch (err) { console.log("error in usersignup", err); localStorage.removeItem("accesstoken"); - alert("something went wrong, please try again later."); + alert("Something went wrong."); setIsLoader(false); } } else { localStorage.removeItem("accesstoken"); - alert("Internal server error !"); + alert("Internal server error."); setIsLoader(false); } } else { @@ -251,13 +252,16 @@ const SSOVerify = () => { } } 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); @@ -286,8 +290,11 @@ const SSOVerify = () => { } return ( <div> - <div className="w-full h-screen flex justify-center items-center text-sm md:text-xl "> - {ssoErrMsg} + <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"> diff --git a/apps/OpenSignServer/auth/authadapter.js b/apps/OpenSignServer/auth/authadapter.js index 528ea8176..1e12c7836 100644 --- a/apps/OpenSignServer/auth/authadapter.js +++ b/apps/OpenSignServer/auth/authadapter.js @@ -1,7 +1,7 @@ import axios from 'axios'; import dotenv from 'dotenv'; dotenv.config(); -const ssoApiUrl = process.env.SSO_API_URL || 'https://osl-jacksonv2.vercel.app/api'; +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 => { diff --git a/apps/OpenSignServer/cloud/parsefunction/ssoSignin.js b/apps/OpenSignServer/cloud/parsefunction/ssoSignin.js index 6e50d0639..1067db69d 100644 --- a/apps/OpenSignServer/cloud/parsefunction/ssoSignin.js +++ b/apps/OpenSignServer/cloud/parsefunction/ssoSignin.js @@ -4,7 +4,7 @@ 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://osl-jacksonv2.vercel.app/api'; +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