diff --git a/portal-frontend/src/App.js b/portal-frontend/src/App.js index 247597a..591bb21 100644 --- a/portal-frontend/src/App.js +++ b/portal-frontend/src/App.js @@ -9,6 +9,7 @@ import ApplicationStatusPage from "./pages/ApplicationStatusPage"; import "./App.css"; import { FullPageLoadingSpinner } from "./components/FullPageLoadingSpinner"; import NotFound from "./components/404page"; +import { useInitializeAuth } from "./utils/authUtils"; const DynamicFormPage = () => { const params = new URLSearchParams(window.location.search); @@ -23,6 +24,8 @@ const DynamicFormPage = () => { function App() { const { isLoading } = useAuth0(); + useInitializeAuth(); + if (isLoading) { return ; } diff --git a/portal-frontend/src/components/WhiteBackground.js b/portal-frontend/src/components/WhiteBackground.js index aa7595e..58e905f 100644 --- a/portal-frontend/src/components/WhiteBackground.js +++ b/portal-frontend/src/components/WhiteBackground.js @@ -29,7 +29,7 @@ export const WhiteBackground = () => { }); return ( -
+
- , + ); diff --git a/portal-frontend/src/pages/ApplicationStatusPage.js b/portal-frontend/src/pages/ApplicationStatusPage.js index 36059a7..3bfd932 100644 --- a/portal-frontend/src/pages/ApplicationStatusPage.js +++ b/portal-frontend/src/pages/ApplicationStatusPage.js @@ -12,6 +12,7 @@ import { QRCodeSVG } from "qrcode.react"; import Button from "../components/Button"; import axios from "axios"; import { useNavigate } from "react-router-dom"; +import { createGetAuthToken } from "../utils/authUtils"; // defaulting this, but only shd happen if page is navigated to directly (no query args) const DEFAULT_FORM_KEY = "2025-cfg-application"; @@ -87,7 +88,11 @@ const ApplicationStatusPage = () => { const checkIfSubmitted = async () => { try { setLoading(true); - const token = await getAccessTokenSilently(); + const getAuthToken = createGetAuthToken(getAccessTokenSilently, setError); + const token = await getAuthToken(); + if (!token) { + return; + } const response = await axios.get( `${process.env.REACT_APP_BACKEND_URL}/application`, { diff --git a/portal-frontend/src/pages/FormPage.js b/portal-frontend/src/pages/FormPage.js index 0dce8a1..d40a264 100644 --- a/portal-frontend/src/pages/FormPage.js +++ b/portal-frontend/src/pages/FormPage.js @@ -9,6 +9,7 @@ import { useNavigate } from "react-router-dom"; import { FullPageLoadingSpinner } from "../components/FullPageLoadingSpinner"; import Modal, { ModalHeader } from "../components/Modal"; import { getFormByKey } from "../forms/forms"; +import { createGetAuthToken } from "../utils/authUtils"; const FormPage = ({ formKey }) => { const { getAccessTokenSilently } = useAuth0(); @@ -24,21 +25,6 @@ const FormPage = ({ formKey }) => { const formDefinition = getFormByKey(formKey); - const getAuthToken = async () => { - try { - return await getAccessTokenSilently(); - } catch (tokenError) { - if (tokenError.message.includes("Missing Refresh Token")) { - openModal(); - setError( - "Session expired. Please try logging out and logging back in." - ); - return null; - } - throw tokenError; - } - }; - const prepareFormData = (data) => { const formData = new FormData(); formData.append("form_key", formKey); @@ -76,6 +62,7 @@ const FormPage = ({ formKey }) => { setError(null); try { + const getAuthToken = createGetAuthToken(getAccessTokenSilently, setError); const token = await getAuthToken(); if (!token) { return; @@ -132,7 +119,14 @@ const FormPage = ({ formKey }) => { try { setLoading(true); - const token = await getAccessTokenSilently(); + const getAuthToken = createGetAuthToken( + getAccessTokenSilently, + setError + ); + const token = await getAuthToken(); + if (!token) { + return; + } const isOpen = await checkFormStatus(token); setIsFormOpen(isOpen); diff --git a/portal-frontend/src/pages/FormsLandingPage.js b/portal-frontend/src/pages/FormsLandingPage.js index 3addbe7..ea0f607 100644 --- a/portal-frontend/src/pages/FormsLandingPage.js +++ b/portal-frontend/src/pages/FormsLandingPage.js @@ -7,6 +7,7 @@ import { WhiteBackground } from "../components/WhiteBackground"; import { FullPageLoadingSpinner } from "../components/FullPageLoadingSpinner"; import Button from "../components/Button"; import { getAllForms } from "../forms/forms"; +import { createGetAuthToken } from "../utils/authUtils"; import "./FormsLandingPage.css"; const FormsLandingPage = () => { @@ -14,71 +15,86 @@ const FormsLandingPage = () => { const navigate = useNavigate(); const [formsStatus, setFormsStatus] = useState({}); const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); const allForms = useMemo(() => getAllForms(), []); useEffect(() => { const fetchAllFormsStatus = async () => { - setLoading(true); - const token = await getAccessTokenSilently(); - const statusPromises = allForms.map(async (form) => { - try { - const statusRes = await axios.get( - `${process.env.REACT_APP_BACKEND_URL}/application/form-status`, - { - headers: { Authorization: `Bearer ${token}` }, - params: { form_key: form.formKey }, - } - ); - - const isOpen = Boolean(statusRes.data?.is_open); + try { + setLoading(true); + setError(null); + + const getAuthToken = createGetAuthToken(getAccessTokenSilently, setError); + const token = await getAuthToken(); + if (!token) { + setLoading(false); + return; + } - let hasSubmitted = false; + const statusPromises = allForms.map(async (form) => { try { - await axios.get( - `${process.env.REACT_APP_BACKEND_URL}/application`, + const statusRes = await axios.get( + `${process.env.REACT_APP_BACKEND_URL}/application/form-status`, { headers: { Authorization: `Bearer ${token}` }, params: { form_key: form.formKey }, } ); - hasSubmitted = true; - } catch (error) { - if (error.response?.status !== 404) { - console.error( - `Error checking submission for ${form.formKey}:`, - error + + const isOpen = Boolean(statusRes.data?.is_open); + + let hasSubmitted = false; + try { + await axios.get( + `${process.env.REACT_APP_BACKEND_URL}/application`, + { + headers: { Authorization: `Bearer ${token}` }, + params: { form_key: form.formKey }, + } ); + hasSubmitted = true; + } catch (error) { + if (error.response?.status !== 404) { + console.error( + `Error checking submission for ${form.formKey}:`, + error + ); + } } - } - return { - formKey: form.formKey, - isOpen, - hasSubmitted, - }; - } catch (error) { - console.error(`Error fetching status for ${form.formKey}:`, error); - return { - formKey: form.formKey, - isOpen: false, - hasSubmitted: false, - error: true, - }; - } - }); - - const results = await Promise.all(statusPromises); - const statusMap = {}; - results.forEach((result) => { - statusMap[result.formKey] = result; - }); - setFormsStatus(statusMap); - setLoading(false); + return { + formKey: form.formKey, + isOpen, + hasSubmitted, + }; + } catch (error) { + console.error(`Error fetching status for ${form.formKey}:`, error); + return { + formKey: form.formKey, + isOpen: false, + hasSubmitted: false, + error: true, + }; + } + }); + + const results = await Promise.all(statusPromises); + const statusMap = {}; + results.forEach((result) => { + statusMap[result.formKey] = result; + }); + setFormsStatus(statusMap); + setLoading(false); + } catch (err) { + console.error("Error in fetchAllFormsStatus:", err); + setError("Failed to load."); + setLoading(false); + } }; fetchAllFormsStatus(); - }, [getAccessTokenSilently, allForms]); + }, [allForms, getAccessTokenSilently]); const handleFormClick = (formKey) => { navigate(`/form?formKey=${formKey}`); @@ -110,6 +126,7 @@ const FormsLandingPage = () => { {loading && } + {error &&
{error}
}

Applications & Forms

diff --git a/portal-frontend/src/utils/authUtils.js b/portal-frontend/src/utils/authUtils.js new file mode 100644 index 0000000..680063d --- /dev/null +++ b/portal-frontend/src/utils/authUtils.js @@ -0,0 +1,58 @@ +import { useAuth0 } from "@auth0/auth0-react"; + +let loginWithRedirect = null; + +export const setLoginWithRedirect = (loginFn) => { + loginWithRedirect = loginFn; +}; + +export const createGetAuthToken = (getAccessTokenSilently, onError) => { + return async () => { + try { + return await getAccessTokenSilently(); + } catch (tokenError) { + const errorMessage = tokenError.message || ""; + const errorCode = tokenError.error || ""; + + console.error("Auth token error:", { + errorMessage, + errorCode, + tokenError, + }); + + if ( + errorMessage.includes("Missing Refresh Token") || + errorCode === "login_required" || + errorCode === "consent_required" || + errorCode === "missing_refresh_token" || + errorCode === "invalid_grant" + ) { + if (loginWithRedirect) { + loginWithRedirect(); + } else if (onError) { + onError("Session expired. Please log in again."); + } + throw tokenError; + } + + if (errorMessage.includes("timeout") || errorCode === "timeout") { + if (onError) { + onError("Authentication took too long. Please try again."); + } + throw tokenError; + } + + if (onError) { + onError( + `Authentication error: ${errorMessage || errorCode || "Unknown error"}` + ); + } + throw tokenError; + } + }; +}; + +export const useInitializeAuth = () => { + const { loginWithRedirect } = useAuth0(); + setLoginWithRedirect(loginWithRedirect); +};