Merge branch 'main' into feature/fix-urlencoding-primary-keys

This commit is contained in:
2023-09-26 08:32:17 -05:00
7 changed files with 111 additions and 22 deletions

View File

@ -33,6 +33,7 @@
"html-react-parser": "1.4.8", "html-react-parser": "1.4.8",
"html-to-text": "^9.0.5", "html-to-text": "^9.0.5",
"http-proxy-middleware": "2.0.6", "http-proxy-middleware": "2.0.6",
"jwt-decode": "3.1.2",
"rapidoc": "9.3.4", "rapidoc": "9.3.4",
"react": "18.0.0", "react": "18.0.0",
"react-ace": "10.1.0", "react-ace": "10.1.0",

View File

@ -33,7 +33,8 @@ import CssBaseline from "@mui/material/CssBaseline";
import Icon from "@mui/material/Icon"; import Icon from "@mui/material/Icon";
import {ThemeProvider} from "@mui/material/styles"; import {ThemeProvider} from "@mui/material/styles";
import {LicenseInfo} from "@mui/x-license-pro"; 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 {useCookies} from "react-cookie";
import {Navigate, Route, Routes, useLocation,} from "react-router-dom"; import {Navigate, Route, Routes, useLocation,} from "react-router-dom";
import {Md5} from "ts-md5/dist/md5"; import {Md5} from "ts-md5/dist/md5";
@ -57,11 +58,11 @@ import ProcessUtils from "qqq/utils/qqq/ProcessUtils";
const qController = Client.getInstance(); const qController = Client.getInstance();
export const SESSION_ID_COOKIE_NAME = "sessionId"; export const SESSION_UUID_COOKIE_NAME = "sessionUUID";
export default function App() 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 {user, getAccessTokenSilently, logout} = useAuth0();
const [loadingToken, setLoadingToken] = useState(false); const [loadingToken, setLoadingToken] = useState(false);
const [isFullyAuthenticated, setIsFullyAuthenticated] = useState(false); const [isFullyAuthenticated, setIsFullyAuthenticated] = useState(false);
@ -69,8 +70,55 @@ export default function App()
const [branding, setBranding] = useState({} as QBrandingMetaData); const [branding, setBranding] = useState({} as QBrandingMetaData);
const [metaData, setMetaData] = useState({} as QInstance); const [metaData, setMetaData] = useState({} as QInstance);
const [needLicenseKey, setNeedLicenseKey] = useState(true); const [needLicenseKey, setNeedLicenseKey] = useState(true);
const [loggedInUser, setLoggedInUser] = useState({} as { name?: string, email?: string });
const [defaultRoute, setDefaultRoute] = useState("/no-apps"); 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(() => useEffect(() =>
{ {
if (loadingToken) if (loadingToken)
@ -92,20 +140,38 @@ export default function App()
{ {
console.log("Loading token from auth0..."); console.log("Loading token from auth0...");
const accessToken = await getAccessTokenSilently(); const accessToken = await getAccessTokenSilently();
qController.setAuthorizationHeaderValue("Bearer " + accessToken);
///////////////////////////////////////////////////////////////////////////////// const lsAccessToken = localStorage.getItem("accessToken");
// we've stopped using session id cook with auth0, so make sure it is not set. // if (shouldStoreNewToken(accessToken, lsAccessToken))
///////////////////////////////////////////////////////////////////////////////// {
removeCookie(SESSION_ID_COOKIE_NAME); 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); setIsFullyAuthenticated(true);
qController.setGotAuthentication();
qController.setAuthorizationHeaderValue("Bearer " + accessToken);
setLoggedInUser(user);
console.log("Token load complete."); console.log("Token load complete.");
} }
catch (e) catch (e)
{ {
console.log(`Error loading token: ${JSON.stringify(e)}`); console.log(`Error loading token: ${JSON.stringify(e)}`);
qController.clearAuthenticationMetaDataLocalStorage(); qController.clearAuthenticationMetaDataLocalStorage();
localStorage.removeItem("accessToken")
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
logout(); logout();
return; return;
} }
@ -116,9 +182,9 @@ export default function App()
// use a random token if anonymous or mock // // use a random token if anonymous or mock //
///////////////////////////////////////////// /////////////////////////////////////////////
console.log("Generating random token..."); console.log("Generating random token...");
qController.setAuthorizationHeaderValue(null); qController.setAuthorizationHeaderValue(Md5.hashStr(`${new Date()}`));
setIsFullyAuthenticated(true); 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."); console.log("Token generation complete.");
return; return;
} }
@ -149,7 +215,7 @@ export default function App()
const [needToLoadRoutes, setNeedToLoadRoutes] = useState(true); const [needToLoadRoutes, setNeedToLoadRoutes] = useState(true);
const [sideNavRoutes, setSideNavRoutes] = useState([]); const [sideNavRoutes, setSideNavRoutes] = useState([]);
const [appRoutes, setAppRoutes] = useState(null as any); 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 // // load qqq meta data to make more routes //
@ -267,14 +333,14 @@ export default function App()
name: `${app.label}`, name: `${app.label}`,
key: app.name, key: app.name,
route: path, route: path,
component: <RecordQuery table={table} key={table.name}/>, component: <RecordQuery table={table} key={table.name} />,
}); });
routeList.push({ routeList.push({
name: `${app.label}`, name: `${app.label}`,
key: app.name, key: app.name,
route: `${path}/savedFilter/:id`, route: `${path}/savedFilter/:id`,
component: <RecordQuery table={table} key={table.name}/>, component: <RecordQuery table={table} key={table.name} />,
}); });
routeList.push({ routeList.push({
@ -429,11 +495,11 @@ export default function App()
let profileRoutes = {}; let profileRoutes = {};
const gravatarBase = "https://www.gravatar.com/avatar/"; 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}`; const profilePicture = `${gravatarBase}${hash}`;
profileRoutes = { profileRoutes = {
type: "collapse", type: "collapse",
name: user?.name, name: loggedInUser?.name ?? "Anonymous",
key: "username", key: "username",
noCollapse: true, noCollapse: true,
icon: <Avatar src={profilePicture} alt="{user?.name}" />, icon: <Avatar src={profilePicture} alt="{user?.name}" />,
@ -469,7 +535,7 @@ export default function App()
} }
const pathToLabelMap: {[path: string]: string} = {} const pathToLabelMap: {[path: string]: string} = {}
for(let i =0; i<appRoutesList.length; i++) for (let i = 0; i < appRoutesList.length; i++)
{ {
const route = appRoutesList[i]; const route = appRoutesList[i];
pathToLabelMap[route.route] = route.name; pathToLabelMap[route.route] = route.name;
@ -495,7 +561,10 @@ export default function App()
{ {
if ((e as QException).status === "401") if ((e as QException).status === "401")
{ {
console.log("Exception is a QException with status = 401. Clearing some of localStorage & cookies");
qController.clearAuthenticationMetaDataLocalStorage(); qController.clearAuthenticationMetaDataLocalStorage();
localStorage.removeItem("accessToken")
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
////////////////////////////////////////////////////// //////////////////////////////////////////////////////
// todo - this is auth0 logout... make more generic // // todo - this is auth0 logout... make more generic //
@ -596,7 +665,7 @@ export default function App()
}}> }}>
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<CssBaseline /> <CssBaseline />
<CommandMenu metaData={metaData}/> <CommandMenu metaData={metaData} />
<Sidenav <Sidenav
color={sidenavColor} color={sidenavColor}
icon={branding.icon} icon={branding.icon}

View File

@ -22,7 +22,7 @@
import {useAuth0} from "@auth0/auth0-react"; import {useAuth0} from "@auth0/auth0-react";
import React, {useEffect} from "react"; import React, {useEffect} from "react";
import {useCookies} from "react-cookie"; import {useCookies} from "react-cookie";
import {SESSION_ID_COOKIE_NAME} from "App"; import {SESSION_UUID_COOKIE_NAME} from "App";
interface Props interface Props
{ {
@ -33,13 +33,13 @@ interface Props
function HandleAuthorizationError({errorMessage}: Props) function HandleAuthorizationError({errorMessage}: Props)
{ {
const [, , removeCookie] = useCookies([SESSION_ID_COOKIE_NAME]); const [, , removeCookie] = useCookies([SESSION_UUID_COOKIE_NAME]);
const {logout} = useAuth0(); const {logout} = useAuth0();
useEffect(() => useEffect(() =>
{ {
logout(); logout();
removeCookie(SESSION_ID_COOKIE_NAME, {path: "/"}); removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
}); });
return ( return (

View File

@ -228,12 +228,19 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
const download = (url: string, fileName: string) => 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(); let xhr = new XMLHttpRequest();
xhr.open("POST", url); xhr.open("POST", url);
xhr.responseType = "blob"; xhr.responseType = "blob";
let formData = new FormData(); let formData = new FormData();
////////////////////////////////////
// todo#authHeader - delete this. //
////////////////////////////////////
const qController = Client.getInstance();
formData.append("Authorization", qController.getAuthorizationHeaderValue()); formData.append("Authorization", qController.getAuthorizationHeaderValue());
// @ts-ignore // @ts-ignore

View File

@ -1140,6 +1140,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
<body> <body>
Generating file <u>${filename}</u>${totalRecords ? " with " + totalRecords.toLocaleString() + " record" + (totalRecords == 1 ? "" : "s") : ""}... Generating file <u>${filename}</u>${totalRecords ? " with " + totalRecords.toLocaleString() + " record" + (totalRecords == 1 ? "" : "s") : ""}...
<form id="exportForm" method="post" action="${url}" > <form id="exportForm" method="post" action="${url}" >
<!-- todo#authHeader - remove this. -->
<input type="hidden" name="Authorization" value="${qController.getAuthorizationHeaderValue()}"> <input type="hidden" name="Authorization" value="${qController.getAuthorizationHeaderValue()}">
<input type="hidden" name="fields" value="${visibleFields.join(",")}"> <input type="hidden" name="fields" value="${visibleFields.join(",")}">
<input type="hidden" name="filter" id="filter"> <input type="hidden" name="filter" id="filter">

View File

@ -95,6 +95,11 @@ export default class HtmlUtils
form.setAttribute("target", "downloadIframe"); form.setAttribute("target", "downloadIframe");
iframe.appendChild(form); 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"); const authorizationInput = document.createElement("input");
authorizationInput.setAttribute("type", "hidden"); authorizationInput.setAttribute("type", "hidden");
authorizationInput.setAttribute("id", "authorizationInput"); authorizationInput.setAttribute("id", "authorizationInput");
@ -118,6 +123,11 @@ export default class HtmlUtils
{ {
if(url.startsWith("data:")) 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"); const openInWindow = window.open("", "_blank");
openInWindow.document.write(`<html lang="en"> openInWindow.document.write(`<html lang="en">
<body style="margin: 0"> <body style="margin: 0">

View File

@ -49,6 +49,7 @@ module.exports = function (app)
app.use("/data/*", getRequestHandler()); app.use("/data/*", getRequestHandler());
app.use("/widget/*", getRequestHandler()); app.use("/widget/*", getRequestHandler());
app.use("/serverInfo", getRequestHandler()); app.use("/serverInfo", getRequestHandler());
app.use("/manageSession", getRequestHandler());
app.use("/processes", getRequestHandler()); app.use("/processes", getRequestHandler());
app.use("/reports", getRequestHandler()); app.use("/reports", getRequestHandler());
app.use("/images", getRequestHandler()); app.use("/images", getRequestHandler());