diff --git a/src/App.tsx b/src/App.tsx
index 4571a3ce2..3b05ebfd0 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -2,10 +2,8 @@ import { useQuery } from '@tanstack/react-query';
import { MarkdownText } from 'MarkdownTest';
import { fetchUserProfile } from 'api/requests';
import useSblAuth from 'api/useSblAuth';
-import FooterCfGovWrapper from 'components/FooterCfGovWrapper';
import { LoadingApp, LoadingContent } from 'components/Loading';
import ScrollToTop from 'components/ScrollToTop';
-import { Alert, PageHeader, SkipNav } from 'design-system-react';
import 'design-system-react/style.css';
import Error500 from 'pages/Error/Error500';
import { NotFound404 } from 'pages/Error/NotFound404';
@@ -25,25 +23,17 @@ import CreateProfileFormWAssoc from 'pages/ProfileForm/Step1Form/Step1Form';
import { SummaryRoutesList } from 'pages/Summary/SummaryRoutes';
import type { ReactElement } from 'react';
import { Suspense, lazy } from 'react';
-import {
- BrowserRouter,
- Navigate,
- Outlet,
- Route,
- Routes,
- useLocation,
-} from 'react-router-dom';
-import type { UserProfileType } from 'types/filingTypes';
+import { BrowserRouter, Route, Routes } from 'react-router-dom';
import getIsRoutingEnabled, {
setIsRoutingEnabled,
toggleRouting,
} from 'utils/getIsRoutingEnabled';
-import { useHeaderAuthLinks } from 'utils/useHeaderAuthLinks';
import ErrorFallback from 'ErrorFallback';
// eslint-disable-next-line import/no-extraneous-dependencies
import { ErrorBoundary } from 'react-error-boundary';
-import release from './constants/release.json';
+import BasicLayout from 'BasicLayout';
+import ProtectedRoute from 'ProtectedRoute';
const FilingHome = lazy(async () => import('pages/Filing/FilingHome'));
const ProfileForm = lazy(async () => import('pages/ProfileForm'));
@@ -73,129 +63,30 @@ if (import.meta.env.DEV) {
}
if (!isRoutingEnabled) console.warn('Routing is disabled!');
-function BasicLayout(): ReactElement {
- const headerLinks = [...useHeaderAuthLinks()];
- const location = useLocation();
- const auth = useSblAuth();
-
- if (auth.error) {
- const errorMessage = auth.error.message;
-
- // Authentication service down
- if (errorMessage.includes('Failed to fetch')) {
- if (!location.pathname.includes('/500')) {
- return (
-
- );
- }
- }
- // User's session has expired
- else if (
- errorMessage.includes("Session doesn't have required client") &&
- location.pathname !== '/'
- ) {
- void auth.onLogout();
- return <>>;
- }
- }
-
- return (
-
-
-
- {/* TODO: Move this component to the DSR for other teams' use */}
- {/* See: https://github.com/cfpb/design-system-react/issues/352 */}
-
-
-
-
-
- {/* Part of fix to the white space below the footer problem */}
-
-
- {release.version}
-
-
-
- );
-}
-
-interface ProtectedRouteProperties {
- isAnyAuthorizationLoading: boolean;
- isAuthenticated: boolean;
- isLoading: boolean;
- onLogin: () => Promise;
- UserProfile: UserProfileType | undefined;
- children: JSX.Element;
-}
-
-function ProtectedRoute({
- isAnyAuthorizationLoading,
- isAuthenticated,
- isLoading: isInitialAuthorizationLoading,
- onLogin,
- UserProfile,
- children,
-}: ProtectedRouteProperties): JSX.Element {
- const { pathname } = useLocation();
- const isProfileFormPath = pathname.includes('/profile/complete');
-
- if (!isRoutingEnabled) {
- return children;
- }
-
- if (!isInitialAuthorizationLoading && !isAuthenticated) {
- void onLogin();
- return <>>;
- }
-
- if (isAnyAuthorizationLoading) return ;
-
- if (!UserProfile) {
- throw new Error('User Profile does not exist');
- }
-
- const isUserAssociatedWithAnyInstitution =
- (UserProfile?.institutions?.length ?? 0) > 0;
- if (!isUserAssociatedWithAnyInstitution && !isProfileFormPath)
- return ;
- if (isProfileFormPath && isUserAssociatedWithAnyInstitution)
- return ;
- return children;
-}
-
export default function App(): ReactElement {
const auth = useSblAuth();
- const {
- isAuthenticated: userIsAuthenticated,
- isLoading: isAuthLoading,
- emailAddress,
- } = auth;
- const { isLoading: isFetchUserProfileLoading, data: UserProfile } = useQuery({
- queryKey: ['fetch-user-profile', emailAddress],
+ const { isLoading, data: UserProfile } = useQuery({
+ queryKey: ['fetch-user-profile', auth.emailAddress],
queryFn: async () => fetchUserProfile(auth),
- enabled: !!userIsAuthenticated,
+ enabled: !!auth?.isAuthenticated,
});
- const loadingStates = [isAuthLoading, isFetchUserProfileLoading];
- const isAnyAuthorizationLoading = loadingStates.some(Boolean);
const ProtectedRouteAuthorizations = {
- ...auth,
+ isLoading,
UserProfile,
- isAnyAuthorizationLoading,
};
+ // TODO: Future, use this to configure a logout countdown notice and confirmation
+ // React.useEffect(() => {
+ // auth.events.addAccessTokenExpiring(props => {
+ // console.log('Expiring:', props);
+ // });
+ // auth.events.addAccessTokenExpired(props => {
+ // console.log('Expired:', props);
+ // })
+ // }, []);
+
return (
diff --git a/src/BasicLayout.tsx b/src/BasicLayout.tsx
new file mode 100644
index 000000000..2f879be88
--- /dev/null
+++ b/src/BasicLayout.tsx
@@ -0,0 +1,65 @@
+import useSblAuth from 'api/useSblAuth';
+import FooterCfGovWrapper from 'components/FooterCfGovWrapper';
+import { Alert, PageHeader, SkipNav } from 'design-system-react';
+import type { ReactElement } from 'react';
+import { Navigate, Outlet, useLocation } from 'react-router-dom';
+import { useHeaderAuthLinks } from 'utils/useHeaderAuthLinks';
+
+import release from './constants/release.json';
+
+export default function BasicLayout(): ReactElement {
+ const headerLinks = [...useHeaderAuthLinks()];
+ const location = useLocation();
+ const auth = useSblAuth();
+
+ if (auth.error) {
+ const errorMessage = auth.error.message;
+
+ // Authentication service down
+ if (errorMessage.includes('Failed to fetch')) {
+ if (!location.pathname.includes('/500')) {
+ return (
+
+ );
+ }
+ }
+ // User's session has expired
+ else if (
+ errorMessage.includes("Session doesn't have required client") &&
+ location.pathname !== '/'
+ ) {
+ void auth.onLogout();
+ return <>>;
+ }
+ }
+
+ return (
+
+
+
+ {/* TODO: Move this component to the DSR for other teams' use */}
+ {/* See: https://github.com/cfpb/design-system-react/issues/352 */}
+
+
+
+
+
+ {/* Part of fix to the white space below the footer problem */}
+
+
+ {release.version}
+
+
+
+ );
+}
diff --git a/src/ProtectedRoute.tsx b/src/ProtectedRoute.tsx
new file mode 100644
index 000000000..022de7b96
--- /dev/null
+++ b/src/ProtectedRoute.tsx
@@ -0,0 +1,46 @@
+import useSblAuth from 'api/useSblAuth';
+import { LoadingContent } from 'components/Loading';
+import { Navigate, useLocation } from 'react-router-dom';
+import type { UserProfileType } from 'types/filingTypes';
+import getIsRoutingEnabled from 'utils/getIsRoutingEnabled';
+
+const isRoutingEnabled = getIsRoutingEnabled();
+
+interface ProtectedRouteProperties {
+ isLoading: boolean;
+ UserProfile: UserProfileType | undefined;
+ children: JSX.Element;
+}
+
+export default function ProtectedRoute({
+ isLoading,
+ UserProfile,
+ children,
+}: ProtectedRouteProperties): JSX.Element {
+ const auth = useSblAuth();
+ const { pathname } = useLocation();
+ const isProfileFormPath = pathname.includes('/profile/complete');
+
+ if (!isRoutingEnabled) {
+ return children;
+ }
+
+ if (!auth.isLoading && !auth.isAuthenticated) {
+ void auth.onLogin();
+ return <>>;
+ }
+
+ if (auth.isLoading || isLoading) return ;
+
+ if (!UserProfile) {
+ throw new Error('User Profile does not exist');
+ }
+
+ const isUserAssociatedWithAnyInstitution =
+ (UserProfile?.institutions?.length ?? 0) > 0;
+ if (!isUserAssociatedWithAnyInstitution && !isProfileFormPath)
+ return ;
+ if (isProfileFormPath && isUserAssociatedWithAnyInstitution)
+ return ;
+ return children;
+}
diff --git a/src/api/axiosService.ts b/src/api/axiosService.ts
index 72c709e18..eeb070ddb 100644
--- a/src/api/axiosService.ts
+++ b/src/api/axiosService.ts
@@ -16,14 +16,24 @@ export const getAxiosInstance = (baseUrl = ''): AxiosInstanceExtended => {
'Content-Type': 'application/json',
},
});
-
- newAxiosInstance.interceptors.request.use(response => {
- const token = getOidcTokenOutsideOfContext();
- if (!token) return response;
- response.headers.Authorization = token;
- axios.defaults.headers.common.Authorization = token;
- return response;
- });
+ newAxiosInstance.interceptors.request.use(
+ request => {
+ const token = getOidcTokenOutsideOfContext();
+ if (!token) return request;
+ request.headers.Authorization = token;
+ axios.defaults.headers.common.Authorization = token;
+ return request;
+ },
+ error => {
+ throw error;
+ },
+ );
+ newAxiosInstance.interceptors.response.use(
+ response => response,
+ error => {
+ throw error;
+ },
+ );
return newAxiosInstance;
};
@@ -73,5 +83,5 @@ export const request = async ({
// @ts-expect-error: A spread argument must either have a tuple type or be passed to a rest parameter.
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
const response = await axiosInstance[method](...argumentList);
- return response.data as unknown as T;
+ return response?.data as unknown as T;
};
diff --git a/src/api/useSblAuth.ts b/src/api/useSblAuth.ts
index 62a2d80e3..0bdca987a 100644
--- a/src/api/useSblAuth.ts
+++ b/src/api/useSblAuth.ts
@@ -1,8 +1,21 @@
import type { AuthContextProps } from 'react-oidc-context';
import { useAuth } from 'react-oidc-context';
-import { One } from 'utils/constants';
+import { One, Thousand, Three } from 'utils/constants';
import { LOGOUT_REDIRECT_URL } from './common';
+const tokenExpiresIn = (token: string | undefined): number | undefined => {
+ if (!token) return undefined;
+ const parts = token.split('.');
+ if (parts.length !== Three) return undefined;
+ const part = atob(parts[One]);
+ if (!part) return undefined;
+ const parsed = JSON.parse(part) as { exp: number };
+ if (!parsed) return undefined;
+ const expires = parsed.exp;
+ if (!expires) return undefined;
+ return (new Date(expires * Thousand).getTime() - Date.now()) / Thousand;
+};
+
export interface SblAuthProperties extends AuthContextProps {
onLogin: () => Promise;
onLogout: () => Promise;
@@ -33,6 +46,15 @@ const useSblAuth = (): SblAuthProperties => {
window.logout = onLogout;
}
+ if (!auth.isLoading) {
+ const accessUntil = tokenExpiresIn(auth?.user?.access_token);
+ const refreshUntil = tokenExpiresIn(auth?.user?.refresh_token);
+
+ if (accessUntil !== undefined && (!refreshUntil || refreshUntil < 0)) {
+ void onLogout();
+ }
+ }
+
return {
...auth,
onLogin,
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
index 77478204c..07930fc2c 100644
--- a/src/utils/constants.ts
+++ b/src/utils/constants.ts
@@ -4,6 +4,7 @@ export const Zero = 0;
export const NegativeOne = -1;
export const One = 1;
export const Two = 2;
+export const Three = 3;
export const MAX_RETRIES = 3;
export const UPLOAD_SUBMIT_MAX_RETRIES = 2;
export const Five = 5;