diff --git a/package.json b/package.json index cedcb40..49a8025 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "html-react-parser": "1.4.8", "html-to-text": "^9.0.5", "http-proxy-middleware": "2.0.6", + "jwt-decode": "3.1.2", "rapidoc": "9.3.4", "react": "18.0.0", "react-ace": "10.1.0", diff --git a/src/App.tsx b/src/App.tsx index c9995f6..edf1372 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -33,7 +33,8 @@ import CssBaseline from "@mui/material/CssBaseline"; import Icon from "@mui/material/Icon"; import {ThemeProvider} from "@mui/material/styles"; import {LicenseInfo} from "@mui/x-license-pro"; -import React, {JSXElementConstructor, Key, ReactElement, useContext, useEffect, useState,} from "react"; +import jwt_decode from "jwt-decode"; +import React, {JSXElementConstructor, Key, ReactElement, useEffect, useState,} from "react"; import {useCookies} from "react-cookie"; import {Navigate, Route, Routes, useLocation,} from "react-router-dom"; import {Md5} from "ts-md5/dist/md5"; @@ -57,11 +58,11 @@ import ProcessUtils from "qqq/utils/qqq/ProcessUtils"; const qController = Client.getInstance(); -export const SESSION_ID_COOKIE_NAME = "sessionId"; +export const SESSION_UUID_COOKIE_NAME = "sessionUUID"; export default function App() { - const [, setCookie, removeCookie] = useCookies([SESSION_ID_COOKIE_NAME]); + const [, setCookie, removeCookie] = useCookies([SESSION_UUID_COOKIE_NAME]); const {user, getAccessTokenSilently, logout} = useAuth0(); const [loadingToken, setLoadingToken] = useState(false); const [isFullyAuthenticated, setIsFullyAuthenticated] = useState(false); @@ -69,8 +70,55 @@ export default function App() const [branding, setBranding] = useState({} as QBrandingMetaData); const [metaData, setMetaData] = useState({} as QInstance); const [needLicenseKey, setNeedLicenseKey] = useState(true); + const [loggedInUser, setLoggedInUser] = useState({} as { name?: string, email?: string }); const [defaultRoute, setDefaultRoute] = useState("/no-apps"); + const shouldStoreNewToken = (newToken: string, oldToken: string): boolean => + { + if (!oldToken) + { + return (true); + } + + try + { + const oldJSON: any = jwt_decode(oldToken); + const newJSON: any = jwt_decode(newToken); + + //////////////////////////////////////////////////////////////////////////////////// + // if the old (local storage) token is expired, then we need to store the new one // + //////////////////////////////////////////////////////////////////////////////////// + const oldExp = oldJSON["exp"]; + if(oldExp * 1000 < (new Date().getTime())) + { + console.log("Access token in local storage was expired."); + return (true); + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + // remove the exp & iat values from what we compare - as they are always different from auth0 // + // note, this is only deleting them from what we compare, not from what we'd store. // + //////////////////////////////////////////////////////////////////////////////////////////////// + delete newJSON["exp"] + delete newJSON["iat"] + delete oldJSON["exp"] + delete oldJSON["iat"] + + const different = JSON.stringify(newJSON) !== JSON.stringify(oldJSON); + if(different) + { + console.log("Latest access token from auth0 has changed vs localStorage."); + } + return (different); + } + catch(e) + { + console.log("Caught in shouldStoreNewToken: " + e) + } + + return (true); + }; + useEffect(() => { if (loadingToken) @@ -92,20 +140,38 @@ export default function App() { console.log("Loading token from auth0..."); const accessToken = await getAccessTokenSilently(); - qController.setAuthorizationHeaderValue("Bearer " + accessToken); - ///////////////////////////////////////////////////////////////////////////////// - // we've stopped using session id cook with auth0, so make sure it is not set. // - ///////////////////////////////////////////////////////////////////////////////// - removeCookie(SESSION_ID_COOKIE_NAME); + const lsAccessToken = localStorage.getItem("accessToken"); + if (shouldStoreNewToken(accessToken, lsAccessToken)) + { + console.log("Sending accessToken to backend, requesting a sessionUUID..."); + const newSessionUuid = await qController.manageSession(accessToken, null); + setCookie(SESSION_UUID_COOKIE_NAME, newSessionUuid, {path: "/"}); + localStorage.setItem("accessToken", accessToken); + } + + /* + //////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // todo#authHeader - this is our quick rollback plan - if we feel the need to stop using the cookie approach. // + // we turn off the shouldStoreNewToken block above, and turn on these 2 lines. // + //////////////////////////////////////////////////////////////////////////////////////////////////////////////// + removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"}); + localStorage.removeItem("accessToken"); + */ setIsFullyAuthenticated(true); + qController.setGotAuthentication(); + qController.setAuthorizationHeaderValue("Bearer " + accessToken); + + setLoggedInUser(user); console.log("Token load complete."); } catch (e) { console.log(`Error loading token: ${JSON.stringify(e)}`); qController.clearAuthenticationMetaDataLocalStorage(); + localStorage.removeItem("accessToken") + removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"}); logout(); return; } @@ -116,9 +182,9 @@ export default function App() // use a random token if anonymous or mock // ///////////////////////////////////////////// console.log("Generating random token..."); - qController.setAuthorizationHeaderValue(null); + qController.setAuthorizationHeaderValue(Md5.hashStr(`${new Date()}`)); setIsFullyAuthenticated(true); - setCookie(SESSION_ID_COOKIE_NAME, Md5.hashStr(`${new Date()}`), {path: "/"}); + setCookie(SESSION_UUID_COOKIE_NAME, Md5.hashStr(`${new Date()}`), {path: "/"}); console.log("Token generation complete."); return; } @@ -149,7 +215,7 @@ export default function App() const [needToLoadRoutes, setNeedToLoadRoutes] = useState(true); const [sideNavRoutes, setSideNavRoutes] = useState([]); const [appRoutes, setAppRoutes] = useState(null as any); - const [pathToLabelMap, setPathToLabelMap] = useState({} as {[path: string]: string}); + const [pathToLabelMap, setPathToLabelMap] = useState({} as { [path: string]: string }); //////////////////////////////////////////// // load qqq meta data to make more routes // @@ -267,14 +333,14 @@ export default function App() name: `${app.label}`, key: app.name, route: path, - component: , + component: , }); routeList.push({ name: `${app.label}`, key: app.name, route: `${path}/savedFilter/:id`, - component: , + component: , }); routeList.push({ @@ -429,11 +495,11 @@ export default function App() let profileRoutes = {}; const gravatarBase = "https://www.gravatar.com/avatar/"; - const hash = Md5.hashStr(user?.email || "user"); + const hash = Md5.hashStr(loggedInUser?.email || "user"); const profilePicture = `${gravatarBase}${hash}`; profileRoutes = { type: "collapse", - name: user?.name, + name: loggedInUser?.name ?? "Anonymous", key: "username", noCollapse: true, icon: , @@ -469,7 +535,7 @@ export default function App() } const pathToLabelMap: {[path: string]: string} = {} - for(let i =0; i - + { logout(); - removeCookie(SESSION_ID_COOKIE_NAME, {path: "/"}); + removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"}); }); return ( diff --git a/src/qqq/pages/processes/ProcessRun.tsx b/src/qqq/pages/processes/ProcessRun.tsx index ae629c3..d0e0a0b 100644 --- a/src/qqq/pages/processes/ProcessRun.tsx +++ b/src/qqq/pages/processes/ProcessRun.tsx @@ -228,12 +228,19 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is const download = (url: string, fileName: string) => { - const qController = Client.getInstance(); - + ///////////////////////////////////////////////////////////////////////////////////////////// + // todo - this could be simplified. // + // it was originally built like this when we had to submit full access token to backend... // + ///////////////////////////////////////////////////////////////////////////////////////////// let xhr = new XMLHttpRequest(); xhr.open("POST", url); xhr.responseType = "blob"; let formData = new FormData(); + + //////////////////////////////////// + // todo#authHeader - delete this. // + //////////////////////////////////// + const qController = Client.getInstance(); formData.append("Authorization", qController.getAuthorizationHeaderValue()); // @ts-ignore diff --git a/src/qqq/pages/records/query/RecordQuery.tsx b/src/qqq/pages/records/query/RecordQuery.tsx index acd75b1..e3f5a62 100644 --- a/src/qqq/pages/records/query/RecordQuery.tsx +++ b/src/qqq/pages/records/query/RecordQuery.tsx @@ -1140,6 +1140,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element Generating file ${filename}${totalRecords ? " with " + totalRecords.toLocaleString() + " record" + (totalRecords == 1 ? "" : "s") : ""}...
+ diff --git a/src/qqq/utils/HtmlUtils.ts b/src/qqq/utils/HtmlUtils.ts index 602b3a8..3d69e50 100644 --- a/src/qqq/utils/HtmlUtils.ts +++ b/src/qqq/utils/HtmlUtils.ts @@ -95,6 +95,11 @@ export default class HtmlUtils form.setAttribute("target", "downloadIframe"); iframe.appendChild(form); + ///////////////////////////////////////////////////////////////////////////////////////////// + // todo#authHeader - remove after comfortable with sessionUUID // + // todo - this could be simplified (i think?) // + // it was originally built like this when we had to submit full access token to backend... // + ///////////////////////////////////////////////////////////////////////////////////////////// const authorizationInput = document.createElement("input"); authorizationInput.setAttribute("type", "hidden"); authorizationInput.setAttribute("id", "authorizationInput"); @@ -118,6 +123,11 @@ export default class HtmlUtils { if(url.startsWith("data:")) { + ///////////////////////////////////////////////////////////////////////////////////////////// + // todo#authHeader - remove the Authorization input after comfortable with sessionUUID // + // todo - this could be simplified (i think?) // + // it was originally built like this when we had to submit full access token to backend... // + ///////////////////////////////////////////////////////////////////////////////////////////// const openInWindow = window.open("", "_blank"); openInWindow.document.write(` diff --git a/src/setupProxy.js b/src/setupProxy.js index d693159..dfe7bdd 100644 --- a/src/setupProxy.js +++ b/src/setupProxy.js @@ -49,6 +49,7 @@ module.exports = function (app) app.use("/data/*", getRequestHandler()); app.use("/widget/*", getRequestHandler()); app.use("/serverInfo", getRequestHandler()); + app.use("/manageSession", getRequestHandler()); app.use("/processes", getRequestHandler()); app.use("/reports", getRequestHandler()); app.use("/images", getRequestHandler());