Compare commits

..

12 Commits

Author SHA1 Message Date
7ea50dd7bb Merge branch 'main' into feature/CE-609-infrastructure-remove-permissions-from-header
# Conflicts:
#	package.json
2023-08-17 11:42:51 -05:00
53d5bc58c1 updated snapshot 2023-08-17 10:18:38 -05:00
eac166b877 circle ci config update 2023-08-17 10:06:44 -05:00
f49ac38e24 changed circle ci to only deploy main automatically 2023-08-17 09:53:57 -05:00
28bdfc19e8 rebuilt package-lock 2023-08-17 09:47:24 -05:00
fa076733fb CE-567 Update version of webdrivermanager, because apparently you have to do that sometimes 2023-08-16 11:46:20 -05:00
8bebef1abe CE-567 Pass table name to init (for table-generic processes that might want it) 2023-08-15 19:44:58 -05:00
37fa578a59 CE-567 show more lines of commit messages 2023-08-15 19:44:13 -05:00
b6b7d8d8b3 CE-609 - Removed DNDTest WIP module 2023-08-15 09:25:45 -05:00
7bf515554d CE-609 - staged-rollout-ready - keeping the auth header, but also setting sessionUUID cookie; placeholder for quick-rollback; added todo#authHeader comments to mark where follow-up needs to happen after happy with new code 2023-08-15 09:08:44 -05:00
069cbf52e1 Merge pull request #28 from Kingsrook/feature/CE-607-mvp-of-transportation-plan-record
CE-607 Support fields from an exposed-join on a view screen.
2023-08-09 12:28:16 -05:00
7fa42a6eb5 Initial WIP Checkpoint of auth0 userSessions 2023-08-09 09:48:22 -05:00
12 changed files with 1152 additions and 1111 deletions

View File

@ -115,7 +115,7 @@ workflows:
context: [ qqq-maven-registry-credentials, kingsrook-slack, build-qqq-sample-app ]
filters:
branches:
ignore: /dev/
ignore: /main/
tags:
ignore: /(version|snapshot)-.*/
deploy:
@ -124,7 +124,7 @@ workflows:
context: [ qqq-maven-registry-credentials, kingsrook-slack, build-qqq-sample-app ]
filters:
branches:
only: /dev/
only: /main/
tags:
only: /(version|snapshot)-.*/

2101
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,7 @@
"@auth0/auth0-react": "1.10.2",
"@emotion/react": "11.7.1",
"@emotion/styled": "11.6.0",
"@kingsrook/qqq-frontend-core": "1.0.80",
"@kingsrook/qqq-frontend-core": "1.0.81",
"@mui/icons-material": "5.4.1",
"@mui/material": "5.11.1",
"@mui/styles": "5.11.1",

View File

@ -29,7 +29,7 @@
<packaging>jar</packaging>
<properties>
<revision>0.18.0-SNAPSHOT</revision>
<revision>0.19.0-SNAPSHOT</revision>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
@ -83,7 +83,7 @@
<dependency>
<groupId>io.github.bonigarcia</groupId>
<artifactId>webdrivermanager</artifactId>
<version>5.3.1</version>
<version>5.4.1</version>
<scope>test</scope>
</dependency>
<dependency>

View File

@ -33,7 +33,7 @@ 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 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 +57,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 +69,67 @@ 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 decodeJWT = (jwt: string): any =>
{
const base64Url = jwt.split(".")[1];
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
const jsonPayload = decodeURIComponent(window.atob(base64).split("").map(function (c)
{
return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2);
}).join(""));
return JSON.parse(jsonPayload);
};
const shouldStoreNewToken = (newToken: string, oldToken: string): boolean =>
{
if (!oldToken)
{
return (true);
}
try
{
const oldJSON = decodeJWT(oldToken);
const newJSON = decodeJWT(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 +151,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 +193,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 +226,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 +344,14 @@ export default function App()
name: `${app.label}`,
key: app.name,
route: path,
component: <RecordQuery table={table} key={table.name}/>,
component: <RecordQuery table={table} key={table.name} />,
});
routeList.push({
name: `${app.label}`,
key: app.name,
route: `${path}/savedFilter/:id`,
component: <RecordQuery table={table} key={table.name}/>,
component: <RecordQuery table={table} key={table.name} />,
});
routeList.push({
@ -429,11 +506,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: <Avatar src={profilePicture} alt="{user?.name}" />,
@ -469,7 +546,7 @@ export default function App()
}
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];
pathToLabelMap[route.route] = route.name;
@ -495,7 +572,10 @@ export default function App()
{
if ((e as QException).status === "401")
{
console.log("Exception is a QException with status = 401. Clearing some of localStorage & cookies");
qController.clearAuthenticationMetaDataLocalStorage();
localStorage.removeItem("accessToken")
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
//////////////////////////////////////////////////////
// todo - this is auth0 logout... make more generic //
@ -596,7 +676,7 @@ export default function App()
}}>
<ThemeProvider theme={theme}>
<CssBaseline />
<CommandMenu metaData={metaData}/>
<CommandMenu metaData={metaData} />
<Sidenav
color={sidenavColor}
icon={branding.icon}

View File

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

View File

@ -216,7 +216,7 @@ export default function DataBagViewer({dataBagId}: Props): JSX.Element
primaryTypographyProps={{fontSize: "1rem"}}
secondaryTypographyProps={{fontSize: ".85rem"}}
primary={
<div style={{whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis"}} title={version.values.get("commitMessage")}>
<div style={{overflow: "hidden", textOverflow: "ellipsis", maxHeight: "5rem"}} title={version.values.get("commitMessage")}>
{currentVersionId == version?.values?.get("id") && <Chip label="CURRENT" color="success" variant="outlined" size="small" sx={{mr: 1, fontSize: "0.75rem"}} />}
{version.values.get("commitMessage")}
</div>

View File

@ -314,7 +314,7 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc
primaryTypographyProps={{fontSize: "1rem"}}
secondaryTypographyProps={{fontSize: ".85rem"}}
primary={
<div style={{whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis"}} title={version.values.get("commitMessage")}>
<div style={{overflow: "hidden", textOverflow: "ellipsis", maxHeight: "5rem"}} title={version.values.get("commitMessage")}>
{scriptRecord.values.get("currentScriptRevisionId") == version?.values?.get("id") && <Chip label="CURRENT" color="success" variant="outlined" size="small" sx={{mr: 1, fontSize: "0.75rem"}} />}
{version.values.get("commitMessage")}
</div>

View File

@ -221,12 +221,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
@ -1147,6 +1154,11 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
}
}
if(tableMetaData)
{
queryStringPairsForInit.push(`tableName=${tableMetaData.name}`)
}
try
{
const processResponse = await Client.getInstance().processInit(processName, queryStringPairsForInit.join("&"));

View File

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

View File

@ -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(`<html lang="en">
<body style="margin: 0">

View File

@ -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());