mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-17 12:50:43 +00:00
Add support for oauth2 authentication module.
In so doing, extract auth0- and anonymous- -authenticationModule implementations from index.tsx and App.tsx, moving each to it own useXyz hook.
This commit is contained in:
@ -36,6 +36,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",
|
"jwt-decode": "3.1.2",
|
||||||
|
"oidc-client-ts": "2.4.1",
|
||||||
|
"react-oidc-context": "2.3.1",
|
||||||
"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",
|
||||||
|
200
src/App.tsx
200
src/App.tsx
@ -19,7 +19,6 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {useAuth0} from "@auth0/auth0-react";
|
|
||||||
import {QException} from "@kingsrook/qqq-frontend-core/lib/exceptions/QException";
|
import {QException} from "@kingsrook/qqq-frontend-core/lib/exceptions/QException";
|
||||||
import {QAppNodeType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAppNodeType";
|
import {QAppNodeType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAppNodeType";
|
||||||
import {QAppTreeNode} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAppTreeNode";
|
import {QAppTreeNode} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAppTreeNode";
|
||||||
@ -34,11 +33,13 @@ 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 CommandMenu from "CommandMenu";
|
import CommandMenu from "CommandMenu";
|
||||||
import jwt_decode from "jwt-decode";
|
|
||||||
import QContext from "QContext";
|
import QContext from "QContext";
|
||||||
|
import useAnonymousAuthenticationModule from "qqq/authorization/anonymous/useAnonymousAuthenticationModule";
|
||||||
|
import useAuth0AuthenticationModule from "qqq/authorization/auth0/useAuth0AuthenticationModule";
|
||||||
|
import useOAuth2AuthenticationModule from "qqq/authorization/oauth2/useOAuth2AuthenticationModule";
|
||||||
import Sidenav from "qqq/components/horseshoe/sidenav/SideNav";
|
import Sidenav from "qqq/components/horseshoe/sidenav/SideNav";
|
||||||
import theme from "qqq/components/legacy/Theme";
|
import theme from "qqq/components/legacy/Theme";
|
||||||
import {setMiniSidenav, setOpenConfigurator, useMaterialUIController} from "qqq/context";
|
import {setMiniSidenav, useMaterialUIController} from "qqq/context";
|
||||||
import AppHome from "qqq/pages/apps/Home";
|
import AppHome from "qqq/pages/apps/Home";
|
||||||
import NoApps from "qqq/pages/apps/NoApps";
|
import NoApps from "qqq/pages/apps/NoApps";
|
||||||
import ProcessRun from "qqq/pages/processes/ProcessRun";
|
import ProcessRun from "qqq/pages/processes/ProcessRun";
|
||||||
@ -65,7 +66,6 @@ export const SESSION_UUID_COOKIE_NAME = "sessionUUID";
|
|||||||
export default function App()
|
export default function App()
|
||||||
{
|
{
|
||||||
const [cookies, setCookie, removeCookie] = useCookies([SESSION_UUID_COOKIE_NAME]);
|
const [cookies, setCookie, removeCookie] = useCookies([SESSION_UUID_COOKIE_NAME]);
|
||||||
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);
|
||||||
const [profileRoutes, setProfileRoutes] = useState({});
|
const [profileRoutes, setProfileRoutes] = useState({});
|
||||||
@ -74,68 +74,21 @@ export default function App()
|
|||||||
const [needLicenseKey, setNeedLicenseKey] = useState(true);
|
const [needLicenseKey, setNeedLicenseKey] = useState(true);
|
||||||
const [loggedInUser, setLoggedInUser] = useState({} as { name?: string, email?: string });
|
const [loggedInUser, setLoggedInUser] = useState({} as { name?: string, email?: string });
|
||||||
const [defaultRoute, setDefaultRoute] = useState("/no-apps");
|
const [defaultRoute, setDefaultRoute] = useState("/no-apps");
|
||||||
|
const [authenticationMetaData, setAuthenticationMetaData] = useState(null as QAuthenticationMetaData | null);
|
||||||
|
const [earlyReturnForAuth, setEarlyReturnForAuth] = useState(null as JSX.Element);
|
||||||
|
|
||||||
|
const {setupSession: auth0SetupSession, logout: auth0Logout} = useAuth0AuthenticationModule({setIsFullyAuthenticated, setLoggedInUser, setEarlyReturnForAuth});
|
||||||
|
const {setupSession: oauth2SetupSession, logout: oauth2Logout} = useOAuth2AuthenticationModule({setIsFullyAuthenticated, setLoggedInUser, setEarlyReturnForAuth});
|
||||||
|
const {setupSession: anonymousSetupSession, logout: anonymousLogout} = useAnonymousAuthenticationModule({setIsFullyAuthenticated, setLoggedInUser, setEarlyReturnForAuth});
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////
|
||||||
// tell the client how to do a logout if it sees a 401 //
|
// tell the client how to do a logout if it sees a 401 //
|
||||||
/////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////
|
||||||
Client.setUnauthorizedCallback(() =>
|
Client.setUnauthorizedCallback(() => doLogout());
|
||||||
{
|
|
||||||
logout();
|
|
||||||
});
|
|
||||||
|
|
||||||
const shouldStoreNewToken = (newToken: string, oldToken: string): boolean =>
|
|
||||||
{
|
|
||||||
if (!cookies[SESSION_UUID_COOKIE_NAME])
|
|
||||||
{
|
|
||||||
console.log("No session uuid cookie - so we should store a new one.");
|
|
||||||
return (true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!oldToken)
|
|
||||||
{
|
|
||||||
console.log("No accessToken in localStorage - so we should store a new one.");
|
|
||||||
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 - so we should store a new one.");
|
|
||||||
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 - so we should store a new one.");
|
|
||||||
}
|
|
||||||
return (different);
|
|
||||||
}
|
|
||||||
catch (e)
|
|
||||||
{
|
|
||||||
console.log("Caught in shouldStoreNewToken: " + e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (true);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////
|
||||||
|
// deal with making sure user is authenticated //
|
||||||
|
/////////////////////////////////////////////////
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
if (loadingToken)
|
if (loadingToken)
|
||||||
@ -147,64 +100,19 @@ export default function App()
|
|||||||
(async () =>
|
(async () =>
|
||||||
{
|
{
|
||||||
const authenticationMetaData: QAuthenticationMetaData = await qController.getAuthenticationMetaData();
|
const authenticationMetaData: QAuthenticationMetaData = await qController.getAuthenticationMetaData();
|
||||||
|
setAuthenticationMetaData(authenticationMetaData);
|
||||||
|
|
||||||
if (authenticationMetaData.type === "AUTH_0")
|
if (authenticationMetaData.type === "AUTH_0")
|
||||||
{
|
{
|
||||||
/////////////////////////////////////////
|
await auth0SetupSession();
|
||||||
// use auth0 if auth type is ... auth0 //
|
}
|
||||||
/////////////////////////////////////////
|
else if (authenticationMetaData.type === "OAUTH2")
|
||||||
try
|
{
|
||||||
{
|
await oauth2SetupSession();
|
||||||
console.log("Loading token from auth0...");
|
|
||||||
const accessToken = await getAccessTokenSilently();
|
|
||||||
|
|
||||||
const lsAccessToken = localStorage.getItem("accessToken");
|
|
||||||
if (shouldStoreNewToken(accessToken, lsAccessToken))
|
|
||||||
{
|
|
||||||
console.log("Sending accessToken to backend, requesting a sessionUUID...");
|
|
||||||
const {uuid: newSessionUuid, values} = await qController.manageSession(accessToken, null);
|
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// the request to the backend should send a header to set the cookie, so we don't need to do it ourselves. //
|
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// setCookie(SESSION_UUID_COOKIE_NAME, newSessionUuid, {path: "/"});
|
|
||||||
|
|
||||||
localStorage.setItem("accessToken", accessToken);
|
|
||||||
localStorage.setItem("sessionValues", JSON.stringify(values));
|
|
||||||
console.log("Got new sessionUUID from backend, and stored new accessToken");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
console.log("Using existing sessionUUID cookie");
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsFullyAuthenticated(true);
|
|
||||||
qController.setGotAuthentication();
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if (authenticationMetaData.type === "FULLY_ANONYMOUS" || authenticationMetaData.type === "MOCK")
|
else if (authenticationMetaData.type === "FULLY_ANONYMOUS" || authenticationMetaData.type === "MOCK")
|
||||||
{
|
{
|
||||||
/////////////////////////////////////////////
|
await anonymousSetupSession();
|
||||||
// use a random token if anonymous or mock //
|
|
||||||
/////////////////////////////////////////////
|
|
||||||
console.log("Generating random token...");
|
|
||||||
setIsFullyAuthenticated(true);
|
|
||||||
qController.setGotAuthentication();
|
|
||||||
setCookie(SESSION_UUID_COOKIE_NAME, Md5.hashStr(`${new Date()}`), {path: "/"});
|
|
||||||
console.log("Token generation complete.");
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -220,11 +128,34 @@ export default function App()
|
|||||||
(async () =>
|
(async () =>
|
||||||
{
|
{
|
||||||
const metaData: QInstance = await qController.loadMetaData();
|
const metaData: QInstance = await qController.loadMetaData();
|
||||||
LicenseInfo.setLicenseKey(metaData.environmentValues.get("MATERIAL_UI_LICENSE_KEY") || process.env.REACT_APP_MATERIAL_UI_LICENSE_KEY);
|
LicenseInfo.setLicenseKey(metaData.environmentValues?.get("MATERIAL_UI_LICENSE_KEY") || process.env.REACT_APP_MATERIAL_UI_LICENSE_KEY);
|
||||||
setNeedLicenseKey(false);
|
setNeedLicenseKey(false);
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
** call approprite logout function based on authentication meta data type
|
||||||
|
***************************************************************************/
|
||||||
|
function doLogout()
|
||||||
|
{
|
||||||
|
if (authenticationMetaData?.type === "AUTH_0")
|
||||||
|
{
|
||||||
|
auth0Logout();
|
||||||
|
}
|
||||||
|
else if (authenticationMetaData?.type === "OAUTH2")
|
||||||
|
{
|
||||||
|
oauth2Logout();
|
||||||
|
}
|
||||||
|
else if (authenticationMetaData?.type === "FULLY_ANONYMOUS" || authenticationMetaData?.type === "MOCK")
|
||||||
|
{
|
||||||
|
anonymousLogout();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
console.log(`No logout callback for authentication type [${authenticationMetaData?.type}].`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const [controller, dispatch] = useMaterialUIController();
|
const [controller, dispatch] = useMaterialUIController();
|
||||||
const {miniSidenav, direction, layout, openConfigurator, sidenavColor} = controller;
|
const {miniSidenav, direction, layout, openConfigurator, sidenavColor} = controller;
|
||||||
const [onMouseEnter, setOnMouseEnter] = useState(false);
|
const [onMouseEnter, setOnMouseEnter] = useState(false);
|
||||||
@ -592,10 +523,7 @@ export default function App()
|
|||||||
localStorage.removeItem("accessToken");
|
localStorage.removeItem("accessToken");
|
||||||
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
|
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
|
||||||
|
|
||||||
//////////////////////////////////////////////////////
|
doLogout();
|
||||||
// todo - this is auth0 logout... make more generic //
|
|
||||||
//////////////////////////////////////////////////////
|
|
||||||
logout();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -603,7 +531,9 @@ export default function App()
|
|||||||
})();
|
})();
|
||||||
}, [needToLoadRoutes, isFullyAuthenticated]);
|
}, [needToLoadRoutes, isFullyAuthenticated]);
|
||||||
|
|
||||||
// Open sidenav when mouse enter on mini sidenav
|
///////////////////////////////////////////////////
|
||||||
|
// Open sidenav when mouse enter on mini sidenav //
|
||||||
|
///////////////////////////////////////////////////
|
||||||
const handleOnMouseEnter = () =>
|
const handleOnMouseEnter = () =>
|
||||||
{
|
{
|
||||||
if (miniSidenav && !onMouseEnter)
|
if (miniSidenav && !onMouseEnter)
|
||||||
@ -613,7 +543,9 @@ export default function App()
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Close sidenav when mouse leave mini sidenav
|
/////////////////////////////////////////////////
|
||||||
|
// Close sidenav when mouse leave mini sidenav //
|
||||||
|
/////////////////////////////////////////////////
|
||||||
const handleOnMouseLeave = () =>
|
const handleOnMouseLeave = () =>
|
||||||
{
|
{
|
||||||
if (onMouseEnter)
|
if (onMouseEnter)
|
||||||
@ -623,16 +555,14 @@ export default function App()
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Change the openConfigurator state
|
|
||||||
const handleConfiguratorOpen = () => setOpenConfigurator(dispatch, !openConfigurator);
|
|
||||||
|
|
||||||
// Setting the dir attribute for the body element
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
document.body.setAttribute("dir", direction);
|
document.body.setAttribute("dir", direction);
|
||||||
}, [direction]);
|
}, [direction]);
|
||||||
|
|
||||||
// Setting page scroll to 0 when changing the route
|
//////////////////////////////////////////////////////
|
||||||
|
// Setting page scroll to 0 when changing the route //
|
||||||
|
//////////////////////////////////////////////////////
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
document.documentElement.scrollTop = 0;
|
document.documentElement.scrollTop = 0;
|
||||||
@ -672,14 +602,14 @@ export default function App()
|
|||||||
const [dotMenuOpen, setDotMenuOpen] = useState(false);
|
const [dotMenuOpen, setDotMenuOpen] = useState(false);
|
||||||
const [keyboardHelpOpen, setKeyboardHelpOpen] = useState(false);
|
const [keyboardHelpOpen, setKeyboardHelpOpen] = useState(false);
|
||||||
const [helpHelpActive] = useState(queryParams.has("helpHelp"));
|
const [helpHelpActive] = useState(queryParams.has("helpHelp"));
|
||||||
const [userId, setUserId] = useState(user?.email);
|
const [userId, setUserId] = useState(loggedInUser?.email);
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
setUserId(user?.email)
|
setUserId(loggedInUser?.email);
|
||||||
}, [user]);
|
}, [loggedInUser]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const [googleAnalyticsUtils] = useState(new GoogleAnalyticsUtils());
|
const [googleAnalyticsUtils] = useState(new GoogleAnalyticsUtils());
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
@ -687,7 +617,16 @@ export default function App()
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
function recordAnalytics(model: AnalyticsModel)
|
function recordAnalytics(model: AnalyticsModel)
|
||||||
{
|
{
|
||||||
googleAnalyticsUtils.recordAnalytics(model)
|
googleAnalyticsUtils.recordAnalytics(model);
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////
|
||||||
|
// if any of the auth/session setup code determined that we need //
|
||||||
|
// to render something and return early - then do so here. //
|
||||||
|
///////////////////////////////////////////////////////////////////
|
||||||
|
if (earlyReturnForAuth)
|
||||||
|
{
|
||||||
|
return (earlyReturnForAuth);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -727,6 +666,7 @@ export default function App()
|
|||||||
routes={sideNavRoutes}
|
routes={sideNavRoutes}
|
||||||
onMouseEnter={handleOnMouseEnter}
|
onMouseEnter={handleOnMouseEnter}
|
||||||
onMouseLeave={handleOnMouseLeave}
|
onMouseLeave={handleOnMouseLeave}
|
||||||
|
logout={doLogout}
|
||||||
/>
|
/>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="*" element={<Navigate to={defaultRoute} />} />
|
<Route path="*" element={<Navigate to={defaultRoute} />} />
|
||||||
|
149
src/index.tsx
149
src/index.tsx
@ -19,116 +19,97 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {Auth0Provider} from "@auth0/auth0-react";
|
|
||||||
import {QAuthenticationMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAuthenticationMetaData";
|
import {QAuthenticationMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAuthenticationMetaData";
|
||||||
import React from "react";
|
|
||||||
import {createRoot} from "react-dom/client";
|
|
||||||
import {BrowserRouter, useNavigate, useSearchParams} from "react-router-dom";
|
|
||||||
import App from "App";
|
import App from "App";
|
||||||
import "qqq/styles/qqq-override-styles.css";
|
import "qqq/styles/qqq-override-styles.css";
|
||||||
import "qqq/styles/globals.scss";
|
import "qqq/styles/globals.scss";
|
||||||
import "qqq/styles/raycast.scss";
|
import "qqq/styles/raycast.scss";
|
||||||
import HandleAuthorizationError from "HandleAuthorizationError";
|
import useAnonymousAuthenticationModule from "qqq/authorization/anonymous/useAnonymousAuthenticationModule";
|
||||||
import ProtectedRoute from "qqq/authorization/auth0/ProtectedRoute";
|
import useAuth0AuthenticationModule from "qqq/authorization/auth0/useAuth0AuthenticationModule";
|
||||||
|
import useOAuth2AuthenticationModule from "qqq/authorization/oauth2/useOAuth2AuthenticationModule";
|
||||||
import {MaterialUIControllerProvider} from "qqq/context";
|
import {MaterialUIControllerProvider} from "qqq/context";
|
||||||
import Client from "qqq/utils/qqq/Client";
|
import Client from "qqq/utils/qqq/Client";
|
||||||
|
import React from "react";
|
||||||
|
import {createRoot} from "react-dom/client";
|
||||||
|
import {BrowserRouter} from "react-router-dom";
|
||||||
|
|
||||||
|
|
||||||
const qController = Client.getInstance();
|
const qController = Client.getInstance();
|
||||||
|
|
||||||
if(document.location.search && document.location.search.indexOf("clearAuthenticationMetaDataLocalStorage") > -1)
|
if (document.location.search && document.location.search.indexOf("clearAuthenticationMetaDataLocalStorage") > -1)
|
||||||
{
|
{
|
||||||
qController.clearAuthenticationMetaDataLocalStorage()
|
qController.clearAuthenticationMetaDataLocalStorage();
|
||||||
}
|
}
|
||||||
|
|
||||||
const authenticationMetaDataPromise: Promise<QAuthenticationMetaData> = qController.getAuthenticationMetaData()
|
const authenticationMetaDataPromise: Promise<QAuthenticationMetaData> = qController.getAuthenticationMetaData();
|
||||||
|
|
||||||
authenticationMetaDataPromise.then((authenticationMetaData) =>
|
authenticationMetaDataPromise.then((authenticationMetaData) =>
|
||||||
{
|
{
|
||||||
// @ts-ignore
|
|
||||||
function Auth0ProviderWithRedirectCallback({children, ...props})
|
|
||||||
{
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const [searchParams] = useSearchParams();
|
|
||||||
|
|
||||||
// @ts-ignore
|
/***************************************************************************
|
||||||
const onRedirectCallback = (appState) =>
|
**
|
||||||
{
|
***************************************************************************/
|
||||||
navigate((appState && appState.returnTo) || window.location.pathname);
|
function Auth0RouterBody()
|
||||||
};
|
{
|
||||||
if (searchParams.get("error"))
|
const {renderAppWrapper} = useAuth0AuthenticationModule({});
|
||||||
{
|
return (renderAppWrapper(authenticationMetaData, null));
|
||||||
return (
|
|
||||||
// @ts-ignore
|
|
||||||
<Auth0Provider {...props}>
|
|
||||||
<HandleAuthorizationError errorMessage={searchParams.get("error_description")} />
|
|
||||||
</Auth0Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return (
|
|
||||||
// @ts-ignore
|
|
||||||
<Auth0Provider onRedirectCallback={onRedirectCallback} {...props}>
|
|
||||||
{children}
|
|
||||||
</Auth0Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
function OAuth2RouterBody()
|
||||||
|
{
|
||||||
|
const {renderAppWrapper} = useOAuth2AuthenticationModule({});
|
||||||
|
return (renderAppWrapper(authenticationMetaData, (
|
||||||
|
<MaterialUIControllerProvider>
|
||||||
|
<App />
|
||||||
|
</MaterialUIControllerProvider>
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
function AnonymousRouterBody()
|
||||||
|
{
|
||||||
|
const {renderAppWrapper} = useAnonymousAuthenticationModule({});
|
||||||
|
return (renderAppWrapper(authenticationMetaData, (
|
||||||
|
<MaterialUIControllerProvider>
|
||||||
|
<App />
|
||||||
|
</MaterialUIControllerProvider>
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const container = document.getElementById("root");
|
const container = document.getElementById("root");
|
||||||
const root = createRoot(container);
|
const root = createRoot(container);
|
||||||
|
|
||||||
if (authenticationMetaData.type === "AUTH_0")
|
if (authenticationMetaData.type === "AUTH_0")
|
||||||
{
|
{
|
||||||
// @ts-ignore
|
root.render(<BrowserRouter>
|
||||||
let domain: string = authenticationMetaData.data.baseUrl;
|
<Auth0RouterBody />
|
||||||
|
</BrowserRouter>);
|
||||||
// @ts-ignore
|
}
|
||||||
const clientId = authenticationMetaData.data.clientId;
|
else if (authenticationMetaData.type === "OAUTH2")
|
||||||
|
{
|
||||||
// @ts-ignore
|
root.render(<BrowserRouter>
|
||||||
const audience = authenticationMetaData.data.audience;
|
<OAuth2RouterBody />
|
||||||
|
</BrowserRouter>);
|
||||||
if(!domain || !clientId)
|
}
|
||||||
{
|
else if (authenticationMetaData.type === "FULLY_ANONYMOUS" || authenticationMetaData.type === "MOCK")
|
||||||
root.render(
|
{
|
||||||
<div>Error: AUTH0 authenticationMetaData is missing domain [{domain}] and/or clientId [{clientId}].</div>
|
root.render(<BrowserRouter>
|
||||||
);
|
<AnonymousRouterBody />
|
||||||
return;
|
</BrowserRouter>);
|
||||||
}
|
|
||||||
|
|
||||||
if(domain.endsWith("/"))
|
|
||||||
{
|
|
||||||
/////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// auth0 lib fails if we have a trailing slash. be a bit more graceful than that. //
|
|
||||||
/////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
domain = domain.replace(/\/$/, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
root.render(
|
|
||||||
<BrowserRouter>
|
|
||||||
<Auth0ProviderWithRedirectCallback
|
|
||||||
domain={domain}
|
|
||||||
clientId={clientId}
|
|
||||||
audience={audience}
|
|
||||||
redirectUri={`${window.location.origin}/`}
|
|
||||||
>
|
|
||||||
<MaterialUIControllerProvider>
|
|
||||||
<ProtectedRoute component={App} />
|
|
||||||
</MaterialUIControllerProvider>
|
|
||||||
</Auth0ProviderWithRedirectCallback>
|
|
||||||
</BrowserRouter>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
root.render(
|
root.render(<div>
|
||||||
<BrowserRouter>
|
Error: Unknown authenticationMetaData type: [{authenticationMetaData.type}].
|
||||||
<MaterialUIControllerProvider>
|
</div>);
|
||||||
<App />
|
|
||||||
</MaterialUIControllerProvider>
|
|
||||||
</BrowserRouter>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
});
|
||||||
|
@ -0,0 +1,82 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {QAuthenticationMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAuthenticationMetaData";
|
||||||
|
import {SESSION_UUID_COOKIE_NAME} from "App";
|
||||||
|
import Client from "qqq/utils/qqq/Client";
|
||||||
|
import {useCookies} from "react-cookie";
|
||||||
|
import {Md5} from "ts-md5/dist/md5";
|
||||||
|
|
||||||
|
const qController = Client.getInstance();
|
||||||
|
|
||||||
|
interface Props
|
||||||
|
{
|
||||||
|
setIsFullyAuthenticated?: (is: boolean) => void;
|
||||||
|
setLoggedInUser?: (user: any) => void;
|
||||||
|
setEarlyReturnForAuth?: (element: JSX.Element | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
** hook for working with the anonymous authentication module
|
||||||
|
***************************************************************************/
|
||||||
|
export default function useAnonymousAuthenticationModule({setIsFullyAuthenticated, setLoggedInUser, setEarlyReturnForAuth}: Props)
|
||||||
|
{
|
||||||
|
const [cookies, setCookie, removeCookie] = useCookies([SESSION_UUID_COOKIE_NAME]);
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
const setupSession = async () =>
|
||||||
|
{
|
||||||
|
console.log("Generating random token...");
|
||||||
|
setIsFullyAuthenticated(true);
|
||||||
|
qController.setGotAuthentication();
|
||||||
|
setCookie(SESSION_UUID_COOKIE_NAME, Md5.hashStr(`${new Date()}`), {path: "/"});
|
||||||
|
console.log("Token generation complete.");
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
const logout = () =>
|
||||||
|
{
|
||||||
|
qController.clearAuthenticationMetaDataLocalStorage();
|
||||||
|
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
const renderAppWrapper = (authenticationMetaData: QAuthenticationMetaData, children: JSX.Element): JSX.Element =>
|
||||||
|
{
|
||||||
|
return children;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return {
|
||||||
|
setupSession,
|
||||||
|
logout,
|
||||||
|
renderAppWrapper
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
248
src/qqq/authorization/auth0/useAuth0AuthenticationModule.tsx
Normal file
248
src/qqq/authorization/auth0/useAuth0AuthenticationModule.tsx
Normal file
@ -0,0 +1,248 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
import {Auth0Provider, useAuth0} from "@auth0/auth0-react";
|
||||||
|
import {QAuthenticationMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAuthenticationMetaData";
|
||||||
|
import App, {SESSION_UUID_COOKIE_NAME} from "App";
|
||||||
|
import HandleAuthorizationError from "HandleAuthorizationError";
|
||||||
|
import jwt_decode from "jwt-decode";
|
||||||
|
import ProtectedRoute from "qqq/authorization/auth0/ProtectedRoute";
|
||||||
|
import {MaterialUIControllerProvider} from "qqq/context";
|
||||||
|
import Client from "qqq/utils/qqq/Client";
|
||||||
|
import {useCookies} from "react-cookie";
|
||||||
|
import {useNavigate, useSearchParams} from "react-router-dom";
|
||||||
|
|
||||||
|
const qController = Client.getInstance();
|
||||||
|
|
||||||
|
interface Props
|
||||||
|
{
|
||||||
|
setIsFullyAuthenticated?: (is: boolean) => void;
|
||||||
|
setLoggedInUser?: (user: any) => void;
|
||||||
|
setEarlyReturnForAuth?: (element: JSX.Element | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
** hook for working with the Auth0 authentication module
|
||||||
|
***************************************************************************/
|
||||||
|
export default function useAuth0AuthenticationModule({setIsFullyAuthenticated, setLoggedInUser, setEarlyReturnForAuth}: Props)
|
||||||
|
{
|
||||||
|
const {user: auth0User, getAccessTokenSilently: auth0GetAccessTokenSilently, logout: useAuth0Logout} = useAuth0();
|
||||||
|
|
||||||
|
const [cookies, setCookie, removeCookie] = useCookies([SESSION_UUID_COOKIE_NAME]);
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
const shouldStoreNewToken = (newToken: string, oldToken: string): boolean =>
|
||||||
|
{
|
||||||
|
if (!cookies[SESSION_UUID_COOKIE_NAME])
|
||||||
|
{
|
||||||
|
console.log("No session uuid cookie - so we should store a new one.");
|
||||||
|
return (true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!oldToken)
|
||||||
|
{
|
||||||
|
console.log("No accessToken in localStorage - so we should store a new one.");
|
||||||
|
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 - so we should store a new one.");
|
||||||
|
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 - so we should store a new one.");
|
||||||
|
}
|
||||||
|
return (different);
|
||||||
|
}
|
||||||
|
catch (e)
|
||||||
|
{
|
||||||
|
console.log("Caught in shouldStoreNewToken: " + e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (true);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
const setupSession = async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
console.log("Loading token from auth0...");
|
||||||
|
const accessToken = await auth0GetAccessTokenSilently();
|
||||||
|
|
||||||
|
const lsAccessToken = localStorage.getItem("accessToken");
|
||||||
|
if (shouldStoreNewToken(accessToken, lsAccessToken))
|
||||||
|
{
|
||||||
|
console.log("Sending accessToken to backend, requesting a sessionUUID...");
|
||||||
|
const {uuid: newSessionUuid, values} = await qController.manageSession(accessToken, null);
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// the request to the backend should send a header to set the cookie, so we don't need to do it ourselves. //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// setCookie(SESSION_UUID_COOKIE_NAME, newSessionUuid, {path: "/"});
|
||||||
|
|
||||||
|
localStorage.setItem("accessToken", accessToken);
|
||||||
|
localStorage.setItem("sessionValues", JSON.stringify(values));
|
||||||
|
console.log("Got new sessionUUID from backend, and stored new accessToken");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
console.log("Using existing sessionUUID cookie");
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsFullyAuthenticated(true);
|
||||||
|
qController.setGotAuthentication();
|
||||||
|
|
||||||
|
setLoggedInUser(auth0User);
|
||||||
|
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: "/"});
|
||||||
|
useAuth0Logout();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
const logout = () =>
|
||||||
|
{
|
||||||
|
useAuth0Logout({returnTo: window.location.origin});
|
||||||
|
};
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
// @ts-ignore
|
||||||
|
function Auth0ProviderWithRedirectCallback({children, ...props})
|
||||||
|
{
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const onRedirectCallback = (appState) =>
|
||||||
|
{
|
||||||
|
navigate((appState && appState.returnTo) || window.location.pathname);
|
||||||
|
};
|
||||||
|
if (searchParams.get("error"))
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
// @ts-ignore
|
||||||
|
<Auth0Provider {...props}>
|
||||||
|
<HandleAuthorizationError errorMessage={searchParams.get("error_description")} />
|
||||||
|
</Auth0Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
// @ts-ignore
|
||||||
|
<Auth0Provider onRedirectCallback={onRedirectCallback} {...props}>
|
||||||
|
{children}
|
||||||
|
</Auth0Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
const renderAppWrapper = (authenticationMetaData: QAuthenticationMetaData, children: JSX.Element): JSX.Element =>
|
||||||
|
{
|
||||||
|
// @ts-ignore
|
||||||
|
let domain: string = authenticationMetaData.data.baseUrl;
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const clientId = authenticationMetaData.data.clientId;
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const audience = authenticationMetaData.data.audience;
|
||||||
|
|
||||||
|
if (!domain || !clientId)
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
<div>Error: AUTH0 authenticationMetaData is missing baseUrl [{domain}] and/or clientId [{clientId}].</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (domain.endsWith("/"))
|
||||||
|
{
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// auth0 lib fails if we have a trailing slash. be a bit more graceful than that. //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
domain = domain.replace(/\/$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Auth0ProviderWithRedirectCallback
|
||||||
|
domain={domain}
|
||||||
|
clientId={clientId}
|
||||||
|
audience={audience}
|
||||||
|
redirectUri={`${window.location.origin}/`}>
|
||||||
|
<MaterialUIControllerProvider>
|
||||||
|
<ProtectedRoute component={App} />
|
||||||
|
</MaterialUIControllerProvider>
|
||||||
|
</Auth0ProviderWithRedirectCallback>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return {
|
||||||
|
setupSession,
|
||||||
|
logout,
|
||||||
|
renderAppWrapper
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
195
src/qqq/authorization/oauth2/useOAuth2AuthenticationModule.tsx
Normal file
195
src/qqq/authorization/oauth2/useOAuth2AuthenticationModule.tsx
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {QAuthenticationMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAuthenticationMetaData";
|
||||||
|
import {SESSION_UUID_COOKIE_NAME} from "App";
|
||||||
|
import Client from "qqq/utils/qqq/Client";
|
||||||
|
import {useCookies} from "react-cookie";
|
||||||
|
import {AuthProvider, useAuth} from "react-oidc-context";
|
||||||
|
import {useNavigate, useSearchParams} from "react-router-dom";
|
||||||
|
|
||||||
|
const qController = Client.getInstance();
|
||||||
|
|
||||||
|
interface Props
|
||||||
|
{
|
||||||
|
setIsFullyAuthenticated?: (is: boolean) => void;
|
||||||
|
setLoggedInUser?: (user: any) => void;
|
||||||
|
setEarlyReturnForAuth?: (element: JSX.Element | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
** hook for working with the OAuth2 authentication module
|
||||||
|
***************************************************************************/
|
||||||
|
export default function useOAuth2AuthenticationModule({setIsFullyAuthenticated, setLoggedInUser, setEarlyReturnForAuth}: Props)
|
||||||
|
{
|
||||||
|
const authOidc = useAuth();
|
||||||
|
|
||||||
|
const [cookies, setCookie, removeCookie] = useCookies([SESSION_UUID_COOKIE_NAME]);
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
const setupSession = async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const preSigninRedirectPathnameKey = "oauth2.preSigninRedirect.pathname";
|
||||||
|
if (window.location.pathname == "/token")
|
||||||
|
{
|
||||||
|
const code = searchParams.get("code");
|
||||||
|
const state = searchParams.get("state");
|
||||||
|
const oidcString = localStorage.getItem(`oidc.${state}`);
|
||||||
|
if (oidcString)
|
||||||
|
{
|
||||||
|
const oidcObject = JSON.parse(oidcString) as { [name: string]: any };
|
||||||
|
console.log(oidcObject);
|
||||||
|
const manageSessionRequestBody = {code: code, codeVerifier: oidcObject.code_verifier, redirectUri: oidcObject.redirect_uri};
|
||||||
|
const {uuid: newSessionUuid, values} = await qController.manageSession(null, null, manageSessionRequestBody);
|
||||||
|
console.log(`we have new session UUID: ${newSessionUuid}`);
|
||||||
|
|
||||||
|
setIsFullyAuthenticated(true);
|
||||||
|
qController.setGotAuthentication();
|
||||||
|
|
||||||
|
setLoggedInUser(values?.user);
|
||||||
|
console.log("Token load complete.");
|
||||||
|
|
||||||
|
const preSigninRedirectPathname = localStorage.getItem(preSigninRedirectPathnameKey);
|
||||||
|
localStorage.removeItem(preSigninRedirectPathname);
|
||||||
|
navigate(preSigninRedirectPathname ?? "/", {replace: true});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const sessionUuid = cookies[SESSION_UUID_COOKIE_NAME];
|
||||||
|
if (sessionUuid)
|
||||||
|
{
|
||||||
|
console.log(`we have session UUID: ${sessionUuid} - validating it...`);
|
||||||
|
const {uuid: newSessionUuid, values} = await qController.manageSession(null, sessionUuid, null);
|
||||||
|
|
||||||
|
setIsFullyAuthenticated(true);
|
||||||
|
qController.setGotAuthentication();
|
||||||
|
|
||||||
|
setLoggedInUser(values?.user);
|
||||||
|
console.log("Token load complete.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
console.log("Loading token from OAuth2 provider...");
|
||||||
|
console.log(authOidc);
|
||||||
|
localStorage.setItem(preSigninRedirectPathnameKey, window.location.pathname);
|
||||||
|
setEarlyReturnForAuth(<div>Signing in...</div>);
|
||||||
|
authOidc.signinRedirect();
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// this is what's in the docs, but, it sure doesn't seem to ever hit any case other than the signinRedirect block //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
/*
|
||||||
|
if (authOidc.isLoading)
|
||||||
|
{
|
||||||
|
setLoadingToken(false); //? so we can come back in? but i'm missing something here.
|
||||||
|
setEarlyReturnForAuth(<div>
|
||||||
|
<div>Loading...</div>
|
||||||
|
<button onClick={() => incrementCheckLoadingCounter()}>check again?</button>
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
else if (authOidc.error)
|
||||||
|
{
|
||||||
|
setEarlyReturnForAuth(<div>Error: {authOidc.error.message}</div>);
|
||||||
|
}
|
||||||
|
else if (authOidc.isAuthenticated)
|
||||||
|
{
|
||||||
|
setEarlyReturnForAuth(
|
||||||
|
<div>
|
||||||
|
Welcome, {authOidc.user?.profile.name}!
|
||||||
|
<button onClick={() => authOidc.signoutRedirect()}>Log out</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
localStorage.setItem(preSigninRedirectPathnameKey, window.location.pathname);
|
||||||
|
setEarlyReturnForAuth(<div>Signing in...</div>);
|
||||||
|
authOidc.signinRedirect();
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e)
|
||||||
|
{
|
||||||
|
console.log(`Error loading token: ${JSON.stringify(e)}`);
|
||||||
|
logout();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
const logout = () =>
|
||||||
|
{
|
||||||
|
qController.clearAuthenticationMetaDataLocalStorage();
|
||||||
|
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
|
||||||
|
authOidc.signoutRedirect();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
const renderAppWrapper = (authenticationMetaData: QAuthenticationMetaData, children: JSX.Element): JSX.Element =>
|
||||||
|
{
|
||||||
|
const authority: string = authenticationMetaData.data.baseUrl;
|
||||||
|
const clientId = authenticationMetaData.data.clientId;
|
||||||
|
|
||||||
|
if (!authority || !clientId)
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
<div>Error: OAuth2 authenticationMetaData is missing baseUrl [{authority}] and/or clientId [{clientId}].</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const oidcConfig =
|
||||||
|
{
|
||||||
|
authority: authority,
|
||||||
|
client_id: clientId,
|
||||||
|
redirect_uri: `${window.location.origin}/token`,
|
||||||
|
response_type: "code",
|
||||||
|
scope: "openid profile email offline_access",
|
||||||
|
};
|
||||||
|
|
||||||
|
return (<AuthProvider {...oidcConfig}>
|
||||||
|
{children}
|
||||||
|
</AuthProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return {
|
||||||
|
setupSession,
|
||||||
|
logout,
|
||||||
|
renderAppWrapper
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
@ -1,38 +0,0 @@
|
|||||||
/*
|
|
||||||
* QQQ - Low-code Application Framework for Engineers.
|
|
||||||
* Copyright (C) 2021-2022. Kingsrook, LLC
|
|
||||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
|
||||||
* contact@kingsrook.com
|
|
||||||
* https://github.com/Kingsrook/
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as
|
|
||||||
* published by the Free Software Foundation, either version 3 of the
|
|
||||||
* License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {useAuth0} from "@auth0/auth0-react";
|
|
||||||
import {Button} from "@mui/material";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
function AuthenticationButton()
|
|
||||||
{
|
|
||||||
const {loginWithRedirect, logout, isAuthenticated} = useAuth0();
|
|
||||||
|
|
||||||
if (isAuthenticated)
|
|
||||||
{
|
|
||||||
return <Button onClick={() => logout({returnTo: window.location.origin})}>Log Out</Button>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Button onClick={() => loginWithRedirect()}>Log In</Button>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AuthenticationButton;
|
|
@ -20,14 +20,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {QBrandingMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QBrandingMetaData";
|
import {QBrandingMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QBrandingMetaData";
|
||||||
|
import {Button} from "@mui/material";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Divider from "@mui/material/Divider";
|
import Divider from "@mui/material/Divider";
|
||||||
import Icon from "@mui/material/Icon";
|
import Icon from "@mui/material/Icon";
|
||||||
import Link from "@mui/material/Link";
|
import Link from "@mui/material/Link";
|
||||||
import List from "@mui/material/List";
|
import List from "@mui/material/List";
|
||||||
import {ReactNode, useEffect, useReducer, useState} from "react";
|
|
||||||
import {NavLink, useLocation} from "react-router-dom";
|
|
||||||
import AuthenticationButton from "qqq/components/buttons/AuthenticationButton";
|
|
||||||
import SideNavCollapse from "qqq/components/horseshoe/sidenav/SideNavCollapse";
|
import SideNavCollapse from "qqq/components/horseshoe/sidenav/SideNavCollapse";
|
||||||
import SideNavItem from "qqq/components/horseshoe/sidenav/SideNavItem";
|
import SideNavItem from "qqq/components/horseshoe/sidenav/SideNavItem";
|
||||||
import SideNavList from "qqq/components/horseshoe/sidenav/SideNavList";
|
import SideNavList from "qqq/components/horseshoe/sidenav/SideNavList";
|
||||||
@ -35,6 +33,8 @@ import SidenavRoot from "qqq/components/horseshoe/sidenav/SideNavRoot";
|
|||||||
import sidenavLogoLabel from "qqq/components/horseshoe/sidenav/styles/SideNav";
|
import sidenavLogoLabel from "qqq/components/horseshoe/sidenav/styles/SideNav";
|
||||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||||
import {setMiniSidenav, setTransparentSidenav, setWhiteSidenav, useMaterialUIController,} from "qqq/context";
|
import {setMiniSidenav, setTransparentSidenav, setWhiteSidenav, useMaterialUIController,} from "qqq/context";
|
||||||
|
import {ReactNode, useEffect, useReducer, useState} from "react";
|
||||||
|
import {NavLink, useLocation} from "react-router-dom";
|
||||||
|
|
||||||
|
|
||||||
interface Props
|
interface Props
|
||||||
@ -44,6 +44,7 @@ interface Props
|
|||||||
logo?: string;
|
logo?: string;
|
||||||
appName?: string;
|
appName?: string;
|
||||||
branding?: QBrandingMetaData;
|
branding?: QBrandingMetaData;
|
||||||
|
logout: () => void;
|
||||||
routes: {
|
routes: {
|
||||||
[key: string]:
|
[key: string]:
|
||||||
| ReactNode
|
| ReactNode
|
||||||
@ -66,7 +67,7 @@ interface Props
|
|||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Sidenav({color, icon, logo, appName, branding, routes, ...rest}: Props): JSX.Element
|
function Sidenav({color, icon, logo, appName, branding, routes, logout, ...rest}: Props): JSX.Element
|
||||||
{
|
{
|
||||||
const [openCollapse, setOpenCollapse] = useState<boolean | string>(false);
|
const [openCollapse, setOpenCollapse] = useState<boolean | string>(false);
|
||||||
const [openNestedCollapse, setOpenNestedCollapse] = useState<boolean | string>(false);
|
const [openNestedCollapse, setOpenNestedCollapse] = useState<boolean | string>(false);
|
||||||
@ -257,7 +258,7 @@ function Sidenav({color, icon, logo, appName, branding, routes, ...rest}: Props)
|
|||||||
active={key === collapseName}
|
active={key === collapseName}
|
||||||
open={openCollapse === key}
|
open={openCollapse === key}
|
||||||
noCollapse={noCollapse}
|
noCollapse={noCollapse}
|
||||||
onClick={() => (! noCollapse ? (openCollapse === key ? setOpenCollapse(false) : setOpenCollapse(key)) : null) }
|
onClick={() => (!noCollapse ? (openCollapse === key ? setOpenCollapse(false) : setOpenCollapse(key)) : null)}
|
||||||
>
|
>
|
||||||
{collapse ? renderCollapse(collapse) : null}
|
{collapse ? renderCollapse(collapse) : null}
|
||||||
</SideNavCollapse>
|
</SideNavCollapse>
|
||||||
@ -350,7 +351,7 @@ function Sidenav({color, icon, logo, appName, branding, routes, ...rest}: Props)
|
|||||||
(darkMode && !transparentSidenav && whiteSidenav)
|
(darkMode && !transparentSidenav && whiteSidenav)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<AuthenticationButton />
|
<Button onClick={logout}>Log Out</Button>
|
||||||
</SidenavRoot>
|
</SidenavRoot>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -102,7 +102,7 @@ export default class GoogleAnalyticsUtils
|
|||||||
console.log("Error reading session values from localStorage: " + e);
|
console.log("Error reading session values from localStorage: " + e);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.metaData.environmentValues.get("GOOGLE_ANALYTICS_ENABLED") == "true" && this.metaData.environmentValues.get("GOOGLE_ANALYTICS_TRACKING_ID"))
|
if (this.metaData.environmentValues?.get("GOOGLE_ANALYTICS_ENABLED") == "true" && this.metaData.environmentValues?.get("GOOGLE_ANALYTICS_TRACKING_ID"))
|
||||||
{
|
{
|
||||||
this.active = true;
|
this.active = true;
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user